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>
144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
import { FMAssumption, Language } from '@/lib/types'
|
|
|
|
interface FinancialSlidersProps {
|
|
assumptions: FMAssumption[]
|
|
onAssumptionChange: (key: string, value: number) => void
|
|
lang: Language
|
|
}
|
|
|
|
function Slider({
|
|
assumption,
|
|
onChange,
|
|
lang,
|
|
}: {
|
|
assumption: FMAssumption
|
|
onChange: (value: number) => void
|
|
lang: Language
|
|
}) {
|
|
const value = typeof assumption.value === 'number' ? assumption.value : Number(assumption.value)
|
|
const label = lang === 'de' ? assumption.label_de : assumption.label_en
|
|
|
|
if (assumption.value_type === 'step') {
|
|
// Display step values as read-only list
|
|
const steps = Array.isArray(assumption.value) ? assumption.value : []
|
|
return (
|
|
<div className="space-y-1">
|
|
<p className="text-[11px] text-white/50">{label}</p>
|
|
<div className="flex gap-1.5">
|
|
{steps.map((s: number, i: number) => (
|
|
<div key={i} className="flex-1 text-center">
|
|
<p className="text-[9px] text-white/30">Y{i + 1}</p>
|
|
<p className="text-xs text-white font-mono">{s}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<div className="flex justify-between text-[11px]">
|
|
<span className="text-white/50">{label}</span>
|
|
<span className="font-mono text-white">{value}{assumption.unit === 'EUR' ? ' EUR' : assumption.unit === '%' ? '%' : ` ${assumption.unit || ''}`}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min={assumption.min_value ?? 0}
|
|
max={assumption.max_value ?? 100}
|
|
step={assumption.step_size ?? 1}
|
|
value={value}
|
|
onChange={(e) => onChange(parseFloat(e.target.value))}
|
|
className="w-full h-1.5 bg-white/10 rounded-full appearance-none cursor-pointer
|
|
[&::-webkit-slider-thumb]:appearance-none
|
|
[&::-webkit-slider-thumb]:w-3.5
|
|
[&::-webkit-slider-thumb]:h-3.5
|
|
[&::-webkit-slider-thumb]:rounded-full
|
|
[&::-webkit-slider-thumb]:bg-indigo-500
|
|
[&::-webkit-slider-thumb]:shadow-lg
|
|
[&::-webkit-slider-thumb]:shadow-indigo-500/30
|
|
[&::-webkit-slider-thumb]:cursor-pointer
|
|
"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface CategoryGroup {
|
|
key: string
|
|
label: string
|
|
items: FMAssumption[]
|
|
}
|
|
|
|
export default function FinancialSliders({ assumptions, onAssumptionChange, lang }: FinancialSlidersProps) {
|
|
const [openCategories, setOpenCategories] = useState<Set<string>>(new Set(['revenue']))
|
|
|
|
// Group assumptions by category
|
|
const categories: CategoryGroup[] = [
|
|
{ key: 'revenue', label: lang === 'de' ? 'Revenue' : 'Revenue', items: [] },
|
|
{ key: 'costs', label: lang === 'de' ? 'Kosten' : 'Costs', items: [] },
|
|
{ key: 'team', label: 'Team', items: [] },
|
|
{ key: 'funding', label: 'Funding', items: [] },
|
|
]
|
|
|
|
for (const a of assumptions) {
|
|
const cat = categories.find(c => c.key === a.category) || categories[0]
|
|
cat.items.push(a)
|
|
}
|
|
|
|
const toggleCategory = (key: string) => {
|
|
setOpenCategories(prev => {
|
|
const next = new Set(prev)
|
|
if (next.has(key)) next.delete(key)
|
|
else next.add(key)
|
|
return next
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{categories.filter(c => c.items.length > 0).map((cat) => {
|
|
const isOpen = openCategories.has(cat.key)
|
|
return (
|
|
<div key={cat.key} className="border border-white/[0.06] rounded-xl overflow-hidden">
|
|
<button
|
|
onClick={() => toggleCategory(cat.key)}
|
|
className="w-full flex items-center justify-between px-3 py-2 text-xs text-white/60 hover:text-white/80 transition-colors"
|
|
>
|
|
<span className="font-medium">{cat.label}</span>
|
|
{isOpen ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
</button>
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="px-3 pb-3 space-y-3">
|
|
{cat.items.map((a) => (
|
|
<Slider
|
|
key={a.key}
|
|
assumption={a}
|
|
onChange={(val) => onAssumptionChange(a.key, val)}
|
|
lang={lang}
|
|
/>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|