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>
This commit is contained in:
93
pitch-deck/components/ui/UnitEconomicsCards.tsx
Normal file
93
pitch-deck/components/ui/UnitEconomicsCards.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user