Some checks failed
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 32s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
CI / Deploy (push) Failing after 4s
Build pitch-deck / build-and-push (push) Failing after 59s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
Replaces raw JSON textarea in version editor with proper form UIs: - Company: single-record form with side-by-side DE/EN tagline + mission - Team: expandable card list with bilingual role/bio, expertise tags - Financials: year-by-year table with numeric inputs - Market: TAM/SAM/SOM row table - Competitors: card list with strengths/weaknesses tag arrays - Features: card list with DE/EN names + checkbox matrix - Milestones: card list with DE/EN title/description + status dropdown - Metrics: card list with DE/EN labels - Funding: form + nested use_of_funds table - Products: card list with DE/EN capabilities + feature tag arrays - FM Scenarios: card list with color picker - FM Assumptions: row table Shared editor primitives (components/pitch-admin/editors/): BilingualField, FormField, ArrayField, RowTable, CardList "Edit as JSON" toggle preserved as escape hatch on every tab. Preview: admin clicks "Preview" on version editor → opens /pitch-preview/[versionId] in new tab showing the full pitch deck with that version's data. Admin-cookie gated (no investor auth). Yellow "PREVIEW MODE" banner at top. Also fixes the [object Object] inline table type cast in FM editor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
249 lines
8.2 KiB
TypeScript
249 lines
8.2 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useState } from 'react'
|
|
import { AnimatePresence } from 'framer-motion'
|
|
import { useSlideNavigation } from '@/lib/hooks/useSlideNavigation'
|
|
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 { Investor } from '@/lib/hooks/useAuth'
|
|
|
|
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 TractionSlide from './slides/TractionSlide'
|
|
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'
|
|
|
|
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 nav = useSlideNavigation()
|
|
const [fabOpen, setFabOpen] = useState(false)
|
|
|
|
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 'cover':
|
|
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
|
case 'problem':
|
|
return <ProblemSlide lang={lang} />
|
|
case 'solution':
|
|
return <SolutionSlide 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} />
|
|
case 'traction':
|
|
return <TractionSlide lang={lang} milestones={data.milestones} metrics={data.metrics} />
|
|
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} />
|
|
case 'the-ask':
|
|
return <TheAskSlide lang={lang} funding={data.funding} />
|
|
case 'ai-qa':
|
|
return <AIQASlide lang={lang} />
|
|
case 'annex-assumptions':
|
|
return <AssumptionsSlide lang={lang} />
|
|
case 'annex-architecture':
|
|
return <ArchitectureSlide lang={lang} />
|
|
case 'annex-gtm':
|
|
return <GTMSlide lang={lang} />
|
|
case 'annex-regulatory':
|
|
return <RegulatorySlide lang={lang} />
|
|
case 'annex-engineering':
|
|
return <EngineeringSlide lang={lang} />
|
|
case 'annex-aipipeline':
|
|
return <AIPipelineSlide 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} />}
|
|
|
|
<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}
|
|
/>
|
|
|
|
{/* 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}
|
|
/>
|
|
|
|
<AnimatePresence>
|
|
{nav.showOverview && (
|
|
<SlideOverview
|
|
currentIndex={nav.currentIndex}
|
|
onGoToSlide={nav.goToSlide}
|
|
onClose={() => nav.setShowOverview(false)}
|
|
lang={lang}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|