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>
422 lines
20 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
|