All checks were successful
CI / test-go-consent (push) Successful in 27s
CI / test-python-voice (push) Successful in 25s
CI / test-bqas (push) Successful in 27s
CI / Deploy (push) Successful in 6s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
Adds investor-facing access controls, persistence, and PWA support to the pitch deck: - Passwordless magic-link auth (jose JWT + nodemailer SMTP) - Per-investor audit logging (logins, slide views, assumption changes, chat) - Financial model snapshot persistence (auto-save/restore per investor) - PWA support (manifest, service worker, offline caching, branded icons) - Safeguards: email watermark overlay, security headers, content protection, rate limiting, IP/new-IP detection, single active session per investor - Admin API: invite, list investors, revoke, query audit logs - pitch-deck service added to docker-compose.coolify.yml Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
245 lines
8.0 KiB
TypeScript
245 lines
8.0 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
|
|
}
|
|
|
|
export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout }: PitchDeckProps) {
|
|
const { data, loading, error } = usePitchData()
|
|
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>
|
|
)
|
|
}
|