Files
breakpilot-core/pitch-deck/components/slides/CompetitionSlide.tsx
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

422 lines
20 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Language, PitchFeature, PitchCompetitor } from '@/lib/types'
import { t } from '@/lib/i18n'
import {
ChevronDown, ChevronRight, Globe, Building2, Users, TrendingUp,
DollarSign, Cpu, Star, Check, X, Minus, Shield,
} from 'lucide-react'
import GradientText from '../ui/GradientText'
import FadeInView from '../ui/FadeInView'
import GlassCard from '../ui/GlassCard'
import BrandName from '../ui/BrandName'
import {
type FeatureStatus, type ComparisonFeature,
EXTENDED_COMPETITORS, ALL_FEATURES, DACH_NOTE, PRICING_COMPARISON, APPSEC_COMPETITORS, APPSEC_FEATURES,
} from './CompetitionSlide.data'
import { StatusIcon, AiBadge, ratio, CompetitorCard, FeatureTable, AppSecCard, AppSecFeatureTable } from './CompetitionSlide.parts'
interface CompetitionSlideProps {
lang: Language
features: PitchFeature[]
competitors: PitchCompetitor[]
}
// ─── Helpers ───────────────────────────────────────────────────────────────────
// ─── Section Accordion ─────────────────────────────────────────────────────────
function SectionHeader({
label,
count,
open,
onToggle,
accent,
}: {
label: string
count: number
open: boolean
onToggle: () => void
accent?: string
}) {
return (
<button
onClick={onToggle}
className="w-full flex items-center gap-2 py-2 px-3 bg-white/[0.03] hover:bg-white/[0.06] border border-white/5 rounded-lg transition-colors text-left"
>
{open ? <ChevronDown className="w-4 h-4 text-white/40 shrink-0" /> : <ChevronRight className="w-4 h-4 text-white/40 shrink-0" />}
<span className={`text-sm font-semibold ${accent || 'text-white/80'}`}>{label}</span>
<span className="text-xs text-white/30 ml-1">({count})</span>
</button>
)
}
// ─── Component ─────────────────────────────────────────────────────────────────
type ViewTab = 'overview' | 'features' | 'pricing' | 'appsec'
export default function CompetitionSlide({ lang, features, competitors }: CompetitionSlideProps) {
const i = t(lang)
const [activeTab, setActiveTab] = useState<ViewTab>('overview')
const [openSections, setOpenSections] = useState<Set<string>>(new Set(['top5']))
const toggleSection = (key: string) => {
setOpenSections(prev => {
const next = new Set(prev)
if (next.has(key)) next.delete(key)
else next.add(key)
return next
})
}
const top5 = ALL_FEATURES.filter(f => f.isDiff)
const usps = ALL_FEATURES.filter(f => f.isUSP)
const allFeatures = ALL_FEATURES
const competitorCols = ['bp', 'vanta', 'drata', 'sprinto', 'proliance', 'dataguard', 'heydata'] as const
const competitorLabels = ['ComplAI', 'Vanta', 'Drata', 'Sprinto', 'Proliance', 'DataGuard', 'heyData']
const featureCount = ALL_FEATURES.length
const uspCount = usps.length
const subtitle = lang === 'de'
? `${featureCount} Features, ${uspCount} USPs — kein Anbieter kombiniert DSGVO + Code-Security + Self-Hosted KI`
: `${featureCount} features, ${uspCount} USPs — no provider combines GDPR + code security + self-hosted AI`
return (
<div className="max-h-[calc(100vh-120px)] overflow-y-auto pr-1 pb-4 scrollbar-thin">
{/* Header */}
<FadeInView className="text-center mb-4">
<h2 className="text-3xl md:text-4xl font-bold mb-2">
<GradientText>{i.competition.title}</GradientText>
</h2>
<p className="text-sm text-white/50 max-w-3xl mx-auto">{subtitle}</p>
</FadeInView>
{/* Tab Bar */}
<FadeInView delay={0.15} className="flex justify-center gap-2 mb-4 flex-wrap">
{([
{ key: 'overview' as ViewTab, de: 'Überblick & Vergleich', en: 'Overview & Comparison' },
{ key: 'features' as ViewTab, de: 'Feature-Matrix (Detail)', en: 'Feature Matrix (Detail)' },
{ key: 'pricing' as ViewTab, de: 'Pricing-Vergleich', en: 'Pricing Comparison' },
{ key: 'appsec' as ViewTab, de: 'Pentesting & AppSec', en: 'Pentesting & AppSec' },
]).map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-4 py-1.5 rounded-full text-xs font-medium transition-all ${
activeTab === tab.key
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
: 'bg-white/[0.04] text-white/40 border border-white/5 hover:bg-white/[0.08] animate-[pulse_3s_ease-in-out_infinite]'
}`}
>
{lang === 'de' ? tab.de : tab.en}
</button>
))}
</FadeInView>
{/* ─── Tab: Overview ─── */}
{activeTab === 'overview' && (
<FadeInView delay={0.2}>
{/* Competitor Profiles */}
<div className="mb-4">
{/* International */}
<div className="flex items-center gap-2 mb-2">
<Globe className="w-3.5 h-3.5 text-blue-400" />
<span className="text-xs font-semibold text-blue-400">{lang === 'de' ? 'International' : 'International'}</span>
</div>
<div className="grid grid-cols-3 gap-2 mb-3">
{EXTENDED_COMPETITORS.filter(c => c.isInternational).map(c => (
<CompetitorCard key={c.name} competitor={c} lang={lang} />
))}
</div>
{/* DACH */}
<div className="flex items-center gap-2 mb-2">
<Building2 className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-xs font-semibold text-emerald-400">DACH</span>
</div>
<div className="grid grid-cols-3 gap-2 mb-3">
{EXTENDED_COMPETITORS.filter(c => !c.isInternational).map(c => (
<CompetitorCard key={c.name} competitor={c} lang={lang} />
))}
</div>
</div>
{/* Efficiency Ratios */}
<GlassCard className="!p-3 mb-4" hover={false}>
<h4 className="text-xs font-semibold text-white/60 mb-2 flex items-center gap-1.5">
<TrendingUp className="w-3.5 h-3.5" />
{lang === 'de' ? 'Effizienz-Kennzahlen' : 'Efficiency Ratios'}
</h4>
<div className="overflow-x-auto">
<table className="w-full text-[11px]">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-1.5 px-2 text-white/40 font-medium">{lang === 'de' ? 'Kennzahl' : 'Metric'}</th>
{EXTENDED_COMPETITORS.map(c => (
<th key={c.name} className="py-1.5 px-2 text-white/50 font-medium text-center">{c.flag} {c.name}</th>
))}
</tr>
</thead>
<tbody>
<tr className="border-b border-white/5">
<td className="py-1.5 px-2 text-white/50">{lang === 'de' ? 'Umsatz / Mitarbeiter' : 'Revenue / Employee'}</td>
{EXTENDED_COMPETITORS.map(c => (
<td key={c.name} className="py-1.5 px-2 text-center text-white/70">
${ratio(c.revenueNum, c.employees)}
</td>
))}
</tr>
<tr className="border-b border-white/5">
<td className="py-1.5 px-2 text-white/50">{lang === 'de' ? 'Kunden / Mitarbeiter' : 'Customers / Employee'}</td>
{EXTENDED_COMPETITORS.map(c => (
<td key={c.name} className="py-1.5 px-2 text-center text-white/70">
{(c.customers / c.employees).toFixed(0)}
</td>
))}
</tr>
<tr>
<td className="py-1.5 px-2 text-white/50">{lang === 'de' ? 'Mitarbeiter' : 'Employees'}</td>
{EXTENDED_COMPETITORS.map(c => (
<td key={c.name} className="py-1.5 px-2 text-center text-white/70">
{c.employees.toLocaleString()}
</td>
))}
</tr>
</tbody>
</table>
</div>
</GlassCard>
{/* DACH Landscape Note */}
<div className="text-[11px] text-white/30 text-center italic">
{DACH_NOTE[lang]}
</div>
</FadeInView>
)}
{/* ─── Tab: Feature Matrix ─── */}
{activeTab === 'features' && (
<FadeInView delay={0.2}>
<div className="space-y-2">
{/* All Features */}
<div>
<SectionHeader
label={lang === 'de' ? 'Alle Features' : 'All Features'}
count={allFeatures.length}
open={openSections.has('all')}
onToggle={() => toggleSection('all')}
/>
{openSections.has('all') && (
<FeatureTable features={allFeatures} lang={lang} cols={competitorCols} labels={competitorLabels} />
)}
</div>
{/* USPs */}
<div>
<SectionHeader
label={lang === 'de' ? 'USP — nur COMPLAI' : 'USP — COMPLAI only'}
count={usps.length}
open={openSections.has('usp')}
onToggle={() => toggleSection('usp')}
accent="text-indigo-400"
/>
{openSections.has('usp') && (
<FeatureTable features={usps} lang={lang} cols={competitorCols} labels={competitorLabels} highlight />
)}
</div>
</div>
{/* Score Summary */}
<div className="mt-4 flex items-center justify-center gap-6">
{[
{ name: 'ComplAI', score: ALL_FEATURES.filter(f => f.bp === true).length, color: 'text-indigo-400' },
{ name: 'Vanta', score: ALL_FEATURES.filter(f => f.vanta === true).length, color: 'text-white/50' },
{ name: 'Drata', score: ALL_FEATURES.filter(f => f.drata === true).length, color: 'text-white/50' },
{ name: 'Sprinto', score: ALL_FEATURES.filter(f => f.sprinto === true).length, color: 'text-white/50' },
{ name: 'Proliance', score: ALL_FEATURES.filter(f => f.proliance === true).length, color: 'text-white/50' },
{ name: 'DataGuard', score: ALL_FEATURES.filter(f => f.dataguard === true).length, color: 'text-white/50' },
{ name: 'heyData', score: ALL_FEATURES.filter(f => f.heydata === true).length, color: 'text-white/50' },
].map(item => (
<div key={item.name} className="text-center">
<div className={`text-lg font-bold ${item.color}`}>{item.score}/{ALL_FEATURES.length}</div>
<div className="text-[10px] text-white/40">{item.name}</div>
</div>
))}
</div>
</FadeInView>
)}
{/* ─── Tab: Pricing ─── */}
{activeTab === 'pricing' && (
<FadeInView delay={0.2}>
<div className="overflow-x-auto">
<table className="w-full text-[11px] border-collapse">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-2 px-2 text-white/40 font-medium min-w-[90px]">{lang === 'de' ? 'Anbieter' : 'Provider'}</th>
<th className="py-2 px-1.5 text-white/40 font-medium text-center">{lang === 'de' ? 'Modell' : 'Model'}</th>
<th className="py-2 px-1.5 text-white/40 font-medium text-center">{lang === 'de' ? 'Einstieg' : 'Entry'}</th>
<th className="py-2 px-1.5 text-white/40 font-medium text-center">Mid</th>
<th className="py-2 px-1.5 text-white/40 font-medium text-center">Enterprise</th>
<th className="py-2 px-1.5 text-white/40 font-medium text-center">Setup</th>
<th className="py-2 px-1.5 text-white/40 font-medium text-center">{lang === 'de' ? 'Öffentlich' : 'Public'}</th>
</tr>
</thead>
<tbody>
{PRICING_COMPARISON.map((cp) => (
<tr key={cp.name} className={`border-b border-white/5 ${cp.isBP ? 'bg-indigo-500/5' : ''}`}>
<td className="py-2 px-2">
<div className="flex items-center gap-1.5">
<span>{cp.flag}</span>
<span className={`font-semibold ${cp.isBP ? 'text-indigo-400' : 'text-white/70'}`}>
{cp.isBP ? <BrandName className="text-[11px]" /> : cp.name}
</span>
</div>
</td>
<td className="py-2 px-1.5 text-center">
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${cp.model === 'Self-Hosted' ? 'bg-green-500/15 text-green-400' : 'bg-white/5 text-white/40'}`}>
{cp.model}
</span>
</td>
{cp.tiers.map((tier, idx) => (
<td key={idx} className="py-2 px-1.5 text-center">
<div className={`font-semibold ${cp.isBP ? 'text-indigo-300' : 'text-white/70'}`}>{tier.price}</div>
<div className="text-[10px] text-white/30">{tier.annual}</div>
<div className="text-[10px] text-white/25 mt-0.5">{lang === 'de' ? tier.notes.de : tier.notes.en}</div>
</td>
))}
<td className="py-2 px-1.5 text-center text-white/40">{cp.setupFee}</td>
<td className="py-2 px-1.5 text-center">
{cp.publicPricing
? <Check className="w-3.5 h-3.5 text-green-400 mx-auto" />
: <X className="w-3.5 h-3.5 text-white/15 mx-auto" />
}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-[10px] text-white/25 text-center mt-3 italic">
{lang === 'de'
? '~ = geschätzte Preise (nicht öffentlich). Alle Preise ohne MwSt. Stand: Q1 2026.'
: '~ = estimated pricing (not public). All prices excl. VAT. As of Q1 2026.'}
</p>
</FadeInView>
)}
{/* ─── Tab: AppSec / Pentesting ─── */}
{activeTab === 'appsec' && (
<FadeInView delay={0.2}>
{/* Intro */}
<GlassCard className="!p-3 mb-4" hover={false}>
<div className="flex items-start gap-2">
<Shield className="w-4 h-4 text-red-400 mt-0.5 shrink-0" />
<div className="text-[11px]">
<span className="text-white/80 font-semibold">
{lang === 'de' ? 'Warum ein 2. Wettbewerbsvergleich?' : 'Why a 2nd competitive comparison?'}
</span>
<p className="text-white/50 mt-1 leading-relaxed">
{lang === 'de'
? 'Kein Compliance-Anbieter (Vanta, Drata, etc.) bietet DAST, SAST oder LLM-basierte Code-Fixes. Kein AppSec-Anbieter (Snyk, Veracode, etc.) bietet DSGVO/AI-Act-Compliance. COMPLAI ist die einzige Plattform, die beides kombiniert.'
: 'No compliance vendor (Vanta, Drata, etc.) offers DAST, SAST, or LLM-based code fixes. No AppSec vendor (Snyk, Veracode, etc.) offers GDPR/AI Act compliance. COMPLAI is the only platform combining both.'}
</p>
</div>
</div>
</GlassCard>
{/* AppSec Competitor Cards */}
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Shield className="w-3.5 h-3.5 text-red-400" />
<span className="text-xs font-semibold text-red-400">{lang === 'de' ? 'AppSec / Pentesting Anbieter' : 'AppSec / Pentesting Providers'}</span>
</div>
<div className="grid grid-cols-4 gap-2">
{APPSEC_COMPETITORS.map(c => (
<AppSecCard key={c.name} competitor={c} lang={lang} />
))}
</div>
</div>
{/* Efficiency Ratios — AppSec */}
<GlassCard className="!p-3 mt-4 mb-4" hover={false}>
<h4 className="text-xs font-semibold text-white/60 mb-2 flex items-center gap-1.5">
<TrendingUp className="w-3.5 h-3.5" />
{lang === 'de' ? 'Effizienz-Kennzahlen' : 'Efficiency Ratios'}
</h4>
<div className="overflow-x-auto">
<table className="w-full text-[11px]">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-1.5 px-2 text-white/40 font-medium">{lang === 'de' ? 'Kennzahl' : 'Metric'}</th>
{APPSEC_COMPETITORS.map(c => (
<th key={c.name} className="py-1.5 px-2 text-white/50 font-medium text-center">{c.flag} {c.name}</th>
))}
</tr>
</thead>
<tbody>
<tr className="border-b border-white/5">
<td className="py-1.5 px-2 text-white/50">{lang === 'de' ? 'Umsatz / Mitarbeiter' : 'Revenue / Employee'}</td>
{APPSEC_COMPETITORS.map(c => (
<td key={c.name} className="py-1.5 px-2 text-center text-white/70">
${ratio(c.revenueNum, c.employees)}
</td>
))}
</tr>
<tr className="border-b border-white/5">
<td className="py-1.5 px-2 text-white/50">{lang === 'de' ? 'Mitarbeiter' : 'Employees'}</td>
{APPSEC_COMPETITORS.map(c => (
<td key={c.name} className="py-1.5 px-2 text-center text-white/70">
{c.employees.toLocaleString()}
</td>
))}
</tr>
</tbody>
</table>
</div>
</GlassCard>
{/* AppSec Feature Matrix */}
<div className="space-y-2 mt-4">
<div>
<SectionHeader
label={lang === 'de' ? 'Alle AppSec Features' : 'All AppSec Features'}
count={APPSEC_FEATURES.length}
open={openSections.has('appsec-all')}
onToggle={() => toggleSection('appsec-all')}
/>
{openSections.has('appsec-all') && (
<AppSecFeatureTable features={APPSEC_FEATURES} lang={lang} />
)}
</div>
</div>
{/* Score Summary */}
<div className="mt-4 flex items-center justify-center gap-4 flex-wrap">
{[
{ name: 'ComplAI', score: APPSEC_FEATURES.filter(f => f.bp === true).length, color: 'text-indigo-400' },
{ name: 'Snyk', score: APPSEC_FEATURES.filter(f => f.snyk === true).length, color: 'text-white/50' },
{ name: 'Veracode', score: APPSEC_FEATURES.filter(f => f.veracode === true).length, color: 'text-white/50' },
{ name: 'Checkmarx', score: APPSEC_FEATURES.filter(f => f.checkmarx === true).length, color: 'text-white/50' },
{ name: 'SonarSrc', score: APPSEC_FEATURES.filter(f => f.sonar === true).length, color: 'text-white/50' },
{ name: 'Semgrep', score: APPSEC_FEATURES.filter(f => f.semgrep === true).length, color: 'text-white/50' },
{ name: 'Pentera', score: APPSEC_FEATURES.filter(f => f.pentera === true).length, color: 'text-white/50' },
{ name: 'Invicti', score: APPSEC_FEATURES.filter(f => f.invicti === true).length, color: 'text-white/50' },
{ name: 'Intruder', score: APPSEC_FEATURES.filter(f => f.intruder === true).length, color: 'text-white/50' },
].map(item => (
<div key={item.name} className="text-center">
<div className={`text-lg font-bold ${item.color}`}>{item.score}/{APPSEC_FEATURES.length}</div>
<div className="text-[10px] text-white/40">{item.name}</div>
</div>
))}
</div>
</FadeInView>
)}
</div>
)
}