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>
200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
'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>
|
||
)
|
||
}
|