Files
breakpilot-core/pitch-deck/components/ui/UnitEconomicsCards.tsx
Benjamin Boenisch f2a24d7341 feat: add pitch-deck service to core infrastructure
Migrated pitch-deck from breakpilot-pwa to breakpilot-core.
Container: bp-core-pitch-deck on port 3012.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 19:44:27 +01:00

94 lines
3.0 KiB
TypeScript

'use client'
import { motion } from 'framer-motion'
import AnimatedCounter from './AnimatedCounter'
interface UnitEconomicsCardsProps {
cac: number
ltv: number
ltvCacRatio: number
grossMargin: number
churnRate: number
lang: 'de' | 'en'
}
function MiniRing({ progress, color, size = 32 }: { progress: number; color: string; size?: number }) {
const radius = (size / 2) - 3
const circumference = 2 * Math.PI * radius
const offset = circumference - (Math.min(progress, 100) / 100) * circumference
return (
<svg width={size} height={size} className="shrink-0">
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="3" />
<motion.circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: circumference }}
animate={{ strokeDashoffset: offset }}
transition={{ duration: 1.5, ease: 'easeOut' }}
transform={`rotate(-90 ${size / 2} ${size / 2})`}
/>
</svg>
)
}
export default function UnitEconomicsCards({ cac, ltv, ltvCacRatio, grossMargin, churnRate, lang }: UnitEconomicsCardsProps) {
const cacPayback = cac > 0 ? Math.ceil(cac / ((ltv / (1 / (churnRate / 100))) || 1)) : 0
const cards = [
{
label: 'CAC Payback',
value: cacPayback,
suffix: lang === 'de' ? ' Mo.' : ' mo.',
ring: Math.min((cacPayback / 12) * 100, 100),
color: cacPayback <= 6 ? '#22c55e' : cacPayback <= 12 ? '#eab308' : '#ef4444',
sub: `CAC: ${cac.toLocaleString('de-DE')} EUR`,
},
{
label: 'LTV',
value: Math.round(ltv),
suffix: ' EUR',
ring: Math.min(ltvCacRatio * 10, 100),
color: ltvCacRatio >= 3 ? '#22c55e' : ltvCacRatio >= 1.5 ? '#eab308' : '#ef4444',
sub: `LTV/CAC: ${ltvCacRatio.toFixed(1)}x`,
},
{
label: 'Gross Margin',
value: grossMargin,
suffix: '%',
ring: grossMargin,
color: grossMargin >= 70 ? '#22c55e' : grossMargin >= 50 ? '#eab308' : '#ef4444',
sub: `Churn: ${churnRate}%`,
},
]
return (
<div className="grid grid-cols-3 gap-3">
{cards.map((card, i) => (
<motion.div
key={card.label}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 + i * 0.1 }}
className="bg-white/[0.06] backdrop-blur-xl border border-white/10 rounded-xl p-3 text-center"
>
<div className="flex justify-center mb-2">
<MiniRing progress={card.ring} color={card.color} />
</div>
<p className="text-sm font-bold text-white">
<AnimatedCounter target={card.value} suffix={card.suffix} duration={1000} />
</p>
<p className="text-[10px] text-white/40 mt-0.5">{card.label}</p>
<p className="text-[9px] text-white/25 mt-0.5">{card.sub}</p>
</motion.div>
))}
</div>
)
}