Files
breakpilot-core/pitch-deck/components/slides/MilestonesSlide.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

200 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import { Language } from '@/lib/types'
import GradientText from '../ui/GradientText'
import FadeInView from '../ui/FadeInView'
import { type Milestone, MILESTONES, STATS } from './MilestonesSlide.data'
import { THEMES } from './MilestonesSlide.themes'
import { StarField, SoftGrid, Timeline, StatCard, DetailModal } from './MilestonesSlide.parts'
interface MilestonesSlideProps { lang: Language }
const MONO: React.CSSProperties = {
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
fontVariantNumeric: 'tabular-nums',
}
const CSS_KF = `
@keyframes msFlow { 0%{stroke-dashoffset:0} 100%{stroke-dashoffset:-18} }
@keyframes msFadeIn { from{opacity:0} to{opacity:1} }
@keyframes msScaleIn { from{opacity:0;transform:scale(.94)} to{opacity:1;transform:scale(1)} }
@keyframes msHeadingDark {
0%,100%{text-shadow:0 0 22px rgba(167,139,250,.3)}
50% {text-shadow:0 0 40px rgba(167,139,250,.6)}
}
@keyframes msHeadingLight {
0%,100%{text-shadow:0 0 22px rgba(124,58,237,.15)}
50% {text-shadow:0 0 36px rgba(124,58,237,.30)}
}
@keyframes msPulse {
0%,100%{r:9;opacity:.4}
50% {r:14;opacity:.05}
}
`
// ── Light mode hook ───────────────────────────────────────────────────────────
function useIsLight() {
const [isLight, setIsLight] = useState(false)
useEffect(() => {
const check = () => setIsLight(document.documentElement.classList.contains('theme-light'))
check()
const obs = new MutationObserver(check)
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
return () => obs.disconnect()
}, [])
return isLight
}
// ── Inner slide (fixed 1280×680) ──────────────────────────────────────────────
function MilestonesInner({ t, de, sel, setSel }: {
t: typeof THEMES.dark; de: boolean
sel: Milestone | null
setSel: (m: Milestone | null) => void
}) {
const doneCnt = useMemo(() => MILESTONES.filter(m => m.done).length, [])
const total = MILESTONES.length
return (
<div style={{
position: 'relative', width: 1280, height: 600, overflow: 'hidden',
background: t.bg, color: t.fg,
fontFamily: '"Inter", system-ui, sans-serif', WebkitFontSmoothing: 'antialiased',
}}>
{/* Ambient glow */}
<div style={{
position: 'absolute', top: -120, left: '50%', transform: 'translateX(-50%)',
width: 800, height: 500, borderRadius: '50%',
background: t.ambient, filter: 'blur(50px)', pointerEvents: 'none',
}} />
{t.stars ? <StarField /> : <SoftGrid t={t} />}
{/* Progress indicator */}
<div style={{
position: 'absolute', top: 36, right: 52, display: 'flex', alignItems: 'center', gap: 10, zIndex: 3,
}}>
<div style={{ ...MONO, fontSize: 10, letterSpacing: 2, color: t.fgMuted, textTransform: 'uppercase' as const, fontWeight: 700 }}>
{de ? 'Fortschritt' : 'Progress'}
</div>
<div style={{
width: 120, height: 6, background: t.progressTrackBg, borderRadius: 3, overflow: 'hidden',
border: `1px solid ${t.progressTrackBorder}`,
}}>
<div style={{
width: `${(doneCnt / total) * 100}%`, height: '100%',
background: `linear-gradient(90deg, ${t.done}, ${t.accent})`,
boxShadow: `0 0 12px ${t.done}99`,
}} />
</div>
<div style={{ ...MONO, fontSize: 11, color: t.fg, fontWeight: 700 }}>
<span style={{ color: t.done }}>{doneCnt}</span>
<span style={{ color: t.fgWhisper }}> / {total}</span>
</div>
</div>
{/* Tip */}
<div style={{
position: 'absolute', top: 36, left: 52, ...MONO, fontSize: 10,
letterSpacing: 2, color: t.fgGhost, textTransform: 'uppercase' as const, fontWeight: 700,
display: 'flex', alignItems: 'center', gap: 8, zIndex: 3,
}}>
<span>{de ? 'Tipp:' : 'Tip:'}</span>
<span style={{ color: t.accent70 }}>{de ? 'Klick auf einen Meilenstein' : 'Click any milestone'}</span>
</div>
{/* Timeline */}
<div style={{ position: 'relative', marginTop: 68 }}>
<Timeline onSelect={setSel} selectedId={sel?.id ?? null} t={t} de={de} />
</div>
{/* Stats */}
<div style={{
position: 'absolute', left: 40, right: 40, bottom: 36,
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 14,
}}>
{STATS.map(s => <StatCard key={s.tint} item={s} t={t} de={de} />)}
</div>
{/* Footer */}
<div style={{
position: 'absolute', left: 0, right: 0, bottom: 14, textAlign: 'center',
...MONO, fontSize: 9, letterSpacing: 3, color: t.accent40,
textTransform: 'uppercase' as const, fontWeight: 700,
}}>
{de ? 'Stand heute · live-Metriken aus der Plattform' : 'As of today · live metrics from the platform'}
</div>
<DetailModal item={sel} onClose={() => setSel(null)} t={t} de={de} />
</div>
)
}
// ── Main slide ────────────────────────────────────────────────────────────────
const INNER_W = 1280
const INNER_H = 600
export default function MilestonesSlide({ lang }: MilestonesSlideProps) {
const de = lang === 'de'
const isLight = useIsLight()
const t = isLight ? THEMES.light : THEMES.dark
const [sel, setSel] = useState<Milestone | null>(null)
const [scale, setScale] = useState(1)
const containerRef = useRef<HTMLDivElement>(null)
const calcScale = useCallback(() => {
if (containerRef.current) {
const w = containerRef.current.offsetWidth
setScale(Math.min(w / INNER_W, 1))
}
}, [])
useEffect(() => {
calcScale()
const obs = new ResizeObserver(calcScale)
if (containerRef.current) obs.observe(containerRef.current)
return () => obs.disconnect()
}, [calcScale])
return (
<div>
<style>{CSS_KF}</style>
<FadeInView className="text-center mb-1">
<h2 className="text-5xl md:text-6xl font-bold mb-1">
<GradientText>{de ? 'Meilensteine' : 'Milestones'}</GradientText>
</h2>
<p className="text-lg text-white/50 max-w-2xl mx-auto">
{de ? 'Von der Idee zur GmbH — was wir bereits erreicht haben' : 'From idea to GmbH — what we have already achieved'}
</p>
</FadeInView>
<FadeInView delay={0.1}>
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: INNER_H * scale,
overflow: 'hidden',
borderRadius: 16,
transform: 'scale(1.12)',
transformOrigin: 'top center',
marginBottom: -40,
}}
>
<div style={{
position: 'absolute', top: 0, left: 0,
width: INNER_W, height: INNER_H,
transform: `scale(${scale})`,
transformOrigin: 'top left',
}}>
<MilestonesInner t={t} de={de} sel={sel} setSel={setSel} />
</div>
</div>
</FadeInView>
</div>
)
}