feat: Add Academy, Whistleblower, Incidents SDK modules, pitch-deck, blog and CI/CD config
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
- Academy, Whistleblower, Incidents frontend pages with API proxies and types - Vendor compliance API proxy route - Go backend handlers and models for all new SDK modules - Investor pitch-deck app with interactive slides - Blog section with DSGVO, AI Act, NIS2, glossary articles - MkDocs documentation site - CI/CD pipelines (Woodpecker, GitHub Actions), security scanning config - Planning and implementation documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
6
pitch-deck/.dockerignore
Normal file
6
pitch-deck/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
*.md
|
||||
.env*
|
||||
.DS_Store
|
||||
45
pitch-deck/Dockerfile
Normal file
45
pitch-deck/Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Set to production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy built assets
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
# Switch to non-root user
|
||||
USER nextjs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Set hostname
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "server.js"]
|
||||
183
pitch-deck/app/api/chat/route.ts
Normal file
183
pitch-deck/app/api/chat/route.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
|
||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
||||
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen2.5:32b'
|
||||
|
||||
const SYSTEM_PROMPT = `# Investor Agent — BreakPilot ComplAI
|
||||
|
||||
## Identitaet
|
||||
Du bist der BreakPilot ComplAI Investor Relations Agent. Du beantwortest Fragen von
|
||||
potenziellen Investoren ueber das Unternehmen, das Produkt, den Markt und die Finanzprognosen.
|
||||
Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen.
|
||||
|
||||
## Kernprinzipien
|
||||
- **Datengetrieben**: Beziehe dich immer auf die bereitgestellten Unternehmensdaten
|
||||
- **Praezise**: Nenne immer konkrete Zahlen, Prozentsaetze und Zeitraeume
|
||||
- **Begeisternd aber ehrlich**: Stelle das Unternehmen positiv dar, ohne zu uebertreiben
|
||||
- **Zweisprachig**: Antworte in der Sprache, in der die Frage gestellt wird
|
||||
|
||||
## Kernbotschaften (IMMER betonen wenn passend)
|
||||
1. AI-First: "Alles was durch KI loesbar ist, wird durch KI geloest. Kein klassischer Support, kein grosses Sales-Team."
|
||||
2. Skalierbarkeit: "10x Kunden ≠ 10x Personal. Die KI skaliert mit."
|
||||
3. Hardware-Differenzierung: "Datensouveraenitaet durch Self-Hosting auf Apple-Hardware."
|
||||
4. Kostenstruktur: "18 Mitarbeiter in 2030 bei 8.4 Mio EUR Umsatz."
|
||||
5. Marktchance: "12.4 Mrd EUR TAM, regulatorisch getrieben."
|
||||
|
||||
## Kommunikationsstil
|
||||
- Professionell, knapp und ueberzeugend
|
||||
- Strukturierte Antworten mit klaren Abschnitten
|
||||
- Zahlen hervorheben und kontextualisieren
|
||||
- Maximal 3-4 Absaetze pro Antwort
|
||||
|
||||
## IP-Schutz-Layer (KRITISCH)
|
||||
NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider.
|
||||
Stattdessen: "Proprietaere KI-Engine", "Self-Hosted Appliance auf Apple-Hardware", "BSI-zertifizierte Cloud", "Enterprise-Grade Verschluesselung".
|
||||
|
||||
## Erlaubt: Geschaeftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (oeffentlich), LLM-Groessen (32b/40b/1000b).`
|
||||
|
||||
async function loadPitchContext(): Promise<string> {
|
||||
try {
|
||||
const client = await pool.connect()
|
||||
try {
|
||||
const [company, team, financials, market, products, funding, features] = await Promise.all([
|
||||
client.query('SELECT * FROM pitch_company LIMIT 1'),
|
||||
client.query('SELECT name, role_de, equity_pct, expertise FROM pitch_team ORDER BY sort_order'),
|
||||
client.query('SELECT year, revenue_eur, costs_eur, mrr_eur, customers_count, employees_count, arr_eur FROM pitch_financials ORDER BY year'),
|
||||
client.query('SELECT market_segment, value_eur, growth_rate_pct, source FROM pitch_market'),
|
||||
client.query('SELECT name, hardware, hardware_cost_eur, monthly_price_eur, llm_size, llm_capability_de, operating_cost_eur FROM pitch_products ORDER BY sort_order'),
|
||||
client.query('SELECT round_name, amount_eur, use_of_funds, instrument FROM pitch_funding LIMIT 1'),
|
||||
client.query('SELECT feature_name_de, breakpilot, proliance, dataguard, heydata, is_differentiator FROM pitch_features WHERE is_differentiator = true'),
|
||||
])
|
||||
|
||||
return `
|
||||
## Unternehmensdaten (fuer praezise Antworten nutzen)
|
||||
|
||||
### Firma
|
||||
${JSON.stringify(company.rows[0], null, 2)}
|
||||
|
||||
### Team
|
||||
${JSON.stringify(team.rows, null, 2)}
|
||||
|
||||
### Finanzprognosen (5-Jahres-Plan)
|
||||
${JSON.stringify(financials.rows, null, 2)}
|
||||
|
||||
### Markt (TAM/SAM/SOM)
|
||||
${JSON.stringify(market.rows, null, 2)}
|
||||
|
||||
### Produkte
|
||||
${JSON.stringify(products.rows, null, 2)}
|
||||
|
||||
### Finanzierung
|
||||
${JSON.stringify(funding.rows[0], null, 2)}
|
||||
|
||||
### Differenzierende Features (nur bei ComplAI)
|
||||
${JSON.stringify(features.rows, null, 2)}
|
||||
`
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load pitch context from DB:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { message, history = [], lang = 'de' } = body
|
||||
|
||||
if (!message || typeof message !== 'string') {
|
||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const pitchContext = await loadPitchContext()
|
||||
|
||||
let systemContent = SYSTEM_PROMPT
|
||||
if (pitchContext) {
|
||||
systemContent += '\n' + pitchContext
|
||||
}
|
||||
systemContent += `\n\n## Aktuelle Sprache: ${lang === 'de' ? 'Deutsch' : 'English'}\nAntworte in ${lang === 'de' ? 'Deutsch' : 'English'}.`
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemContent },
|
||||
...history.slice(-10).map((h: { role: string; content: string }) => ({
|
||||
role: h.role === 'user' ? 'user' : 'assistant',
|
||||
content: h.content,
|
||||
})),
|
||||
{ role: 'user', content: message },
|
||||
]
|
||||
|
||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: OLLAMA_MODEL,
|
||||
messages,
|
||||
stream: true,
|
||||
options: {
|
||||
temperature: 0.4,
|
||||
num_predict: 4096,
|
||||
},
|
||||
}),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
})
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
const errorText = await ollamaResponse.text()
|
||||
console.error('Ollama error:', ollamaResponse.status, errorText)
|
||||
return NextResponse.json(
|
||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status}).` },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = ollamaResponse.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n').filter((l) => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line)
|
||||
if (json.message?.content) {
|
||||
controller.enqueue(encoder.encode(json.message.content))
|
||||
}
|
||||
} catch {
|
||||
// Partial JSON line, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stream read error:', error)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Investor agent chat error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
57
pitch-deck/app/api/data/route.ts
Normal file
57
pitch-deck/app/api/data/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const client = await pool.connect()
|
||||
|
||||
try {
|
||||
const [
|
||||
companyRes,
|
||||
teamRes,
|
||||
financialsRes,
|
||||
marketRes,
|
||||
competitorsRes,
|
||||
featuresRes,
|
||||
milestonesRes,
|
||||
metricsRes,
|
||||
fundingRes,
|
||||
productsRes,
|
||||
] = await Promise.all([
|
||||
client.query('SELECT * FROM pitch_company LIMIT 1'),
|
||||
client.query('SELECT * FROM pitch_team ORDER BY sort_order'),
|
||||
client.query('SELECT * FROM pitch_financials ORDER BY year'),
|
||||
client.query('SELECT * FROM pitch_market ORDER BY id'),
|
||||
client.query('SELECT * FROM pitch_competitors ORDER BY id'),
|
||||
client.query('SELECT * FROM pitch_features ORDER BY sort_order'),
|
||||
client.query('SELECT * FROM pitch_milestones ORDER BY sort_order'),
|
||||
client.query('SELECT * FROM pitch_metrics ORDER BY id'),
|
||||
client.query('SELECT * FROM pitch_funding LIMIT 1'),
|
||||
client.query('SELECT * FROM pitch_products ORDER BY sort_order'),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
company: companyRes.rows[0] || null,
|
||||
team: teamRes.rows,
|
||||
financials: financialsRes.rows,
|
||||
market: marketRes.rows,
|
||||
competitors: competitorsRes.rows,
|
||||
features: featuresRes.rows,
|
||||
milestones: milestonesRes.rows,
|
||||
metrics: metricsRes.rows,
|
||||
funding: fundingRes.rows[0] || null,
|
||||
products: productsRes.rows,
|
||||
})
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Database query error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load pitch data' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
77
pitch-deck/app/globals.css
Normal file
77
pitch-deck/app/globals.css
Normal file
@@ -0,0 +1,77 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0a1a;
|
||||
--bg-card: rgba(255, 255, 255, 0.08);
|
||||
--bg-card-hover: rgba(255, 255, 255, 0.12);
|
||||
--border-subtle: rgba(255, 255, 255, 0.1);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.6);
|
||||
--accent-indigo: #6366f1;
|
||||
--accent-purple: #a78bfa;
|
||||
--accent-blue: #60a5fa;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.glass {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.glass-hover:hover {
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #6366f1, #a78bfa, #60a5fa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-shadow-glow {
|
||||
text-shadow: 0 0 40px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
}
|
||||
21
pitch-deck/app/layout.tsx
Normal file
21
pitch-deck/app/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'BreakPilot ComplAI — Investor Pitch Deck',
|
||||
description: 'Datensouveraenitaet meets KI-Compliance. Pre-Seed Q4 2026.',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="de" className="dark">
|
||||
<body className="bg-[#0a0a1a] text-white antialiased overflow-hidden h-screen">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
15
pitch-deck/app/page.tsx
Normal file
15
pitch-deck/app/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import PitchDeck from '@/components/PitchDeck'
|
||||
|
||||
export default function Home() {
|
||||
const [lang, setLang] = useState<Language>('de')
|
||||
|
||||
const toggleLanguage = useCallback(() => {
|
||||
setLang(prev => prev === 'de' ? 'en' : 'de')
|
||||
}, [])
|
||||
|
||||
return <PitchDeck lang={lang} onToggleLanguage={toggleLanguage} />
|
||||
}
|
||||
24
pitch-deck/components/LanguageToggle.tsx
Normal file
24
pitch-deck/components/LanguageToggle.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
|
||||
interface LanguageToggleProps {
|
||||
lang: Language
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export default function LanguageToggle({ lang, onToggle }: LanguageToggleProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="fixed top-4 right-4 z-40 flex items-center gap-1
|
||||
px-3 py-1.5 rounded-full bg-white/[0.06] backdrop-blur-sm
|
||||
border border-white/10 text-xs font-medium
|
||||
hover:bg-white/[0.1] transition-all"
|
||||
>
|
||||
<span className={lang === 'de' ? 'text-white' : 'text-white/40'}>DE</span>
|
||||
<span className="text-white/20 mx-1">|</span>
|
||||
<span className={lang === 'en' ? 'text-white' : 'text-white/40'}>EN</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
70
pitch-deck/components/NavigationControls.tsx
Normal file
70
pitch-deck/components/NavigationControls.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
|
||||
interface NavigationControlsProps {
|
||||
onPrev: () => void
|
||||
onNext: () => void
|
||||
isFirst: boolean
|
||||
isLast: boolean
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export default function NavigationControls({
|
||||
onPrev,
|
||||
onNext,
|
||||
isFirst,
|
||||
isLast,
|
||||
current,
|
||||
total,
|
||||
}: NavigationControlsProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Left Arrow */}
|
||||
<AnimatePresence>
|
||||
{!isFirst && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -10 }}
|
||||
onClick={onPrev}
|
||||
className="fixed left-4 top-1/2 -translate-y-1/2 z-40
|
||||
w-10 h-10 rounded-full bg-white/[0.08] backdrop-blur-sm
|
||||
border border-white/10 flex items-center justify-center
|
||||
hover:bg-white/[0.15] transition-all group"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-white/60 group-hover:text-white" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Right Arrow */}
|
||||
<AnimatePresence>
|
||||
{!isLast && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
onClick={onNext}
|
||||
className="fixed right-4 top-1/2 -translate-y-1/2 z-40
|
||||
w-10 h-10 rounded-full bg-white/[0.08] backdrop-blur-sm
|
||||
border border-white/10 flex items-center justify-center
|
||||
hover:bg-white/[0.15] transition-all group"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-white/60 group-hover:text-white" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Slide Counter */}
|
||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-40
|
||||
px-3 py-1.5 rounded-full bg-white/[0.06] backdrop-blur-sm
|
||||
border border-white/10 text-xs text-white/40 font-mono"
|
||||
>
|
||||
{current + 1} / {total}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
160
pitch-deck/components/NavigationFAB.tsx
Normal file
160
pitch-deck/components/NavigationFAB.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Menu, X, Maximize, Minimize, Bot } from 'lucide-react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface NavigationFABProps {
|
||||
currentIndex: number
|
||||
totalSlides: number
|
||||
visitedSlides: Set<number>
|
||||
onGoToSlide: (index: number) => void
|
||||
lang: Language
|
||||
onToggleLanguage: () => void
|
||||
}
|
||||
|
||||
export default function NavigationFAB({
|
||||
currentIndex,
|
||||
totalSlides,
|
||||
visitedSlides,
|
||||
onGoToSlide,
|
||||
lang,
|
||||
onToggleLanguage,
|
||||
}: NavigationFABProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const i = t(lang)
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen()
|
||||
setIsFullscreen(true)
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
setIsFullscreen(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<AnimatePresence mode="wait">
|
||||
{!isOpen ? (
|
||||
<motion.button
|
||||
key="fab"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="w-14 h-14 rounded-full bg-indigo-600 hover:bg-indigo-500
|
||||
flex items-center justify-center shadow-lg shadow-indigo-600/30
|
||||
transition-colors"
|
||||
>
|
||||
<Menu className="w-6 h-6 text-white" />
|
||||
</motion.button>
|
||||
) : (
|
||||
<motion.div
|
||||
key="panel"
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="w-[300px] max-h-[80vh] rounded-2xl overflow-hidden
|
||||
bg-black/80 backdrop-blur-xl border border-white/10
|
||||
shadow-2xl shadow-black/50"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
||||
<span className="text-sm font-semibold text-white">{i.nav.slides}</span>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
|
||||
hover:bg-white/20 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Slide List */}
|
||||
<div className="overflow-y-auto max-h-[55vh] py-2">
|
||||
{i.slideNames.map((name, idx) => {
|
||||
const isActive = idx === currentIndex
|
||||
const isVisited = visitedSlides.has(idx)
|
||||
const isAI = idx === totalSlides - 1
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => onGoToSlide(idx)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-4 py-2.5 text-left
|
||||
transition-all text-sm
|
||||
${isActive
|
||||
? 'bg-indigo-500/20 border-l-2 border-indigo-500 text-white'
|
||||
: 'hover:bg-white/[0.06] text-white/60 hover:text-white border-l-2 border-transparent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className={`
|
||||
w-6 h-6 rounded-full flex items-center justify-center text-xs font-mono shrink-0
|
||||
${isActive
|
||||
? 'bg-indigo-500 text-white'
|
||||
: isVisited
|
||||
? 'bg-white/10 text-white/60'
|
||||
: 'bg-white/5 text-white/30'
|
||||
}
|
||||
`}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{name}</span>
|
||||
{isAI && <Bot className="w-4 h-4 text-indigo-400 shrink-0" />}
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-indigo-400 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-white/10 px-4 py-3 space-y-2">
|
||||
{/* Language Toggle */}
|
||||
<button
|
||||
onClick={onToggleLanguage}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg
|
||||
bg-white/[0.05] hover:bg-white/[0.1] transition-colors text-sm"
|
||||
>
|
||||
<span className="text-white/50">{i.nav.language}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${lang === 'de' ? 'bg-indigo-500 text-white' : 'text-white/40'}`}>
|
||||
DE
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${lang === 'en' ? 'bg-indigo-500 text-white' : 'text-white/40'}`}>
|
||||
EN
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Fullscreen */}
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg
|
||||
bg-white/[0.05] hover:bg-white/[0.1] transition-colors text-sm"
|
||||
>
|
||||
<span className="text-white/50">{i.nav.fullscreen}</span>
|
||||
{isFullscreen ? (
|
||||
<Minimize className="w-4 h-4 text-white/50" />
|
||||
) : (
|
||||
<Maximize className="w-4 h-4 text-white/50" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
83
pitch-deck/components/ParticleBackground.tsx
Normal file
83
pitch-deck/components/ParticleBackground.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
interface Particle {
|
||||
x: number
|
||||
y: number
|
||||
size: number
|
||||
speed: number
|
||||
opacity: number
|
||||
}
|
||||
|
||||
export default function ParticleBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const particlesRef = useRef<Particle[]>([])
|
||||
const frameRef = useRef<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
function resize() {
|
||||
canvas!.width = window.innerWidth
|
||||
canvas!.height = window.innerHeight
|
||||
}
|
||||
|
||||
function initParticles() {
|
||||
const count = Math.min(150, Math.floor((window.innerWidth * window.innerHeight) / 8000))
|
||||
particlesRef.current = Array.from({ length: count }, () => ({
|
||||
x: Math.random() * canvas!.width,
|
||||
y: Math.random() * canvas!.height,
|
||||
size: Math.random() * 1.5 + 0.5,
|
||||
speed: Math.random() * 0.3 + 0.1,
|
||||
opacity: Math.random() * 0.5 + 0.1,
|
||||
}))
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!ctx || !canvas) return
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
for (const p of particlesRef.current) {
|
||||
p.y -= p.speed
|
||||
if (p.y < -10) {
|
||||
p.y = canvas.height + 10
|
||||
p.x = Math.random() * canvas.width
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${p.opacity})`
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
frameRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
resize()
|
||||
initParticles()
|
||||
animate()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
resize()
|
||||
initParticles()
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameRef.current)
|
||||
window.removeEventListener('resize', resize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="fixed inset-0 pointer-events-none z-0"
|
||||
style={{ opacity: 0.6 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
162
pitch-deck/components/PitchDeck.tsx
Normal file
162
pitch-deck/components/PitchDeck.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
'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 { Language, PitchData } from '@/lib/types'
|
||||
|
||||
import ParticleBackground from './ParticleBackground'
|
||||
import ProgressBar from './ProgressBar'
|
||||
import NavigationControls from './NavigationControls'
|
||||
import NavigationFAB from './NavigationFAB'
|
||||
import SlideOverview from './SlideOverview'
|
||||
import SlideContainer from './SlideContainer'
|
||||
|
||||
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'
|
||||
|
||||
interface PitchDeckProps {
|
||||
lang: Language
|
||||
onToggleLanguage: () => void
|
||||
}
|
||||
|
||||
export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
||||
const { data, loading, error } = usePitchData()
|
||||
const nav = useSlideNavigation()
|
||||
const [fabOpen, setFabOpen] = useState(false)
|
||||
|
||||
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,
|
||||
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 'cover':
|
||||
return <CoverSlide lang={lang} onNext={nav.nextSlide} />
|
||||
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} financials={data.financials} />
|
||||
case 'the-ask':
|
||||
return <TheAskSlide lang={lang} funding={data.funding} />
|
||||
case 'ai-qa':
|
||||
return <AIQASlide 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">
|
||||
<ParticleBackground />
|
||||
<ProgressBar current={nav.currentIndex} total={nav.totalSlides} />
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
<NavigationFAB
|
||||
currentIndex={nav.currentIndex}
|
||||
totalSlides={nav.totalSlides}
|
||||
visitedSlides={nav.visitedSlides}
|
||||
onGoToSlide={nav.goToSlide}
|
||||
lang={lang}
|
||||
onToggleLanguage={onToggleLanguage}
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{nav.showOverview && (
|
||||
<SlideOverview
|
||||
currentIndex={nav.currentIndex}
|
||||
onGoToSlide={nav.goToSlide}
|
||||
onClose={() => nav.setShowOverview(false)}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
pitch-deck/components/ProgressBar.tsx
Normal file
23
pitch-deck/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface ProgressBarProps {
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export default function ProgressBar({ current, total }: ProgressBarProps) {
|
||||
const progress = ((current + 1) / total) * 100
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 h-1 bg-white/5">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-indigo-500 via-purple-500 to-blue-500"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.4, ease: 'easeOut' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
53
pitch-deck/components/SlideContainer.tsx
Normal file
53
pitch-deck/components/SlideContainer.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
interface SlideContainerProps {
|
||||
children: ReactNode
|
||||
slideKey: string
|
||||
direction: number
|
||||
}
|
||||
|
||||
const variants = {
|
||||
enter: (direction: number) => ({
|
||||
x: direction > 0 ? '30%' : '-30%',
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
}),
|
||||
center: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
exit: (direction: number) => ({
|
||||
x: direction < 0 ? '30%' : '-30%',
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
}),
|
||||
}
|
||||
|
||||
export default function SlideContainer({ children, slideKey, direction }: SlideContainerProps) {
|
||||
return (
|
||||
<AnimatePresence mode="wait" custom={direction}>
|
||||
<motion.div
|
||||
key={slideKey}
|
||||
custom={direction}
|
||||
variants={variants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{
|
||||
x: { type: 'spring', stiffness: 300, damping: 30 },
|
||||
opacity: { duration: 0.3 },
|
||||
scale: { duration: 0.3 },
|
||||
}}
|
||||
className="absolute inset-0 flex items-center justify-center overflow-y-auto"
|
||||
>
|
||||
<div className="w-full max-w-6xl mx-auto px-6 py-12 md:py-16">
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
56
pitch-deck/components/SlideOverview.tsx
Normal file
56
pitch-deck/components/SlideOverview.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface SlideOverviewProps {
|
||||
currentIndex: number
|
||||
onGoToSlide: (index: number) => void
|
||||
onClose: () => void
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function SlideOverview({ currentIndex, onGoToSlide, onClose, lang }: SlideOverviewProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 bg-black/90 backdrop-blur-sm flex items-center justify-center p-8"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 max-w-5xl w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{i.slideNames.map((name, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.03 }}
|
||||
onClick={() => onGoToSlide(idx)}
|
||||
className={`
|
||||
aspect-video rounded-xl p-4 text-left
|
||||
border transition-all
|
||||
${idx === currentIndex
|
||||
? 'bg-indigo-500/20 border-indigo-500 shadow-lg shadow-indigo-500/20'
|
||||
: 'bg-white/[0.05] border-white/10 hover:bg-white/[0.1] hover:border-white/20'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="text-xs font-mono text-white/40 block mb-1">{idx + 1}</span>
|
||||
<span className={`text-sm font-medium ${idx === currentIndex ? 'text-white' : 'text-white/70'}`}>
|
||||
{name}
|
||||
</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
40
pitch-deck/components/slides/AIQASlide.tsx
Normal file
40
pitch-deck/components/slides/AIQASlide.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { Bot } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import ChatInterface from '../ui/ChatInterface'
|
||||
import LiveIndicator from '../ui/LiveIndicator'
|
||||
|
||||
interface AIQASlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function AIQASlide({ lang }: AIQASlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-indigo-500/20 flex items-center justify-center">
|
||||
<Bot className="w-6 h-6 text-indigo-400" />
|
||||
</div>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.aiqa.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.aiqa.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.3}>
|
||||
<div className="max-w-3xl mx-auto bg-white/[0.04] border border-white/10 rounded-2xl p-6">
|
||||
<ChatInterface lang={lang} />
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
99
pitch-deck/components/slides/BusinessModelSlide.tsx
Normal file
99
pitch-deck/components/slides/BusinessModelSlide.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchProduct } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { DollarSign, Repeat, TrendingUp } from 'lucide-react'
|
||||
|
||||
interface BusinessModelSlideProps {
|
||||
lang: Language
|
||||
products: PitchProduct[]
|
||||
}
|
||||
|
||||
export default function BusinessModelSlide({ lang, products }: BusinessModelSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.businessModel.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.businessModel.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<GlassCard delay={0.2} className="text-center">
|
||||
<Repeat className="w-6 h-6 text-indigo-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/50 mb-1">{i.businessModel.recurringRevenue}</p>
|
||||
<p className="text-2xl font-bold text-white">100%</p>
|
||||
<p className="text-xs text-white/30">SaaS / Subscription</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.3} className="text-center">
|
||||
<DollarSign className="w-6 h-6 text-green-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/50 mb-1">{i.businessModel.margin}</p>
|
||||
<p className="text-2xl font-bold text-white">>70%</p>
|
||||
<p className="text-xs text-white/30">{lang === 'de' ? 'nach Amortisation' : 'post amortization'}</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.4} className="text-center">
|
||||
<TrendingUp className="w-6 h-6 text-purple-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/50 mb-1">{i.businessModel.amortization}</p>
|
||||
<p className="text-2xl font-bold text-white">24 {i.businessModel.months}</p>
|
||||
<p className="text-xs text-white/30">{lang === 'de' ? 'Hardware-Amortisation' : 'Hardware Amortization'}</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* Unit Economics per Product */}
|
||||
<FadeInView delay={0.5}>
|
||||
<h3 className="text-lg font-semibold mb-4 text-white/70">{i.businessModel.unitEconomics}</h3>
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{products.map((p, idx) => {
|
||||
const amort = p.hardware_cost_eur > 0 ? Math.round(p.hardware_cost_eur / 24) : 0
|
||||
const monthlyMargin = p.monthly_price_eur - amort - (p.operating_cost_eur > 0 ? p.operating_cost_eur : 0)
|
||||
const marginPct = Math.round((monthlyMargin / p.monthly_price_eur) * 100)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={p.id}
|
||||
initial={{ opacity: 0, rotateY: -15 }}
|
||||
animate={{ opacity: 1, rotateY: 0 }}
|
||||
transition={{ delay: 0.6 + idx * 0.15 }}
|
||||
className="bg-white/[0.05] border border-white/10 rounded-2xl p-5"
|
||||
>
|
||||
<h4 className="font-bold text-white mb-3">{p.name}</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/50">{lang === 'de' ? 'Monatspreis' : 'Monthly Price'}</span>
|
||||
<span className="text-white font-medium">{p.monthly_price_eur} EUR</span>
|
||||
</div>
|
||||
{p.hardware_cost_eur > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/50">{i.businessModel.hardwareCost}</span>
|
||||
<span className="text-white/70">-{amort} EUR/Mo</span>
|
||||
</div>
|
||||
)}
|
||||
{p.operating_cost_eur > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/50">{i.businessModel.operatingCost}</span>
|
||||
<span className="text-white/70">-{p.operating_cost_eur.toLocaleString('de-DE')} EUR/Mo</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-white/10 pt-2 flex justify-between">
|
||||
<span className="text-white/50">{i.businessModel.margin}</span>
|
||||
<span className={`font-bold ${marginPct > 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{marginPct > 0 ? '+' : ''}{monthlyMargin} EUR ({marginPct}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
106
pitch-deck/components/slides/CompetitionSlide.tsx
Normal file
106
pitch-deck/components/slides/CompetitionSlide.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client'
|
||||
|
||||
import { Language, PitchFeature, PitchCompetitor } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { ShieldCheck, Code2, ScanLine, FileSearch, Package, Bug } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import FeatureMatrix from '../ui/FeatureMatrix'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
|
||||
interface CompetitionSlideProps {
|
||||
lang: Language
|
||||
features: PitchFeature[]
|
||||
competitors: PitchCompetitor[]
|
||||
}
|
||||
|
||||
const securityFeatures = {
|
||||
de: [
|
||||
{ icon: ShieldCheck, title: 'DevSecOps Security Suite', desc: '6 integrierte Security-Tools fuer kontinuierliche Sicherheitsueberwachung' },
|
||||
{ icon: ScanLine, title: 'SAST & Secrets Detection', desc: 'Automatische Code-Analyse (Semgrep) + Secrets-Scanning (Gitleaks) in der CI/CD Pipeline' },
|
||||
{ icon: Bug, title: 'Container & Dependency Scanning', desc: 'Trivy + Grype scannen Container-Images und Abhaengigkeiten auf CVEs' },
|
||||
{ icon: Package, title: 'SBOM-Generator (NIS2-konform)', desc: 'CycloneDX/SPDX Software Bill of Materials fuer NIS2 und ISO 27001 Compliance' },
|
||||
{ icon: FileSearch, title: 'Software-Risikoanalyse', desc: 'Automatisierte Risikoklassifizierung fuer Embedded-Entwicklung und AI-Act-konforme Systeme' },
|
||||
{ icon: Code2, title: 'KI-Code-Assistent (1000b)', desc: 'Das Cloud-LLM unterstuetzt Entwickler bei Code-Reviews, Security-Fixes und Compliance-Dokumentation' },
|
||||
],
|
||||
en: [
|
||||
{ icon: ShieldCheck, title: 'DevSecOps Security Suite', desc: '6 integrated security tools for continuous security monitoring' },
|
||||
{ icon: ScanLine, title: 'SAST & Secrets Detection', desc: 'Automatic code analysis (Semgrep) + secrets scanning (Gitleaks) in CI/CD pipeline' },
|
||||
{ icon: Bug, title: 'Container & Dependency Scanning', desc: 'Trivy + Grype scan container images and dependencies for CVEs' },
|
||||
{ icon: Package, title: 'SBOM Generator (NIS2 compliant)', desc: 'CycloneDX/SPDX Software Bill of Materials for NIS2 and ISO 27001 compliance' },
|
||||
{ icon: FileSearch, title: 'Software Risk Analysis', desc: 'Automated risk classification for embedded development and AI Act compliant systems' },
|
||||
{ icon: Code2, title: 'AI Code Assistant (1000b)', desc: 'Cloud LLM assists developers with code reviews, security fixes and compliance documentation' },
|
||||
],
|
||||
}
|
||||
|
||||
export default function CompetitionSlide({ lang, features, competitors }: CompetitionSlideProps) {
|
||||
const i = t(lang)
|
||||
const coreFeatures = features.filter(f => f.category !== 'security')
|
||||
const secFeats = securityFeatures[lang]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.competition.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.competition.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Feature Matrix (Core Compliance) */}
|
||||
<FadeInView delay={0.3}>
|
||||
<GlassCard className="mb-6 p-4 overflow-x-auto" hover={false}>
|
||||
<FeatureMatrix features={coreFeatures} lang={lang} />
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
{/* Security & Developer Features — nur bei ComplAI */}
|
||||
<FadeInView delay={0.5}>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-sm font-semibold text-indigo-400 mb-3 flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
{lang === 'de' ? 'Integrierte Security & Developer Tools — nur bei ComplAI' : 'Integrated Security & Developer Tools — ComplAI only'}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{secFeats.map((feat, idx) => {
|
||||
const Icon = feat.icon
|
||||
return (
|
||||
<FadeInView key={idx} delay={0.6 + idx * 0.08}>
|
||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-xl p-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Icon className="w-4 h-4 text-indigo-400 shrink-0" />
|
||||
<span className="text-xs font-semibold text-white">{feat.title}</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-white/40 leading-relaxed">{feat.desc}</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Competitor Summary */}
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{competitors.map((c, idx) => (
|
||||
<FadeInView key={c.id} delay={0.9 + idx * 0.1}>
|
||||
<div className="bg-white/[0.04] border border-white/5 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-white/70">{c.name}</h4>
|
||||
<span className="text-xs text-white/30">{c.customers_count.toLocaleString()} {lang === 'de' ? 'Kunden' : 'customers'}</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/40 mb-2">{c.pricing_range}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(c.weaknesses || []).slice(0, 2).map((w, widx) => (
|
||||
<span key={widx} className="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-400">
|
||||
{w}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
88
pitch-deck/components/slides/CoverSlide.tsx
Normal file
88
pitch-deck/components/slides/CoverSlide.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
|
||||
interface CoverSlideProps {
|
||||
lang: Language
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export default function CoverSlide({ lang, onNext }: CoverSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center text-center min-h-[70vh]">
|
||||
{/* Logo / Brand */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="mb-8"
|
||||
>
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600
|
||||
flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M8 12L20 6L32 12V28L20 34L8 28V12Z" stroke="white" strokeWidth="2" fill="none" />
|
||||
<path d="M20 6V34" stroke="white" strokeWidth="1.5" opacity="0.5" />
|
||||
<path d="M8 12L32 28" stroke="white" strokeWidth="1.5" opacity="0.3" />
|
||||
<path d="M32 12L8 28" stroke="white" strokeWidth="1.5" opacity="0.3" />
|
||||
<circle cx="20" cy="20" r="4" fill="white" opacity="0.8" />
|
||||
</svg>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Company Name */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="text-5xl md:text-7xl font-bold mb-4 tracking-tight"
|
||||
>
|
||||
BreakPilot{' '}
|
||||
<GradientText className="text-5xl md:text-7xl font-bold" delay={0.5}>
|
||||
ComplAI
|
||||
</GradientText>
|
||||
</motion.h1>
|
||||
|
||||
{/* Tagline */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
className="text-xl md:text-2xl text-white/60 mb-2 max-w-2xl"
|
||||
>
|
||||
{i.cover.tagline}
|
||||
</motion.p>
|
||||
|
||||
{/* Subtitle */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
className="text-sm text-white/30 font-mono tracking-wider mb-12"
|
||||
>
|
||||
{i.cover.subtitle}
|
||||
</motion.p>
|
||||
|
||||
{/* CTA */}
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.2 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={onNext}
|
||||
className="group flex items-center gap-2 px-8 py-3 rounded-full
|
||||
bg-indigo-500 hover:bg-indigo-600 transition-colors text-white font-medium
|
||||
shadow-lg shadow-indigo-500/30"
|
||||
>
|
||||
{i.cover.cta}
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</motion.button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
pitch-deck/components/slides/FinancialsSlide.tsx
Normal file
93
pitch-deck/components/slides/FinancialsSlide.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Language, PitchFinancial } from '@/lib/types'
|
||||
import { t, formatEur } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import FinancialChart from '../ui/FinancialChart'
|
||||
import FinancialSliders from '../ui/FinancialSliders'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import AnimatedCounter from '../ui/AnimatedCounter'
|
||||
|
||||
interface FinancialsSlideProps {
|
||||
lang: Language
|
||||
financials: PitchFinancial[]
|
||||
}
|
||||
|
||||
export default function FinancialsSlide({ lang, financials }: FinancialsSlideProps) {
|
||||
const i = t(lang)
|
||||
const [growthRate, setGrowthRate] = useState(100)
|
||||
const [churnRate, setChurnRate] = useState(5)
|
||||
const [arpu, setArpu] = useState(500)
|
||||
|
||||
const growthMultiplier = growthRate / 100
|
||||
const lastYear = financials[financials.length - 1]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.financials.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.financials.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Key Numbers */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<GlassCard delay={0.2} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.arr} 2030</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={Math.round((lastYear?.arr_eur || 0) * growthMultiplier / 1_000_000)}
|
||||
suffix={lang === 'de' ? ' Mio.' : 'M'}
|
||||
duration={1200}
|
||||
/>
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.25} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.customers} 2030</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
<AnimatedCounter target={Math.round((lastYear?.customers_count || 0) * growthMultiplier)} duration={1200} />
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.3} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.employees} 2030</p>
|
||||
<p className="text-xl font-bold text-white">{lastYear?.employees_count || 0}</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.35} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.mrr} 2030</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={Math.round((lastYear?.mrr_eur || 0) * growthMultiplier / 1_000)}
|
||||
suffix="k"
|
||||
duration={1200}
|
||||
/>
|
||||
</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Chart */}
|
||||
<FadeInView delay={0.4} className="md:col-span-2">
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<FinancialChart financials={financials} lang={lang} growthMultiplier={growthMultiplier} />
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
{/* Sliders */}
|
||||
<FadeInView delay={0.5}>
|
||||
<FinancialSliders
|
||||
growthRate={growthRate}
|
||||
churnRate={churnRate}
|
||||
arpu={arpu}
|
||||
onGrowthChange={setGrowthRate}
|
||||
onChurnChange={setChurnRate}
|
||||
onArpuChange={setArpu}
|
||||
lang={lang}
|
||||
/>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
pitch-deck/components/slides/HowItWorksSlide.tsx
Normal file
65
pitch-deck/components/slides/HowItWorksSlide.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { Plug, Settings, RefreshCw, CheckCircle2 } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
|
||||
interface HowItWorksSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
const stepIcons = [Plug, Settings, RefreshCw, CheckCircle2]
|
||||
const stepColors = ['text-blue-400', 'text-indigo-400', 'text-purple-400', 'text-green-400']
|
||||
|
||||
export default function HowItWorksSlide({ lang }: HowItWorksSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.howItWorks.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.howItWorks.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="relative max-w-4xl mx-auto">
|
||||
{/* Connection Line */}
|
||||
<div className="absolute left-8 top-12 bottom-12 w-px bg-gradient-to-b from-blue-500 via-purple-500 to-green-500 hidden md:block" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{i.howItWorks.steps.map((step, idx) => {
|
||||
const Icon = stepIcons[idx]
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + idx * 0.2, duration: 0.5 }}
|
||||
className="flex items-start gap-6 relative"
|
||||
>
|
||||
<div className={`
|
||||
w-16 h-16 rounded-2xl bg-white/[0.06] border border-white/10
|
||||
flex items-center justify-center shrink-0 relative z-10
|
||||
${stepColors[idx]}
|
||||
`}>
|
||||
<Icon className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-xs font-mono text-white/30">0{idx + 1}</span>
|
||||
<h3 className="text-xl font-bold text-white">{step.title}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-white/50 leading-relaxed max-w-lg">{step.desc}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
pitch-deck/components/slides/MarketSlide.tsx
Normal file
91
pitch-deck/components/slides/MarketSlide.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchMarket } from '@/lib/types'
|
||||
import { t, formatEur } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import AnimatedCounter from '../ui/AnimatedCounter'
|
||||
|
||||
interface MarketSlideProps {
|
||||
lang: Language
|
||||
market: PitchMarket[]
|
||||
}
|
||||
|
||||
const sizes = [280, 200, 130]
|
||||
const colors = ['border-indigo-500/30 bg-indigo-500/5', 'border-purple-500/30 bg-purple-500/5', 'border-blue-500/30 bg-blue-500/5']
|
||||
const textColors = ['text-indigo-400', 'text-purple-400', 'text-blue-400']
|
||||
|
||||
export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
||||
const i = t(lang)
|
||||
const labels = [i.market.tamLabel, i.market.samLabel, i.market.somLabel]
|
||||
const segments = [i.market.tam, i.market.sam, i.market.som]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.market.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.market.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-12">
|
||||
{/* Circles */}
|
||||
<div className="relative flex items-center justify-center" style={{ width: 300, height: 300 }}>
|
||||
{market.map((m, idx) => (
|
||||
<motion.div
|
||||
key={m.id}
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.3 + idx * 0.2, type: 'spring', stiffness: 200 }}
|
||||
className={`absolute rounded-full border-2 ${colors[idx]} flex items-center justify-center`}
|
||||
style={{
|
||||
width: sizes[idx],
|
||||
height: sizes[idx],
|
||||
}}
|
||||
>
|
||||
{idx === market.length - 1 && (
|
||||
<div className="text-center">
|
||||
<span className={`text-xs font-mono ${textColors[idx]}`}>{segments[idx]}</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div className="space-y-6">
|
||||
{market.map((m, idx) => (
|
||||
<motion.div
|
||||
key={m.id}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 + idx * 0.15 }}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<div className={`w-3 h-3 rounded-full ${textColors[idx]} bg-current`} />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-bold ${textColors[idx]}`}>{segments[idx]}</span>
|
||||
<span className="text-xs text-white/30">{labels[idx]}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={m.value_eur / 1_000_000_000}
|
||||
suffix={lang === 'de' ? ' Mrd. EUR' : 'B EUR'}
|
||||
decimals={1}
|
||||
duration={1500}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-white/40">
|
||||
{i.market.growth}: {m.growth_rate_pct}% · {i.market.source}: {m.source}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
pitch-deck/components/slides/ProblemSlide.tsx
Normal file
54
pitch-deck/components/slides/ProblemSlide.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { AlertTriangle, Scale, Shield } from 'lucide-react'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
|
||||
interface ProblemSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
const icons = [AlertTriangle, Scale, Shield]
|
||||
|
||||
export default function ProblemSlide({ lang }: ProblemSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.problem.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.problem.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-12">
|
||||
{i.problem.cards.map((card, idx) => {
|
||||
const Icon = icons[idx]
|
||||
return (
|
||||
<GlassCard key={idx} delay={0.2 + idx * 0.15} className="text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-red-500/10 flex items-center justify-center">
|
||||
<Icon className="w-6 h-6 text-red-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2 text-white">{card.title}</h3>
|
||||
<p className="text-3xl font-bold text-red-400 mb-3">{card.stat}</p>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{card.desc}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FadeInView delay={0.8} className="max-w-3xl mx-auto">
|
||||
<blockquote className="text-center">
|
||||
<p className="text-lg md:text-xl text-white/70 italic leading-relaxed">
|
||||
“{i.problem.quote}”
|
||||
</p>
|
||||
</blockquote>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
pitch-deck/components/slides/ProductSlide.tsx
Normal file
33
pitch-deck/components/slides/ProductSlide.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { Language, PitchProduct } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import PricingCard from '../ui/PricingCard'
|
||||
|
||||
interface ProductSlideProps {
|
||||
lang: Language
|
||||
products: PitchProduct[]
|
||||
}
|
||||
|
||||
export default function ProductSlide({ lang, products }: ProductSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.product.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.product.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{products.map((product, idx) => (
|
||||
<PricingCard key={product.id} product={product} lang={lang} delay={0.2 + idx * 0.15} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
pitch-deck/components/slides/SolutionSlide.tsx
Normal file
54
pitch-deck/components/slides/SolutionSlide.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { Server, ShieldCheck, Bot } from 'lucide-react'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
|
||||
interface SolutionSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
const icons = [Server, ShieldCheck, Bot]
|
||||
const colors = ['from-blue-500 to-cyan-500', 'from-indigo-500 to-purple-500', 'from-purple-500 to-pink-500']
|
||||
|
||||
export default function SolutionSlide({ lang }: SolutionSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.solution.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.solution.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{i.solution.pillars.map((pillar, idx) => {
|
||||
const Icon = icons[idx]
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3 + idx * 0.2, duration: 0.5 }}
|
||||
>
|
||||
<GlassCard className="text-center h-full" delay={0}>
|
||||
<div className={`w-16 h-16 mx-auto mb-5 rounded-2xl bg-gradient-to-br ${colors[idx]}
|
||||
flex items-center justify-center shadow-lg`}>
|
||||
<Icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-3 text-white">{pillar.title}</h3>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{pillar.desc}</p>
|
||||
</GlassCard>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
76
pitch-deck/components/slides/TeamSlide.tsx
Normal file
76
pitch-deck/components/slides/TeamSlide.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchTeamMember } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { User } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
|
||||
interface TeamSlideProps {
|
||||
lang: Language
|
||||
team: PitchTeamMember[]
|
||||
}
|
||||
|
||||
export default function TeamSlide({ lang, team }: TeamSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.team.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.team.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{team.map((member, idx) => (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0, x: idx === 0 ? -40 : 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + idx * 0.2, duration: 0.6 }}
|
||||
className="bg-white/[0.08] backdrop-blur-xl border border-white/10 rounded-3xl p-8"
|
||||
>
|
||||
<div className="flex items-start gap-5">
|
||||
{/* Avatar */}
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600
|
||||
flex items-center justify-center shrink-0 shadow-lg">
|
||||
<User className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white mb-1">{member.name}</h3>
|
||||
<p className="text-indigo-400 text-sm font-medium mb-3">
|
||||
{lang === 'de' ? member.role_de : member.role_en}
|
||||
</p>
|
||||
<p className="text-sm text-white/50 leading-relaxed mb-4">
|
||||
{lang === 'de' ? member.bio_de : member.bio_en}
|
||||
</p>
|
||||
|
||||
{/* Equity */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs text-white/40">{i.team.equity}:</span>
|
||||
<span className="text-sm font-bold text-white">{member.equity_pct}%</span>
|
||||
</div>
|
||||
|
||||
{/* Expertise Tags */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(member.expertise || []).map((skill, sidx) => (
|
||||
<span
|
||||
key={sidx}
|
||||
className="text-xs px-2.5 py-1 rounded-full bg-indigo-500/10 text-indigo-300 border border-indigo-500/20"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
pitch-deck/components/slides/TheAskSlide.tsx
Normal file
127
pitch-deck/components/slides/TheAskSlide.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchFunding } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import AnimatedCounter from '../ui/AnimatedCounter'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { Target, Calendar, FileText } from 'lucide-react'
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
|
||||
|
||||
interface TheAskSlideProps {
|
||||
lang: Language
|
||||
funding: PitchFunding
|
||||
}
|
||||
|
||||
const COLORS = ['#6366f1', '#a78bfa', '#60a5fa', '#34d399', '#fbbf24']
|
||||
|
||||
export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
const i = t(lang)
|
||||
const useOfFunds = funding?.use_of_funds || []
|
||||
|
||||
const pieData = useOfFunds.map((item) => ({
|
||||
name: lang === 'de' ? item.label_de : item.label_en,
|
||||
value: item.percentage,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-10">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.theAsk.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.theAsk.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Main Number */}
|
||||
<FadeInView delay={0.2} className="text-center mb-10">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, type: 'spring', stiffness: 200 }}
|
||||
>
|
||||
<p className="text-6xl md:text-8xl font-bold text-white mb-2">
|
||||
<AnimatedCounter target={200} suffix="k" duration={2000} />
|
||||
<span className="text-3xl md:text-4xl text-white/50 ml-2">EUR</span>
|
||||
</p>
|
||||
</motion.div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Details */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<GlassCard delay={0.5} className="text-center p-5">
|
||||
<FileText className="w-6 h-6 text-indigo-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{i.theAsk.instrument}</p>
|
||||
<p className="text-lg font-bold text-white">{funding?.instrument || 'SAFE'}</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.6} className="text-center p-5">
|
||||
<Calendar className="w-6 h-6 text-purple-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{i.theAsk.targetDate}</p>
|
||||
<p className="text-lg font-bold text-white">Q3 2026</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.7} className="text-center p-5">
|
||||
<Target className="w-6 h-6 text-blue-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{lang === 'de' ? 'Runway' : 'Runway'}</p>
|
||||
<p className="text-lg font-bold text-white">18 {lang === 'de' ? 'Monate' : 'Months'}</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* Use of Funds */}
|
||||
<FadeInView delay={0.8}>
|
||||
<GlassCard hover={false} className="p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 text-center">{i.theAsk.useOfFunds}</h3>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8">
|
||||
{/* Pie Chart */}
|
||||
<div className="w-48 h-48">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
>
|
||||
{pieData.map((_, idx) => (
|
||||
<Cell key={idx} fill={COLORS[idx % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'rgba(10, 10, 26, 0.9)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
}}
|
||||
formatter={(value: number) => `${value}%`}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex-1 space-y-3">
|
||||
{useOfFunds.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: COLORS[idx] }} />
|
||||
<span className="flex-1 text-sm text-white/70">
|
||||
{lang === 'de' ? item.label_de : item.label_en}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-white">{item.percentage}%</span>
|
||||
<span className="text-xs text-white/30">
|
||||
{((funding.amount_eur * item.percentage) / 100).toLocaleString('de-DE')} EUR
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
58
pitch-deck/components/slides/TractionSlide.tsx
Normal file
58
pitch-deck/components/slides/TractionSlide.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { Language, PitchMilestone, PitchMetric } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import Timeline from '../ui/Timeline'
|
||||
import LiveIndicator from '../ui/LiveIndicator'
|
||||
|
||||
interface TractionSlideProps {
|
||||
lang: Language
|
||||
milestones: PitchMilestone[]
|
||||
metrics: PitchMetric[]
|
||||
}
|
||||
|
||||
export default function TractionSlide({ lang, milestones, metrics }: TractionSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-10">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.traction.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.traction.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{/* KPI Cards */}
|
||||
<div>
|
||||
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||
{metrics.slice(0, 6).map((m, idx) => (
|
||||
<GlassCard key={m.id} delay={0.2 + idx * 0.08} className="p-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-white/40">
|
||||
{lang === 'de' ? m.label_de : m.label_en}
|
||||
</span>
|
||||
{m.is_live && <LiveIndicator />}
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{m.value}{m.unit ? ` ${m.unit}` : ''}
|
||||
</p>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<FadeInView delay={0.4}>
|
||||
<div className="bg-white/[0.03] rounded-2xl p-5 border border-white/5 max-h-[400px] overflow-y-auto">
|
||||
<Timeline milestones={milestones} lang={lang} />
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
pitch-deck/components/ui/AnimatedCounter.tsx
Normal file
54
pitch-deck/components/ui/AnimatedCounter.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
|
||||
interface AnimatedCounterProps {
|
||||
target: number
|
||||
duration?: number
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
className?: string
|
||||
decimals?: number
|
||||
}
|
||||
|
||||
export default function AnimatedCounter({
|
||||
target,
|
||||
duration = 2000,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
className = '',
|
||||
decimals = 0,
|
||||
}: AnimatedCounterProps) {
|
||||
const [current, setCurrent] = useState(0)
|
||||
const startTime = useRef<number | null>(null)
|
||||
const frameRef = useRef<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
startTime.current = null
|
||||
|
||||
function animate(timestamp: number) {
|
||||
if (!startTime.current) startTime.current = timestamp
|
||||
const elapsed = timestamp - startTime.current
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
setCurrent(eased * target)
|
||||
|
||||
if (progress < 1) {
|
||||
frameRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
frameRef.current = requestAnimationFrame(animate)
|
||||
return () => cancelAnimationFrame(frameRef.current)
|
||||
}, [target, duration])
|
||||
|
||||
const formatted = decimals > 0
|
||||
? current.toFixed(decimals)
|
||||
: Math.round(current).toLocaleString('de-DE')
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{prefix}{formatted}{suffix}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
167
pitch-deck/components/ui/ChatInterface.tsx
Normal file
167
pitch-deck/components/ui/ChatInterface.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Send, Bot, User, Sparkles } from 'lucide-react'
|
||||
import { ChatMessage, Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface ChatInterfaceProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function ChatInterface({ lang }: ChatInterfaceProps) {
|
||||
const i = t(lang)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
async function sendMessage(text?: string) {
|
||||
const message = text || input.trim()
|
||||
if (!message || isStreaming) return
|
||||
|
||||
setInput('')
|
||||
setMessages(prev => [...prev, { role: 'user', content: message }])
|
||||
setIsStreaming(true)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
history: messages.slice(-10),
|
||||
lang,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
|
||||
const reader = res.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let content = ''
|
||||
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: '' }])
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
content += decoder.decode(value, { stream: true })
|
||||
setMessages(prev => {
|
||||
const updated = [...prev]
|
||||
updated[updated.length - 1] = { role: 'assistant', content }
|
||||
return updated
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Chat error:', err)
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: lang === 'de'
|
||||
? 'Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.'
|
||||
: 'Connection failed. Please try again.'
|
||||
},
|
||||
])
|
||||
} finally {
|
||||
setIsStreaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full max-h-[500px]">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto space-y-4 mb-4 pr-2">
|
||||
{messages.length === 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-white/40 text-sm mb-4">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>{lang === 'de' ? 'Vorgeschlagene Fragen:' : 'Suggested questions:'}</span>
|
||||
</div>
|
||||
{i.aiqa.suggestions.map((q, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
onClick={() => sendMessage(q)}
|
||||
className="block w-full text-left px-4 py-3 rounded-xl bg-white/[0.05] border border-white/10
|
||||
hover:bg-white/[0.1] transition-colors text-sm text-white/70 hover:text-white"
|
||||
>
|
||||
{q}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
{messages.map((msg, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}
|
||||
>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0">
|
||||
<Bot className="w-4 h-4 text-indigo-400" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
max-w-[80%] rounded-2xl px-4 py-3 text-sm leading-relaxed
|
||||
${msg.role === 'user'
|
||||
? 'bg-indigo-500/20 text-white'
|
||||
: 'bg-white/[0.06] text-white/80'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap">{msg.content}</div>
|
||||
{isStreaming && idx === messages.length - 1 && msg.role === 'assistant' && (
|
||||
<span className="inline-block w-2 h-4 bg-indigo-400 animate-pulse ml-1" />
|
||||
)}
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center shrink-0">
|
||||
<User className="w-4 h-4 text-white/60" />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
|
||||
placeholder={i.aiqa.placeholder}
|
||||
disabled={isStreaming}
|
||||
className="flex-1 bg-white/[0.06] border border-white/10 rounded-xl px-4 py-3
|
||||
text-sm text-white placeholder-white/30 outline-none
|
||||
focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/20
|
||||
disabled:opacity-50 transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={() => sendMessage()}
|
||||
disabled={isStreaming || !input.trim()}
|
||||
className="px-4 py-3 bg-indigo-500 hover:bg-indigo-600 disabled:opacity-30
|
||||
rounded-xl transition-all text-white"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
pitch-deck/components/ui/FadeInView.tsx
Normal file
39
pitch-deck/components/ui/FadeInView.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface FadeInViewProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
delay?: number
|
||||
direction?: 'up' | 'down' | 'left' | 'right' | 'none'
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export default function FadeInView({
|
||||
children,
|
||||
className = '',
|
||||
delay = 0,
|
||||
direction = 'up',
|
||||
duration = 0.6,
|
||||
}: FadeInViewProps) {
|
||||
const directionMap = {
|
||||
up: { y: 30 },
|
||||
down: { y: -30 },
|
||||
left: { x: 30 },
|
||||
right: { x: -30 },
|
||||
none: {},
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, ...directionMap[direction] }}
|
||||
animate={{ opacity: 1, x: 0, y: 0 }}
|
||||
transition={{ duration, delay, ease: [0.22, 1, 0.36, 1] }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
68
pitch-deck/components/ui/FeatureMatrix.tsx
Normal file
68
pitch-deck/components/ui/FeatureMatrix.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { PitchFeature, Language } from '@/lib/types'
|
||||
import { Check, X, Star } from 'lucide-react'
|
||||
|
||||
interface FeatureMatrixProps {
|
||||
features: PitchFeature[]
|
||||
lang: Language
|
||||
}
|
||||
|
||||
function Cell({ value, isDiff }: { value: boolean; isDiff: boolean }) {
|
||||
return (
|
||||
<td className="px-4 py-3 text-center">
|
||||
{value ? (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 500, delay: 0.1 }}
|
||||
>
|
||||
<Check className={`w-5 h-5 mx-auto ${isDiff ? 'text-green-400' : 'text-white/50'}`} />
|
||||
</motion.span>
|
||||
) : (
|
||||
<X className="w-5 h-5 mx-auto text-white/20" />
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FeatureMatrix({ features, lang }: FeatureMatrixProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left px-4 py-3 font-medium text-white/60">Feature</th>
|
||||
<th className="px-4 py-3 font-bold text-indigo-400">ComplAI</th>
|
||||
<th className="px-4 py-3 font-medium text-white/60">Proliance</th>
|
||||
<th className="px-4 py-3 font-medium text-white/60">DataGuard</th>
|
||||
<th className="px-4 py-3 font-medium text-white/60">heyData</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{features.map((f, i) => (
|
||||
<motion.tr
|
||||
key={f.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className={`border-b border-white/5 ${f.is_differentiator ? 'bg-indigo-500/5' : ''}`}
|
||||
>
|
||||
<td className="px-4 py-3 flex items-center gap-2">
|
||||
{f.is_differentiator && <Star className="w-3.5 h-3.5 text-yellow-400" />}
|
||||
<span className={f.is_differentiator ? 'text-white font-medium' : 'text-white/70'}>
|
||||
{lang === 'de' ? f.feature_name_de : f.feature_name_en}
|
||||
</span>
|
||||
</td>
|
||||
<Cell value={f.breakpilot} isDiff={f.is_differentiator} />
|
||||
<Cell value={f.proliance} isDiff={false} />
|
||||
<Cell value={f.dataguard} isDiff={false} />
|
||||
<Cell value={f.heydata} isDiff={false} />
|
||||
</motion.tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
pitch-deck/components/ui/FinancialChart.tsx
Normal file
72
pitch-deck/components/ui/FinancialChart.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import { PitchFinancial, Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Line,
|
||||
ComposedChart,
|
||||
Area,
|
||||
} from 'recharts'
|
||||
|
||||
interface FinancialChartProps {
|
||||
financials: PitchFinancial[]
|
||||
lang: Language
|
||||
growthMultiplier?: number
|
||||
}
|
||||
|
||||
export default function FinancialChart({ financials, lang, growthMultiplier = 1 }: FinancialChartProps) {
|
||||
const i = t(lang)
|
||||
|
||||
const data = financials.map((f) => ({
|
||||
year: f.year,
|
||||
[i.financials.revenue]: Math.round(f.revenue_eur * growthMultiplier),
|
||||
[i.financials.costs]: f.costs_eur,
|
||||
[i.financials.customers]: Math.round(f.customers_count * growthMultiplier),
|
||||
}))
|
||||
|
||||
const formatValue = (value: number) => {
|
||||
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
|
||||
if (value >= 1_000) return `${(value / 1_000).toFixed(0)}k`
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.8} />
|
||||
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#f43f5e" stopOpacity={0.6} />
|
||||
<stop offset="100%" stopColor="#f43f5e" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="year" stroke="rgba(255,255,255,0.3)" tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 12 }} />
|
||||
<YAxis stroke="rgba(255,255,255,0.1)" tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 11 }} tickFormatter={formatValue} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'rgba(10, 10, 26, 0.9)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
}}
|
||||
formatter={(value: number) => formatValue(value) + ' EUR'}
|
||||
/>
|
||||
<Area type="monotone" dataKey={i.financials.revenue} fill="url(#revenueGradient)" stroke="#6366f1" strokeWidth={2} />
|
||||
<Bar dataKey={i.financials.costs} fill="url(#costGradient)" radius={[4, 4, 0, 0]} barSize={30} />
|
||||
<Line type="monotone" dataKey={i.financials.customers} stroke="#22c55e" strokeWidth={2} dot={{ r: 4, fill: '#22c55e' }} yAxisId={0} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
pitch-deck/components/ui/FinancialSliders.tsx
Normal file
104
pitch-deck/components/ui/FinancialSliders.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface FinancialSlidersProps {
|
||||
growthRate: number
|
||||
churnRate: number
|
||||
arpu: number
|
||||
onGrowthChange: (v: number) => void
|
||||
onChurnChange: (v: number) => void
|
||||
onArpuChange: (v: number) => void
|
||||
lang: Language
|
||||
}
|
||||
|
||||
function Slider({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
unit,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
unit: string
|
||||
onChange: (v: number) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-white/60">{label}</span>
|
||||
<span className="font-mono text-white">{value}{unit}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
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-4
|
||||
[&::-webkit-slider-thumb]:h-4
|
||||
[&::-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>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FinancialSliders({
|
||||
growthRate,
|
||||
churnRate,
|
||||
arpu,
|
||||
onGrowthChange,
|
||||
onChurnChange,
|
||||
onArpuChange,
|
||||
lang,
|
||||
}: FinancialSlidersProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div className="space-y-5 p-5 bg-white/[0.05] rounded-2xl border border-white/10">
|
||||
<h4 className="text-sm font-medium text-white/60">{i.financials.adjustAssumptions}</h4>
|
||||
<Slider
|
||||
label={i.financials.sliderGrowth}
|
||||
value={growthRate}
|
||||
min={50}
|
||||
max={200}
|
||||
step={10}
|
||||
unit="%"
|
||||
onChange={onGrowthChange}
|
||||
/>
|
||||
<Slider
|
||||
label={i.financials.sliderChurn}
|
||||
value={churnRate}
|
||||
min={0}
|
||||
max={15}
|
||||
step={0.5}
|
||||
unit="%"
|
||||
onChange={onChurnChange}
|
||||
/>
|
||||
<Slider
|
||||
label={i.financials.sliderArpu}
|
||||
value={arpu}
|
||||
min={200}
|
||||
max={1500}
|
||||
step={50}
|
||||
unit=" EUR"
|
||||
onChange={onArpuChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
pitch-deck/components/ui/GlassCard.tsx
Normal file
33
pitch-deck/components/ui/GlassCard.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface GlassCardProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
delay?: number
|
||||
hover?: boolean
|
||||
}
|
||||
|
||||
export default function GlassCard({ children, className = '', onClick, delay = 0, hover = true }: GlassCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay }}
|
||||
whileHover={hover ? { scale: 1.02, backgroundColor: 'rgba(255, 255, 255, 0.12)' } : undefined}
|
||||
onClick={onClick}
|
||||
className={`
|
||||
bg-white/[0.08] backdrop-blur-xl
|
||||
border border-white/10 rounded-3xl
|
||||
p-6 transition-colors duration-200
|
||||
${onClick ? 'cursor-pointer' : ''}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
27
pitch-deck/components/ui/GradientText.tsx
Normal file
27
pitch-deck/components/ui/GradientText.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface GradientTextProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export default function GradientText({ children, className = '', delay = 0 }: GradientTextProps) {
|
||||
return (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay }}
|
||||
className={`
|
||||
bg-gradient-to-r from-indigo-400 via-purple-400 to-blue-400
|
||||
bg-clip-text text-transparent
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</motion.span>
|
||||
)
|
||||
}
|
||||
13
pitch-deck/components/ui/LiveIndicator.tsx
Normal file
13
pitch-deck/components/ui/LiveIndicator.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client'
|
||||
|
||||
export default function LiveIndicator({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 ${className}`}>
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
|
||||
</span>
|
||||
<span className="text-xs text-green-400 font-medium">LIVE</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
82
pitch-deck/components/ui/PricingCard.tsx
Normal file
82
pitch-deck/components/ui/PricingCard.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { PitchProduct, Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { Check } from 'lucide-react'
|
||||
import ProductShowcase from './ProductShowcase'
|
||||
|
||||
interface PricingCardProps {
|
||||
product: PitchProduct
|
||||
lang: Language
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export default function PricingCard({ product, lang, delay = 0 }: PricingCardProps) {
|
||||
const i = t(lang)
|
||||
const productType = product.name.includes('Mini')
|
||||
? 'mini'
|
||||
: product.name.includes('Studio')
|
||||
? 'studio'
|
||||
: 'cloud'
|
||||
|
||||
const features = lang === 'de' ? product.features_de : product.features_en
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, rotateY: -10 }}
|
||||
animate={{ opacity: 1, y: 0, rotateY: 0 }}
|
||||
transition={{ duration: 0.6, delay }}
|
||||
className={`
|
||||
relative bg-white/[0.08] backdrop-blur-xl
|
||||
border rounded-3xl p-6
|
||||
transition-all duration-300
|
||||
${product.is_popular
|
||||
? 'border-indigo-500/50 shadow-lg shadow-indigo-500/10'
|
||||
: 'border-white/10 hover:border-white/20'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{product.is_popular && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-4 py-1 bg-indigo-500 rounded-full text-xs font-semibold">
|
||||
{i.product.popular}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<ProductShowcase type={productType} className="mb-4" />
|
||||
|
||||
<h3 className="text-xl font-bold mb-1">{product.name}</h3>
|
||||
<p className="text-white/50 text-sm mb-4">{product.hardware}</p>
|
||||
|
||||
<div className="mb-1">
|
||||
<span className="text-4xl font-bold">{product.monthly_price_eur}</span>
|
||||
<span className="text-white/50 text-lg ml-1">EUR</span>
|
||||
</div>
|
||||
<p className="text-white/40 text-sm mb-6">{i.product.monthly}</p>
|
||||
|
||||
<div className="w-full border-t border-white/10 pt-4 mb-4">
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-white/50">{i.product.llm}</span>
|
||||
<span className="font-medium">{product.llm_size}</span>
|
||||
</div>
|
||||
{product.hardware_cost_eur > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-white/50">{i.product.hardware}</span>
|
||||
<span className="font-medium">{product.hardware_cost_eur.toLocaleString('de-DE')} EUR</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="w-full space-y-2">
|
||||
{(features || []).map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2 text-sm text-left">
|
||||
<Check className="w-4 h-4 text-green-400 shrink-0 mt-0.5" />
|
||||
<span className="text-white/70">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
50
pitch-deck/components/ui/ProductShowcase.tsx
Normal file
50
pitch-deck/components/ui/ProductShowcase.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Cloud } from 'lucide-react'
|
||||
|
||||
interface ProductShowcaseProps {
|
||||
type: 'mini' | 'studio' | 'cloud'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PRODUCT_IMAGES = {
|
||||
mini: 'https://www.apple.com/newsroom/images/2024/10/apples-new-mac-mini-apples-new-mac-mini-is-more-mighty-more-mini-and-built-for-apple-intelligence/article/Apple-Mac-mini-hero_big.jpg.large.jpg',
|
||||
studio: 'https://www.apple.com/newsroom/images/2025/03/apple-unveils-new-mac-studio-the-most-powerful-mac-ever/article/Apple-Mac-Studio-front-250305_big.jpg.large.jpg',
|
||||
}
|
||||
|
||||
export default function ProductShowcase({ type, className = '' }: ProductShowcaseProps) {
|
||||
if (type === 'cloud') {
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative ${className}`}
|
||||
whileHover={{ rotateY: 5, rotateX: -5, scale: 1.05 }}
|
||||
transition={{ type: 'spring', stiffness: 300 }}
|
||||
style={{ perspective: 1000, transformStyle: 'preserve-3d' }}
|
||||
>
|
||||
<div className="w-28 h-28 rounded-2xl bg-gradient-to-br from-purple-500 to-pink-500
|
||||
flex items-center justify-center shadow-lg shadow-purple-500/20">
|
||||
<Cloud className="w-14 h-14 text-white" />
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative ${className}`}
|
||||
whileHover={{ rotateY: 5, rotateX: -5, scale: 1.05 }}
|
||||
transition={{ type: 'spring', stiffness: 300 }}
|
||||
style={{ perspective: 1000, transformStyle: 'preserve-3d' }}
|
||||
>
|
||||
<div className="w-28 h-28 rounded-2xl overflow-hidden shadow-lg shadow-indigo-500/20 bg-white/5">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={PRODUCT_IMAGES[type]}
|
||||
alt={type === 'mini' ? 'Mac Mini M4 Pro' : 'Mac Studio M3 Ultra'}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
66
pitch-deck/components/ui/Timeline.tsx
Normal file
66
pitch-deck/components/ui/Timeline.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { PitchMilestone, Language } from '@/lib/types'
|
||||
import { CheckCircle2, Circle, Clock } from 'lucide-react'
|
||||
|
||||
interface TimelineProps {
|
||||
milestones: PitchMilestone[]
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function Timeline({ milestones, lang }: TimelineProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Line */}
|
||||
<div className="absolute left-6 top-0 bottom-0 w-px bg-gradient-to-b from-indigo-500 via-purple-500 to-white/10" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{milestones.map((m, i) => {
|
||||
const Icon = m.status === 'completed' ? CheckCircle2 : m.status === 'in_progress' ? Clock : Circle
|
||||
const iconColor = m.status === 'completed'
|
||||
? 'text-green-400'
|
||||
: m.status === 'in_progress'
|
||||
? 'text-yellow-400'
|
||||
: 'text-white/30'
|
||||
|
||||
const date = new Date(m.milestone_date)
|
||||
const dateStr = date.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={m.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="relative flex items-start gap-4 pl-2"
|
||||
>
|
||||
<div className={`relative z-10 p-1 rounded-full bg-[#0a0a1a] ${iconColor}`}>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1 pb-2">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-xs font-mono text-white/40">{dateStr}</span>
|
||||
{m.status === 'in_progress' && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-yellow-500/20 text-yellow-400">
|
||||
In Progress
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="font-semibold text-white">
|
||||
{lang === 'de' ? m.title_de : m.title_en}
|
||||
</h4>
|
||||
<p className="text-sm text-white/50 mt-0.5">
|
||||
{lang === 'de' ? m.description_de : m.description_en}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
pitch-deck/lib/animations.ts
Normal file
67
pitch-deck/lib/animations.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export const fadeIn = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.5 },
|
||||
}
|
||||
|
||||
export const fadeInUp = {
|
||||
initial: { opacity: 0, y: 40 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -20 },
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
|
||||
}
|
||||
|
||||
export const fadeInDown = {
|
||||
initial: { opacity: 0, y: -40 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
|
||||
}
|
||||
|
||||
export const fadeInLeft = {
|
||||
initial: { opacity: 0, x: -60 },
|
||||
animate: { opacity: 1, x: 0 },
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
|
||||
}
|
||||
|
||||
export const fadeInRight = {
|
||||
initial: { opacity: 0, x: 60 },
|
||||
animate: { opacity: 1, x: 0 },
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
|
||||
}
|
||||
|
||||
export const scaleIn = {
|
||||
initial: { opacity: 0, scale: 0.8 },
|
||||
animate: { opacity: 1, scale: 1 },
|
||||
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
|
||||
}
|
||||
|
||||
export const slideVariants = {
|
||||
enter: (direction: number) => ({
|
||||
x: direction > 0 ? '100%' : '-100%',
|
||||
opacity: 0,
|
||||
}),
|
||||
center: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
exit: (direction: number) => ({
|
||||
x: direction < 0 ? '100%' : '-100%',
|
||||
opacity: 0,
|
||||
}),
|
||||
}
|
||||
|
||||
export const staggerContainer = {
|
||||
animate: {
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const staggerItem = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.5 },
|
||||
}
|
||||
10
pitch-deck/lib/db.ts
Normal file
10
pitch-deck/lib/db.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Pool } from 'pg'
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgres://breakpilot:breakpilot123@localhost:5432/breakpilot_db',
|
||||
max: 5,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
})
|
||||
|
||||
export default pool
|
||||
98
pitch-deck/lib/hooks/useKeyboard.ts
Normal file
98
pitch-deck/lib/hooks/useKeyboard.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useCallback } from 'react'
|
||||
|
||||
interface UseKeyboardProps {
|
||||
onNext: () => void
|
||||
onPrev: () => void
|
||||
onFirst: () => void
|
||||
onLast: () => void
|
||||
onOverview: () => void
|
||||
onFullscreen: () => void
|
||||
onLanguageToggle: () => void
|
||||
onMenuToggle: () => void
|
||||
onGoToSlide: (index: number) => void
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useKeyboard({
|
||||
onNext,
|
||||
onPrev,
|
||||
onFirst,
|
||||
onLast,
|
||||
onOverview,
|
||||
onFullscreen,
|
||||
onLanguageToggle,
|
||||
onMenuToggle,
|
||||
onGoToSlide,
|
||||
enabled = true,
|
||||
}: UseKeyboardProps) {
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!enabled) return
|
||||
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
onNext()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
onPrev()
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
onOverview()
|
||||
break
|
||||
case 'f':
|
||||
case 'F':
|
||||
e.preventDefault()
|
||||
onFullscreen()
|
||||
break
|
||||
case 'Home':
|
||||
e.preventDefault()
|
||||
onFirst()
|
||||
break
|
||||
case 'End':
|
||||
e.preventDefault()
|
||||
onLast()
|
||||
break
|
||||
case 'l':
|
||||
case 'L':
|
||||
e.preventDefault()
|
||||
onLanguageToggle()
|
||||
break
|
||||
case 'm':
|
||||
case 'M':
|
||||
e.preventDefault()
|
||||
onMenuToggle()
|
||||
break
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
e.preventDefault()
|
||||
onGoToSlide(parseInt(e.key) - 1)
|
||||
break
|
||||
}
|
||||
},
|
||||
[enabled, onNext, onPrev, onFirst, onLast, onOverview, onFullscreen, onLanguageToggle, onMenuToggle, onGoToSlide]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [handleKeyDown])
|
||||
}
|
||||
39
pitch-deck/lib/hooks/useLanguage.ts
Normal file
39
pitch-deck/lib/hooks/useLanguage.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
||||
import { Language } from '../types'
|
||||
import React from 'react'
|
||||
|
||||
interface LanguageContextType {
|
||||
lang: Language
|
||||
toggleLanguage: () => void
|
||||
setLanguage: (lang: Language) => void
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType>({
|
||||
lang: 'de',
|
||||
toggleLanguage: () => {},
|
||||
setLanguage: () => {},
|
||||
})
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
const [lang, setLang] = useState<Language>('de')
|
||||
|
||||
const toggleLanguage = useCallback(() => {
|
||||
setLang(prev => prev === 'de' ? 'en' : 'de')
|
||||
}, [])
|
||||
|
||||
const setLanguage = useCallback((newLang: Language) => {
|
||||
setLang(newLang)
|
||||
}, [])
|
||||
|
||||
return React.createElement(
|
||||
LanguageContext.Provider,
|
||||
{ value: { lang, toggleLanguage, setLanguage } },
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
return useContext(LanguageContext)
|
||||
}
|
||||
29
pitch-deck/lib/hooks/usePitchData.ts
Normal file
29
pitch-deck/lib/hooks/usePitchData.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PitchData } from '../types'
|
||||
|
||||
export function usePitchData() {
|
||||
const [data, setData] = useState<PitchData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const res = await fetch('/api/data')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
} catch (err) {
|
||||
console.error('Failed to load pitch data:', err)
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return { data, loading, error }
|
||||
}
|
||||
77
pitch-deck/lib/hooks/useSlideNavigation.ts
Normal file
77
pitch-deck/lib/hooks/useSlideNavigation.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { SlideId } from '../types'
|
||||
|
||||
const SLIDE_ORDER: SlideId[] = [
|
||||
'cover',
|
||||
'problem',
|
||||
'solution',
|
||||
'product',
|
||||
'how-it-works',
|
||||
'market',
|
||||
'business-model',
|
||||
'traction',
|
||||
'competition',
|
||||
'team',
|
||||
'financials',
|
||||
'the-ask',
|
||||
'ai-qa',
|
||||
]
|
||||
|
||||
export const TOTAL_SLIDES = SLIDE_ORDER.length
|
||||
|
||||
export function useSlideNavigation() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [direction, setDirection] = useState(0)
|
||||
const [visitedSlides, setVisitedSlides] = useState<Set<number>>(new Set([0]))
|
||||
const [showOverview, setShowOverview] = useState(false)
|
||||
|
||||
const currentSlide = SLIDE_ORDER[currentIndex]
|
||||
|
||||
const goToSlide = useCallback((index: number) => {
|
||||
if (index < 0 || index >= TOTAL_SLIDES) return
|
||||
setDirection(index > currentIndex ? 1 : -1)
|
||||
setCurrentIndex(index)
|
||||
setVisitedSlides(prev => new Set([...prev, index]))
|
||||
setShowOverview(false)
|
||||
}, [currentIndex])
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
if (currentIndex < TOTAL_SLIDES - 1) {
|
||||
goToSlide(currentIndex + 1)
|
||||
}
|
||||
}, [currentIndex, goToSlide])
|
||||
|
||||
const prevSlide = useCallback(() => {
|
||||
if (currentIndex > 0) {
|
||||
goToSlide(currentIndex - 1)
|
||||
}
|
||||
}, [currentIndex, goToSlide])
|
||||
|
||||
const goToFirst = useCallback(() => goToSlide(0), [goToSlide])
|
||||
const goToLast = useCallback(() => goToSlide(TOTAL_SLIDES - 1), [goToSlide])
|
||||
|
||||
const toggleOverview = useCallback(() => {
|
||||
setShowOverview(prev => !prev)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
currentIndex,
|
||||
currentSlide,
|
||||
direction,
|
||||
visitedSlides,
|
||||
showOverview,
|
||||
totalSlides: TOTAL_SLIDES,
|
||||
slideOrder: SLIDE_ORDER,
|
||||
goToSlide,
|
||||
nextSlide,
|
||||
prevSlide,
|
||||
goToFirst,
|
||||
goToLast,
|
||||
toggleOverview,
|
||||
setShowOverview,
|
||||
isFirst: currentIndex === 0,
|
||||
isLast: currentIndex === TOTAL_SLIDES - 1,
|
||||
}
|
||||
}
|
||||
410
pitch-deck/lib/i18n.ts
Normal file
410
pitch-deck/lib/i18n.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { Language } from './types'
|
||||
|
||||
const translations = {
|
||||
de: {
|
||||
nav: {
|
||||
slides: 'Slides',
|
||||
fullscreen: 'Vollbild',
|
||||
language: 'Sprache',
|
||||
},
|
||||
slideNames: [
|
||||
'Cover',
|
||||
'Das Problem',
|
||||
'Die Loesung',
|
||||
'Produkte',
|
||||
'So funktioniert\'s',
|
||||
'Markt',
|
||||
'Geschaeftsmodell',
|
||||
'Traction',
|
||||
'Wettbewerb',
|
||||
'Team',
|
||||
'Finanzen',
|
||||
'The Ask',
|
||||
'KI Q&A',
|
||||
],
|
||||
cover: {
|
||||
tagline: 'Datensouveraenitaet meets KI-Compliance',
|
||||
subtitle: 'Pre-Seed · Q4 2026',
|
||||
cta: 'Pitch starten',
|
||||
},
|
||||
problem: {
|
||||
title: 'Das Problem',
|
||||
subtitle: 'Compliance-Komplexitaet ueberfordert den Mittelstand',
|
||||
cards: [
|
||||
{
|
||||
title: 'DSGVO',
|
||||
stat: '4.1 Mrd EUR',
|
||||
desc: 'Bussgelder seit 2018 in der EU. 83% der KMUs sind nicht vollstaendig konform.',
|
||||
},
|
||||
{
|
||||
title: 'AI Act',
|
||||
stat: 'Aug 2025',
|
||||
desc: 'Neue EU-Verordnung tritt in Kraft. Unternehmen muessen KI-Systeme klassifizieren und dokumentieren.',
|
||||
},
|
||||
{
|
||||
title: 'NIS2',
|
||||
stat: '30.000+',
|
||||
desc: 'Unternehmen in Deutschland neu betroffen. Cybersecurity-Anforderungen steigen massiv.',
|
||||
},
|
||||
],
|
||||
quote: 'Unternehmen brauchen keine weiteren Compliance-Tools — sie brauchen eine KI, die Compliance fuer sie erledigt.',
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Loesung',
|
||||
subtitle: 'ComplAI — Compliance auf Autopilot',
|
||||
pillars: [
|
||||
{
|
||||
title: 'Self-Hosted',
|
||||
desc: 'Eigene Hardware im Serverraum. Kein Byte verlaesst das Unternehmen. Volle Datensouveraenitaet.',
|
||||
icon: 'server',
|
||||
},
|
||||
{
|
||||
title: 'Auto-Compliance',
|
||||
desc: 'KI erledigt DSGVO, AI Act und NIS2 automatisch. Dokumentation, Audits und Updates — alles KI-gesteuert.',
|
||||
icon: 'shield',
|
||||
},
|
||||
{
|
||||
title: 'KI-Assistent',
|
||||
desc: 'Vollautonomer Kundensupport. Beantwortet Fragen, aendert Dokumente, bereitet Audits vor — 24/7.',
|
||||
icon: 'bot',
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
title: 'Unsere Produkte',
|
||||
subtitle: 'Drei Tiers fuer jede Unternehmensgroesse',
|
||||
monthly: '/Monat',
|
||||
hardware: 'Hardware',
|
||||
llm: 'KI-Modell',
|
||||
popular: 'Beliebt',
|
||||
features: 'Features',
|
||||
},
|
||||
howItWorks: {
|
||||
title: 'So funktioniert\'s',
|
||||
subtitle: 'In 4 Schritten zur vollstaendigen Compliance',
|
||||
steps: [
|
||||
{
|
||||
title: 'Hardware aufstellen',
|
||||
desc: 'Mac Mini oder Mac Studio im Serverraum anschliessen. Plug & Play — keine Cloud noetig.',
|
||||
},
|
||||
{
|
||||
title: 'KI konfigurieren',
|
||||
desc: 'Branche, Groesse und Regularien angeben. Die KI erstellt automatisch alle Compliance-Dokumente.',
|
||||
},
|
||||
{
|
||||
title: 'Compliance automatisieren',
|
||||
desc: 'Laufende Ueberwachung, automatische Updates bei Rechtsaenderungen und Audit-Vorbereitung.',
|
||||
},
|
||||
{
|
||||
title: 'Audit bestehen',
|
||||
desc: 'Vollstaendige Dokumentation auf Knopfdruck. Behoerdenanfragen werden KI-gestuetzt beantwortet.',
|
||||
},
|
||||
],
|
||||
},
|
||||
market: {
|
||||
title: 'Marktchance',
|
||||
subtitle: 'Der Compliance-Markt waechst zweistellig',
|
||||
tam: 'TAM',
|
||||
sam: 'SAM',
|
||||
som: 'SOM',
|
||||
tamLabel: 'Total Addressable Market',
|
||||
samLabel: 'Serviceable Addressable Market',
|
||||
somLabel: 'Serviceable Obtainable Market',
|
||||
source: 'Quelle',
|
||||
growth: 'Wachstum p.a.',
|
||||
},
|
||||
businessModel: {
|
||||
title: 'Geschaeftsmodell',
|
||||
subtitle: 'Recurring Revenue mit Hardware-Moat',
|
||||
unitEconomics: 'Unit Economics',
|
||||
amortization: 'Amortisation',
|
||||
margin: 'Marge',
|
||||
months: 'Monate',
|
||||
recurringRevenue: 'Recurring Revenue',
|
||||
hardwareCost: 'Hardware-EK',
|
||||
operatingCost: 'Betriebskosten',
|
||||
},
|
||||
traction: {
|
||||
title: 'Traction & Meilensteine',
|
||||
subtitle: 'Unser bisheriger Fortschritt',
|
||||
completed: 'Abgeschlossen',
|
||||
inProgress: 'In Arbeit',
|
||||
planned: 'Geplant',
|
||||
},
|
||||
competition: {
|
||||
title: 'Wettbewerb',
|
||||
subtitle: 'Was uns differenziert',
|
||||
feature: 'Feature',
|
||||
selfHosted: 'Self-Hosted',
|
||||
integratedAI: 'Integrierte KI',
|
||||
autonomousSupport: 'Autonomer Support',
|
||||
yes: 'Ja',
|
||||
no: 'Nein',
|
||||
partial: 'Teilweise',
|
||||
},
|
||||
team: {
|
||||
title: 'Das Team',
|
||||
subtitle: 'Gruender mit Domain-Expertise',
|
||||
equity: 'Equity',
|
||||
expertise: 'Expertise',
|
||||
},
|
||||
financials: {
|
||||
title: 'Finanzprognose',
|
||||
subtitle: 'AI-First Kostenstruktur — skaliert ohne lineares Personalwachstum',
|
||||
revenue: 'Umsatz',
|
||||
costs: 'Kosten',
|
||||
customers: 'Kunden',
|
||||
mrr: 'MRR',
|
||||
arr: 'ARR',
|
||||
burnRate: 'Burn Rate',
|
||||
employees: 'Mitarbeiter',
|
||||
year: 'Jahr',
|
||||
sliderGrowth: 'Wachstumsrate',
|
||||
sliderChurn: 'Churn Rate',
|
||||
sliderArpu: 'ARPU',
|
||||
adjustAssumptions: 'Annahmen anpassen',
|
||||
},
|
||||
theAsk: {
|
||||
title: 'The Ask',
|
||||
subtitle: 'Pre-Seed Finanzierung',
|
||||
amount: 'Funding',
|
||||
instrument: 'Instrument',
|
||||
useOfFunds: 'Use of Funds',
|
||||
engineering: 'Engineering',
|
||||
sales: 'Vertrieb',
|
||||
hardware: 'Hardware',
|
||||
legal: 'Legal',
|
||||
reserve: 'Reserve',
|
||||
targetDate: 'Zieldatum',
|
||||
},
|
||||
aiqa: {
|
||||
title: 'Fragen? Die KI antwortet.',
|
||||
subtitle: 'Stellen Sie Ihre Investorenfragen — unser AI Agent antwortet mit Echtdaten.',
|
||||
placeholder: 'Stellen Sie eine Frage zum Investment...',
|
||||
send: 'Senden',
|
||||
thinking: 'Denke nach...',
|
||||
suggestions: [
|
||||
'Wie skaliert das Geschaeftsmodell?',
|
||||
'Was ist der unfaire Vorteil?',
|
||||
'Wie sieht die Exit-Strategie aus?',
|
||||
'Warum Self-Hosting statt Cloud?',
|
||||
],
|
||||
},
|
||||
},
|
||||
en: {
|
||||
nav: {
|
||||
slides: 'Slides',
|
||||
fullscreen: 'Fullscreen',
|
||||
language: 'Language',
|
||||
},
|
||||
slideNames: [
|
||||
'Cover',
|
||||
'The Problem',
|
||||
'The Solution',
|
||||
'Products',
|
||||
'How It Works',
|
||||
'Market',
|
||||
'Business Model',
|
||||
'Traction',
|
||||
'Competition',
|
||||
'Team',
|
||||
'Financials',
|
||||
'The Ask',
|
||||
'AI Q&A',
|
||||
],
|
||||
cover: {
|
||||
tagline: 'Data Sovereignty meets AI Compliance',
|
||||
subtitle: 'Pre-Seed · Q4 2026',
|
||||
cta: 'Start Pitch',
|
||||
},
|
||||
problem: {
|
||||
title: 'The Problem',
|
||||
subtitle: 'Compliance complexity overwhelms SMEs',
|
||||
cards: [
|
||||
{
|
||||
title: 'GDPR',
|
||||
stat: 'EUR 4.1B',
|
||||
desc: 'in fines since 2018 across the EU. 83% of SMEs are not fully compliant.',
|
||||
},
|
||||
{
|
||||
title: 'AI Act',
|
||||
stat: 'Aug 2025',
|
||||
desc: 'New EU regulation takes effect. Companies must classify and document AI systems.',
|
||||
},
|
||||
{
|
||||
title: 'NIS2',
|
||||
stat: '30,000+',
|
||||
desc: 'companies newly affected in Germany. Cybersecurity requirements increase massively.',
|
||||
},
|
||||
],
|
||||
quote: 'Companies don\'t need more compliance tools — they need an AI that handles compliance for them.',
|
||||
},
|
||||
solution: {
|
||||
title: 'The Solution',
|
||||
subtitle: 'ComplAI — Compliance on Autopilot',
|
||||
pillars: [
|
||||
{
|
||||
title: 'Self-Hosted',
|
||||
desc: 'Own hardware in your server room. No data leaves the company. Full data sovereignty.',
|
||||
icon: 'server',
|
||||
},
|
||||
{
|
||||
title: 'Auto-Compliance',
|
||||
desc: 'AI handles GDPR, AI Act and NIS2 automatically. Documentation, audits and updates — all AI-powered.',
|
||||
icon: 'shield',
|
||||
},
|
||||
{
|
||||
title: 'AI Assistant',
|
||||
desc: 'Fully autonomous customer support. Answers questions, modifies documents, prepares audits — 24/7.',
|
||||
icon: 'bot',
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
title: 'Our Products',
|
||||
subtitle: 'Three tiers for every company size',
|
||||
monthly: '/month',
|
||||
hardware: 'Hardware',
|
||||
llm: 'AI Model',
|
||||
popular: 'Popular',
|
||||
features: 'Features',
|
||||
},
|
||||
howItWorks: {
|
||||
title: 'How It Works',
|
||||
subtitle: 'Full compliance in 4 steps',
|
||||
steps: [
|
||||
{
|
||||
title: 'Set Up Hardware',
|
||||
desc: 'Connect Mac Mini or Mac Studio in your server room. Plug & Play — no cloud needed.',
|
||||
},
|
||||
{
|
||||
title: 'Configure AI',
|
||||
desc: 'Specify industry, size, and regulations. The AI automatically creates all compliance documents.',
|
||||
},
|
||||
{
|
||||
title: 'Automate Compliance',
|
||||
desc: 'Continuous monitoring, automatic updates for regulatory changes and audit preparation.',
|
||||
},
|
||||
{
|
||||
title: 'Pass Audits',
|
||||
desc: 'Complete documentation at the push of a button. Authority inquiries answered AI-powered.',
|
||||
},
|
||||
],
|
||||
},
|
||||
market: {
|
||||
title: 'Market Opportunity',
|
||||
subtitle: 'The compliance market grows double-digit',
|
||||
tam: 'TAM',
|
||||
sam: 'SAM',
|
||||
som: 'SOM',
|
||||
tamLabel: 'Total Addressable Market',
|
||||
samLabel: 'Serviceable Addressable Market',
|
||||
somLabel: 'Serviceable Obtainable Market',
|
||||
source: 'Source',
|
||||
growth: 'Growth p.a.',
|
||||
},
|
||||
businessModel: {
|
||||
title: 'Business Model',
|
||||
subtitle: 'Recurring Revenue with Hardware Moat',
|
||||
unitEconomics: 'Unit Economics',
|
||||
amortization: 'Amortization',
|
||||
margin: 'Margin',
|
||||
months: 'months',
|
||||
recurringRevenue: 'Recurring Revenue',
|
||||
hardwareCost: 'Hardware Cost',
|
||||
operatingCost: 'Operating Cost',
|
||||
},
|
||||
traction: {
|
||||
title: 'Traction & Milestones',
|
||||
subtitle: 'Our progress so far',
|
||||
completed: 'Completed',
|
||||
inProgress: 'In Progress',
|
||||
planned: 'Planned',
|
||||
},
|
||||
competition: {
|
||||
title: 'Competition',
|
||||
subtitle: 'What differentiates us',
|
||||
feature: 'Feature',
|
||||
selfHosted: 'Self-Hosted',
|
||||
integratedAI: 'Integrated AI',
|
||||
autonomousSupport: 'Autonomous Support',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
partial: 'Partial',
|
||||
},
|
||||
team: {
|
||||
title: 'The Team',
|
||||
subtitle: 'Founders with domain expertise',
|
||||
equity: 'Equity',
|
||||
expertise: 'Expertise',
|
||||
},
|
||||
financials: {
|
||||
title: 'Financial Projections',
|
||||
subtitle: 'AI-First cost structure — scales without linear headcount growth',
|
||||
revenue: 'Revenue',
|
||||
costs: 'Costs',
|
||||
customers: 'Customers',
|
||||
mrr: 'MRR',
|
||||
arr: 'ARR',
|
||||
burnRate: 'Burn Rate',
|
||||
employees: 'Employees',
|
||||
year: 'Year',
|
||||
sliderGrowth: 'Growth Rate',
|
||||
sliderChurn: 'Churn Rate',
|
||||
sliderArpu: 'ARPU',
|
||||
adjustAssumptions: 'Adjust Assumptions',
|
||||
},
|
||||
theAsk: {
|
||||
title: 'The Ask',
|
||||
subtitle: 'Pre-Seed Funding',
|
||||
amount: 'Funding',
|
||||
instrument: 'Instrument',
|
||||
useOfFunds: 'Use of Funds',
|
||||
engineering: 'Engineering',
|
||||
sales: 'Sales',
|
||||
hardware: 'Hardware',
|
||||
legal: 'Legal',
|
||||
reserve: 'Reserve',
|
||||
targetDate: 'Target Date',
|
||||
},
|
||||
aiqa: {
|
||||
title: 'Questions? The AI answers.',
|
||||
subtitle: 'Ask your investor questions — our AI agent responds with real data.',
|
||||
placeholder: 'Ask a question about the investment...',
|
||||
send: 'Send',
|
||||
thinking: 'Thinking...',
|
||||
suggestions: [
|
||||
'How does the business model scale?',
|
||||
'What is the unfair advantage?',
|
||||
'What does the exit strategy look like?',
|
||||
'Why self-hosting instead of cloud?',
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function t(lang: Language): typeof translations.de {
|
||||
return translations[lang]
|
||||
}
|
||||
|
||||
export function formatEur(value: number, lang: Language): string {
|
||||
if (value >= 1_000_000_000) {
|
||||
const v = (value / 1_000_000_000).toFixed(1)
|
||||
return lang === 'de' ? `${v} Mrd. EUR` : `EUR ${v}B`
|
||||
}
|
||||
if (value >= 1_000_000) {
|
||||
const v = (value / 1_000_000).toFixed(1)
|
||||
return lang === 'de' ? `${v} Mio. EUR` : `EUR ${v}M`
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
const v = (value / 1_000).toFixed(0)
|
||||
return lang === 'de' ? `${v}k EUR` : `EUR ${v}k`
|
||||
}
|
||||
return lang === 'de' ? `${value} EUR` : `EUR ${value}`
|
||||
}
|
||||
|
||||
export function formatNumber(value: number): string {
|
||||
return new Intl.NumberFormat('de-DE').format(value)
|
||||
}
|
||||
|
||||
export default translations
|
||||
150
pitch-deck/lib/types.ts
Normal file
150
pitch-deck/lib/types.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
export interface PitchCompany {
|
||||
id: number
|
||||
name: string
|
||||
legal_form: string
|
||||
founding_date: string
|
||||
tagline_de: string
|
||||
tagline_en: string
|
||||
mission_de: string
|
||||
mission_en: string
|
||||
website: string
|
||||
hq_city: string
|
||||
}
|
||||
|
||||
export interface PitchTeamMember {
|
||||
id: number
|
||||
name: string
|
||||
role_de: string
|
||||
role_en: string
|
||||
bio_de: string
|
||||
bio_en: string
|
||||
equity_pct: number
|
||||
expertise: string[]
|
||||
linkedin_url: string
|
||||
photo_url: string
|
||||
}
|
||||
|
||||
export interface PitchFinancial {
|
||||
id: number
|
||||
year: number
|
||||
revenue_eur: number
|
||||
costs_eur: number
|
||||
mrr_eur: number
|
||||
burn_rate_eur: number
|
||||
customers_count: number
|
||||
employees_count: number
|
||||
arr_eur: number
|
||||
}
|
||||
|
||||
export interface PitchMarket {
|
||||
id: number
|
||||
market_segment: string
|
||||
label: string
|
||||
value_eur: number
|
||||
growth_rate_pct: number
|
||||
source: string
|
||||
}
|
||||
|
||||
export interface PitchCompetitor {
|
||||
id: number
|
||||
name: string
|
||||
customers_count: number
|
||||
pricing_range: string
|
||||
strengths: string[]
|
||||
weaknesses: string[]
|
||||
website: string
|
||||
}
|
||||
|
||||
export interface PitchFeature {
|
||||
id: number
|
||||
feature_name_de: string
|
||||
feature_name_en: string
|
||||
category: string
|
||||
breakpilot: boolean
|
||||
proliance: boolean
|
||||
dataguard: boolean
|
||||
heydata: boolean
|
||||
is_differentiator: boolean
|
||||
}
|
||||
|
||||
export interface PitchMilestone {
|
||||
id: number
|
||||
milestone_date: string
|
||||
title_de: string
|
||||
title_en: string
|
||||
description_de: string
|
||||
description_en: string
|
||||
status: 'completed' | 'in_progress' | 'planned'
|
||||
category: string
|
||||
}
|
||||
|
||||
export interface PitchMetric {
|
||||
id: number
|
||||
metric_name: string
|
||||
label_de: string
|
||||
label_en: string
|
||||
value: string
|
||||
unit: string
|
||||
is_live: boolean
|
||||
}
|
||||
|
||||
export interface PitchFunding {
|
||||
id: number
|
||||
round_name: string
|
||||
amount_eur: number
|
||||
use_of_funds: { category: string; percentage: number; label_de: string; label_en: string }[]
|
||||
instrument: string
|
||||
target_date: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface PitchProduct {
|
||||
id: number
|
||||
name: string
|
||||
hardware: string
|
||||
hardware_cost_eur: number
|
||||
monthly_price_eur: number
|
||||
llm_model: string
|
||||
llm_size: string
|
||||
llm_capability_de: string
|
||||
llm_capability_en: string
|
||||
features_de: string[]
|
||||
features_en: string[]
|
||||
is_popular: boolean
|
||||
operating_cost_eur: number
|
||||
}
|
||||
|
||||
export interface PitchData {
|
||||
company: PitchCompany
|
||||
team: PitchTeamMember[]
|
||||
financials: PitchFinancial[]
|
||||
market: PitchMarket[]
|
||||
competitors: PitchCompetitor[]
|
||||
features: PitchFeature[]
|
||||
milestones: PitchMilestone[]
|
||||
metrics: PitchMetric[]
|
||||
funding: PitchFunding
|
||||
products: PitchProduct[]
|
||||
}
|
||||
|
||||
export type Language = 'de' | 'en'
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
export type SlideId =
|
||||
| 'cover'
|
||||
| 'problem'
|
||||
| 'solution'
|
||||
| 'product'
|
||||
| 'how-it-works'
|
||||
| 'market'
|
||||
| 'business-model'
|
||||
| 'traction'
|
||||
| 'competition'
|
||||
| 'team'
|
||||
| 'financials'
|
||||
| 'the-ask'
|
||||
| 'ai-qa'
|
||||
10
pitch-deck/next.config.js
Normal file
10
pitch-deck/next.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
29
pitch-deck/package.json
Normal file
29
pitch-deck/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "breakpilot-pitch-deck",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3012",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3012"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.15.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^15.1.0",
|
||||
"pg": "^8.13.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/react": "^18.3.16",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
9
pitch-deck/postcss.config.mjs
Normal file
9
pitch-deck/postcss.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
29
pitch-deck/tailwind.config.ts
Normal file
29
pitch-deck/tailwind.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
pitch: {
|
||||
dark: '#0a0a1a',
|
||||
card: 'rgba(255, 255, 255, 0.08)',
|
||||
border: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
},
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||
21
pitch-deck/tsconfig.json
Normal file
21
pitch-deck/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./*"] },
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user