Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
446 changes: 132 additions & 314 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
'use client';

import { useEffect, useState } from 'react';
import { AlertTriangle, CheckCircle2, Loader2, Shield, ShieldAlert } from 'lucide-react';

interface Finding {
severity: 'critical' | 'high' | 'medium' | 'low';
title: string;
location: string;
}

const findings: Finding[] = [
{ severity: 'critical', title: 'SQL Injection in /api/users', location: 'POST /api/users?search=' },
{ severity: 'high', title: 'Stored XSS in comments', location: 'POST /api/comments' },
{ severity: 'high', title: 'Broken access control', location: 'GET /api/admin/settings' },
{ severity: 'medium', title: 'Missing rate limiting', location: 'POST /api/auth/login' },
{ severity: 'medium', title: 'Insecure CORS policy', location: 'Origin: *' },
{ severity: 'low', title: 'Missing security headers', location: 'X-Frame-Options' },
];

const agents = [
'Reconnaissance',
'Authentication testing',
'Injection testing',
'Access control audit',
'Configuration review',
];

const severityColors = {
critical: 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-400',
high: 'bg-orange-100 text-orange-700 dark:bg-orange-950/40 dark:text-orange-400',
medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-950/40 dark:text-yellow-400',
low: 'bg-blue-100 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400',
};

export function PentestPreviewAnimation() {
const [progress, setProgress] = useState(0);
const [currentAgent, setCurrentAgent] = useState(0);
const [visibleFindings, setVisibleFindings] = useState(0);
const [phase, setPhase] = useState<'scanning' | 'complete'>('scanning');

useEffect(() => {
const totalDuration = 8000;
const interval = 50;
let elapsed = 0;

let pausing = false;

const timer = setInterval(() => {
if (pausing) return;

elapsed += interval;
const t = elapsed / totalDuration;

if (t >= 1) {
setPhase('complete');
setProgress(100);
setVisibleFindings(findings.length);
setCurrentAgent(agents.length - 1);

// Pause, then reset once
pausing = true;
setTimeout(() => {
elapsed = 0;
pausing = false;
setPhase('scanning');
setProgress(0);
setVisibleFindings(0);
setCurrentAgent(0);
}, 3000);
return;
}

// Progress with easing
const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
setProgress(Math.round(eased * 100));

// Cycle through agents
setCurrentAgent(Math.min(Math.floor(t * agents.length), agents.length - 1));

// Reveal findings progressively
setVisibleFindings(Math.floor(t * (findings.length + 1)));
}, interval);

return () => clearInterval(timer);
}, []);

const isComplete = phase === 'complete';

return (
<div className="px-5 py-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
{isComplete ? (
<ShieldAlert className="h-4 w-4 text-orange-500" />
) : (
<Shield className="h-4 w-4 text-primary animate-pulse" />
)}
<span className="font-semibold text-sm">app.acme.com</span>
{isComplete ? (
<span className="rounded-full bg-green-100 text-green-700 dark:bg-green-950/40 dark:text-green-400 px-2 py-0.5 text-[10px] font-medium">
Complete
</span>
) : (
<span className="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-[10px] font-medium">
Running
</span>
)}
</div>
<span className="text-xs text-muted-foreground font-mono">{progress}%</span>
</div>

{/* Progress bar */}
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden mb-4">
<div
className="h-full rounded-full transition-all duration-100 ease-out"
style={{
width: `${progress}%`,
backgroundColor: isComplete ? 'var(--color-green-500, #22c55e)' : 'var(--color-primary)',
}}
/>
</div>

{/* Current agent */}
{!isComplete && (
<div className="flex items-center gap-2 mb-4 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin text-primary" />
<span>{agents[currentAgent]}…</span>
<span className="ml-auto">{currentAgent + 1}/{agents.length} agents</span>
</div>
)}

{isComplete && (
<div className="flex items-center gap-2 mb-4 text-xs text-muted-foreground">
<CheckCircle2 className="h-3 w-3 text-green-500" />
<span>Scan complete — {findings.length} findings</span>
<span className="ml-auto">5/5 agents</span>
</div>
)}

{/* Findings */}
<div className="space-y-0 divide-y">
{findings.slice(0, visibleFindings).map((finding, i) => (
<div
key={finding.title}
className="flex items-center justify-between py-2.5 animate-in fade-in slide-in-from-bottom-1 duration-300"
style={{ animationDelay: `${i * 50}ms` }}
>
<div className="flex items-center gap-2.5 min-w-0">
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<p className="text-xs font-medium truncate">{finding.title}</p>
<p className="text-[10px] text-muted-foreground font-mono truncate">{finding.location}</p>
</div>
</div>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase ${severityColors[finding.severity]}`}>
{finding.severity}
</span>
</div>
))}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Organization } from '@db';
import { AnimatePresence, motion } from 'framer-motion';
import { AlertCircle, Loader2 } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Balancer from 'react-wrap-balancer';

import { usePostPaymentOnboarding } from '../hooks/usePostPaymentOnboarding';

interface PostPaymentOnboardingProps {
Expand Down Expand Up @@ -157,12 +157,12 @@ export function PostPaymentOnboarding({
<div className="mb-8">
<AnimatedWrapper delay={800} animationKey={`title-${step?.key}`}>
<h1 className="text-2xl md:text-4xl font-bold text-foreground mb-2">
<Balancer>{step?.question || ''}</Balancer>
<span className="text-balance">{step?.question || ''}</span>
</h1>
</AnimatedWrapper>
<AnimatedWrapper delay={1000} animationKey={`subtitle-${step?.key}`}>
<p className="text-md md:text-lg text-muted-foreground flex items-center flex-wrap">
<Balancer>Our AI will personalize the platform based on your answers.</Balancer>
<span className="text-balance">Our AI will personalize the platform based on your answers.</span>
</p>
</AnimatedWrapper>
</div>
Expand Down
Loading
Loading