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>
94 lines
3.0 KiB
TypeScript
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>
|
|
)
|
|
}
|