be126a7a39
Build pitch-deck / build-push-deploy (push) Successful in 1m22s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 31s
CI / test-python-voice (push) Successful in 30s
CI / test-bqas (push) Successful in 31s
NavigationFAB and SlideOverview now accept slideNames prop and render only the active slide list (filtered for showcase mode). Adds AI presenter start button to the FAB footer so it's accessible even when intro-presenter slide is hidden. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
329 lines
12 KiB
TypeScript
329 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import { AnimatePresence } from 'framer-motion'
|
|
import { useSlideNavigation, SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
|
|
import { SHOWCASE_HIDDEN_SLIDES } from '@/lib/slide-order'
|
|
import { useKeyboard } from '@/lib/hooks/useKeyboard'
|
|
import { usePitchData } from '@/lib/hooks/usePitchData'
|
|
import { usePresenterMode } from '@/lib/hooks/usePresenterMode'
|
|
import { useAuditTracker } from '@/lib/hooks/useAuditTracker'
|
|
import { Language, PitchData } from '@/lib/types'
|
|
import { t } from '@/lib/i18n'
|
|
import { Investor } from '@/lib/hooks/useAuth'
|
|
|
|
import Link from 'next/link'
|
|
import { FolderOpen } from 'lucide-react'
|
|
import ParticleBackground from './ParticleBackground'
|
|
import ProgressBar from './ProgressBar'
|
|
import NavigationControls from './NavigationControls'
|
|
import NavigationFAB from './NavigationFAB'
|
|
import ChatFAB from './ChatFAB'
|
|
import SlideOverview from './SlideOverview'
|
|
import SlideContainer from './SlideContainer'
|
|
import PresenterOverlay from './presenter/PresenterOverlay'
|
|
import AvatarPlaceholder from './presenter/AvatarPlaceholder'
|
|
import Watermark from './Watermark'
|
|
|
|
import IntroPresenterSlide from './slides/IntroPresenterSlide'
|
|
import CoverSlide from './slides/CoverSlide'
|
|
import ProblemSlide from './slides/ProblemSlide'
|
|
import SolutionSlide from './slides/SolutionSlide'
|
|
import ProductSlide from './slides/ProductSlide'
|
|
import HowItWorksSlide from './slides/HowItWorksSlide'
|
|
import MarketSlide from './slides/MarketSlide'
|
|
import BusinessModelSlide from './slides/BusinessModelSlide'
|
|
import CompetitionSlide from './slides/CompetitionSlide'
|
|
import TeamSlide from './slides/TeamSlide'
|
|
import FinancialsSlide from './slides/FinancialsSlide'
|
|
import TheAskSlide from './slides/TheAskSlide'
|
|
import AIQASlide from './slides/AIQASlide'
|
|
import AssumptionsSlide from './slides/AssumptionsSlide'
|
|
import ArchitectureSlide from './slides/ArchitectureSlide'
|
|
import GTMSlide from './slides/GTMSlide'
|
|
import RegulatorySlide from './slides/RegulatorySlide'
|
|
import EngineeringSlide from './slides/EngineeringSlide'
|
|
import AIPipelineSlide from './slides/AIPipelineSlide'
|
|
import USPSlide from './slides/USPSlide'
|
|
import DisclaimerSlide from './slides/DisclaimerSlide'
|
|
import ExecutiveSummarySlide from './slides/ExecutiveSummarySlide'
|
|
import RegulatoryLandscapeSlide from './slides/RegulatoryLandscapeSlide'
|
|
import CapTableSlide from './slides/CapTableSlide'
|
|
import SavingsSlide from './slides/SavingsSlide'
|
|
import SDKDemoSlide from './slides/SDKDemoSlide'
|
|
import StrategySlide from './slides/StrategySlide'
|
|
import FinanzplanSlide from './slides/FinanzplanSlide'
|
|
import GlossarySlide from './slides/GlossarySlide'
|
|
import RiskSlide from './slides/RiskSlide'
|
|
import MilestonesSlide from './slides/MilestonesSlide'
|
|
|
|
interface PitchDeckProps {
|
|
lang: Language
|
|
onToggleLanguage: () => void
|
|
investor: Investor | null
|
|
onLogout: () => void
|
|
previewData?: PitchData | null
|
|
}
|
|
|
|
export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, previewData }: PitchDeckProps) {
|
|
const fetched = usePitchData()
|
|
const data = previewData || fetched.data
|
|
const loading = previewData ? false : fetched.loading
|
|
const error = previewData ? null : fetched.error
|
|
const [fabOpen, setFabOpen] = useState(false)
|
|
const isWandeldarlehen = (data?.funding?.instrument || '').toLowerCase() === 'wandeldarlehen'
|
|
const isShowcase = investor?.is_showcase === true
|
|
|
|
// Derive fp_scenario IDs from version snapshot (fm_scenarios stores fp_scenario IDs directly)
|
|
const fpScenarios = data?.fp_scenarios || []
|
|
const fpBaseScenarioId = fpScenarios.find(s => s.is_default)?.id ?? fpScenarios[0]?.id ?? null
|
|
const preferredScenarioId = fpBaseScenarioId
|
|
|
|
// Showcase mode: filter out investor/financial slides
|
|
const activeSlideOrder = isShowcase
|
|
? SLIDE_ORDER.filter(s => !SHOWCASE_HIDDEN_SLIDES.has(s))
|
|
: SLIDE_ORDER
|
|
|
|
const nav = useSlideNavigation(activeSlideOrder)
|
|
|
|
// Map active slide IDs → localized names for sidebar/overview
|
|
const i18n = t(lang)
|
|
const activeSlideNames = activeSlideOrder.map(id => {
|
|
const idx = SLIDE_ORDER.indexOf(id)
|
|
return idx >= 0 ? i18n.slideNames[idx] : id
|
|
})
|
|
|
|
// Skip cap-table slide for Wandeldarlehen versions
|
|
useEffect(() => {
|
|
if (nav.currentSlide === 'cap-table' && isWandeldarlehen) {
|
|
nav.nextSlide()
|
|
}
|
|
}, [nav.currentSlide, isWandeldarlehen, nav.nextSlide])
|
|
|
|
const presenter = usePresenterMode({
|
|
goToSlide: nav.goToSlide,
|
|
currentSlide: nav.currentIndex,
|
|
totalSlides: nav.totalSlides,
|
|
language: lang,
|
|
})
|
|
|
|
// Audit tracking
|
|
useAuditTracker({
|
|
investorId: investor?.id || null,
|
|
currentSlide: nav.currentSlide,
|
|
enabled: !!investor,
|
|
})
|
|
|
|
const toggleFullscreen = useCallback(() => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen()
|
|
} else {
|
|
document.exitFullscreen()
|
|
}
|
|
}, [])
|
|
|
|
const toggleMenu = useCallback(() => {
|
|
setFabOpen(prev => !prev)
|
|
}, [])
|
|
|
|
useKeyboard({
|
|
onNext: nav.nextSlide,
|
|
onPrev: nav.prevSlide,
|
|
onFirst: nav.goToFirst,
|
|
onLast: nav.goToLast,
|
|
onOverview: nav.toggleOverview,
|
|
onFullscreen: toggleFullscreen,
|
|
onLanguageToggle: onToggleLanguage,
|
|
onMenuToggle: toggleMenu,
|
|
onPresenterToggle: presenter.toggle,
|
|
onGoToSlide: nav.goToSlide,
|
|
enabled: !nav.showOverview,
|
|
})
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="w-12 h-12 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
<p className="text-white/40 text-sm">{lang === 'de' ? 'Lade Pitch-Daten...' : 'Loading pitch data...'}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<div className="h-screen flex items-center justify-center">
|
|
<div className="text-center max-w-md">
|
|
<p className="text-red-400 mb-2">{lang === 'de' ? 'Fehler beim Laden' : 'Loading error'}</p>
|
|
<p className="text-white/40 text-sm">{error || 'No data'}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function renderSlide() {
|
|
if (!data) return null
|
|
|
|
switch (nav.currentSlide) {
|
|
case 'intro-presenter':
|
|
return (
|
|
<IntroPresenterSlide
|
|
lang={lang}
|
|
onStartPresenter={presenter.start}
|
|
isPresenting={presenter.state !== 'idle'}
|
|
/>
|
|
)
|
|
case 'executive-summary':
|
|
return <ExecutiveSummarySlide lang={lang} data={data} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpBaseScenarioId={fpBaseScenarioId} />
|
|
case 'cover':
|
|
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
|
case 'problem':
|
|
return <ProblemSlide lang={lang} />
|
|
case 'solution':
|
|
return <SolutionSlide lang={lang} />
|
|
case 'usp':
|
|
return <USPSlide lang={lang} />
|
|
case 'regulatory-landscape':
|
|
return <RegulatoryLandscapeSlide lang={lang} />
|
|
case 'product':
|
|
return <ProductSlide lang={lang} products={data.products} />
|
|
case 'how-it-works':
|
|
return <HowItWorksSlide lang={lang} />
|
|
case 'market':
|
|
return <MarketSlide lang={lang} market={data.market} />
|
|
case 'business-model':
|
|
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
|
case 'traction':
|
|
return <MilestonesSlide lang={lang} />
|
|
case 'competition':
|
|
return <CompetitionSlide lang={lang} features={data.features} competitors={data.competitors} />
|
|
case 'team':
|
|
return <TeamSlide lang={lang} team={data.team} />
|
|
case 'financials':
|
|
return <FinancialsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpBaseScenarioId={fpBaseScenarioId} />
|
|
case 'the-ask':
|
|
return <TheAskSlide lang={lang} funding={data.funding} isWandeldarlehen={isWandeldarlehen} />
|
|
case 'cap-table':
|
|
if (isWandeldarlehen) return null
|
|
return <CapTableSlide lang={lang} />
|
|
case 'customer-savings':
|
|
return <SavingsSlide lang={lang} />
|
|
case 'ai-qa':
|
|
return <AIQASlide lang={lang} />
|
|
case 'annex-assumptions':
|
|
return <AssumptionsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpScenarios={fpScenarios} />
|
|
case 'annex-architecture':
|
|
return <ArchitectureSlide lang={lang} />
|
|
case 'annex-gtm':
|
|
return <GTMSlide lang={lang} isWandeldarlehen={isWandeldarlehen} />
|
|
case 'annex-regulatory':
|
|
return <RegulatorySlide lang={lang} />
|
|
case 'annex-engineering':
|
|
return <EngineeringSlide lang={lang} />
|
|
case 'annex-aipipeline':
|
|
return <AIPipelineSlide lang={lang} />
|
|
case 'annex-sdk-demo':
|
|
return <SDKDemoSlide lang={lang} />
|
|
case 'annex-strategy':
|
|
return <StrategySlide lang={lang} isWandeldarlehen={isWandeldarlehen} />
|
|
case 'annex-finanzplan':
|
|
return <FinanzplanSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpBaseScenarioId={fpBaseScenarioId} fpScenarios={fpScenarios} />
|
|
case 'annex-glossary':
|
|
return <GlossarySlide lang={lang} />
|
|
case 'risks':
|
|
return <RiskSlide lang={lang} />
|
|
case 'legal-disclaimer':
|
|
return <DisclaimerSlide lang={lang} />
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="h-screen relative overflow-hidden bg-gradient-to-br from-slate-950 via-[#0a0a1a] to-slate-950 select-none"
|
|
onContextMenu={(e) => e.preventDefault()}
|
|
>
|
|
<ParticleBackground />
|
|
<ProgressBar current={nav.currentIndex} total={nav.totalSlides} />
|
|
|
|
{/* Investor watermark */}
|
|
{investor && <Watermark text={investor.email} />}
|
|
|
|
{/* Data Room link — only for real investor sessions, not preview */}
|
|
{investor && !previewData && (
|
|
<Link
|
|
href="/dataroom"
|
|
className="fixed top-4 right-4 z-40 flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-white/[0.06] border border-white/[0.08] text-white/50 hover:text-white/80 hover:bg-white/[0.1] backdrop-blur-sm transition-all text-xs"
|
|
>
|
|
<FolderOpen className="w-3.5 h-3.5" />
|
|
Data Room
|
|
</Link>
|
|
)}
|
|
|
|
<SlideContainer slideKey={nav.currentSlide} direction={nav.direction}>
|
|
{renderSlide()}
|
|
</SlideContainer>
|
|
|
|
<NavigationControls
|
|
onPrev={nav.prevSlide}
|
|
onNext={nav.nextSlide}
|
|
isFirst={nav.isFirst}
|
|
isLast={nav.isLast}
|
|
current={nav.currentIndex}
|
|
total={nav.totalSlides}
|
|
/>
|
|
|
|
<ChatFAB
|
|
lang={lang}
|
|
currentSlide={nav.currentSlide}
|
|
currentIndex={nav.currentIndex}
|
|
visitedSlides={nav.visitedSlides}
|
|
onGoToSlide={nav.goToSlide}
|
|
presenterState={presenter.state}
|
|
onPresenterInterrupt={presenter.pause}
|
|
/>
|
|
|
|
<NavigationFAB
|
|
currentIndex={nav.currentIndex}
|
|
totalSlides={nav.totalSlides}
|
|
visitedSlides={nav.visitedSlides}
|
|
onGoToSlide={nav.goToSlide}
|
|
lang={lang}
|
|
onToggleLanguage={onToggleLanguage}
|
|
slideNames={activeSlideNames}
|
|
onPresenterStart={isShowcase ? presenter.start : undefined}
|
|
/>
|
|
|
|
{/* Presenter UI */}
|
|
<AvatarPlaceholder state={presenter.state} />
|
|
<PresenterOverlay
|
|
state={presenter.state}
|
|
currentIndex={nav.currentIndex}
|
|
totalSlides={nav.totalSlides}
|
|
progress={presenter.progress}
|
|
displayText={presenter.displayText}
|
|
lang={lang}
|
|
onPause={presenter.pause}
|
|
onResume={presenter.resume}
|
|
onStop={presenter.stop}
|
|
onSkip={presenter.skipSlide}
|
|
onPrev={presenter.prevSlide}
|
|
/>
|
|
|
|
<AnimatePresence>
|
|
{nav.showOverview && (
|
|
<SlideOverview
|
|
currentIndex={nav.currentIndex}
|
|
onGoToSlide={nav.goToSlide}
|
|
onClose={() => nav.setShowOverview(false)}
|
|
lang={lang}
|
|
slideNames={activeSlideNames}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|