Files
Sharang Parnerkar ea752088f6
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
feat(pitch-admin): structured form editors, bilingual fields, version preview
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>
2026-04-10 10:34:42 +02:00

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>
)
}