diff --git a/pitch-deck/app/api/admin/investors/[id]/generate-link/route.ts b/pitch-deck/app/api/admin/investors/[id]/generate-link/route.ts index d0586fc..b84842f 100644 --- a/pitch-deck/app/api/admin/investors/[id]/generate-link/route.ts +++ b/pitch-deck/app/api/admin/investors/[id]/generate-link/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import pool from '@/lib/db' import { generateToken } from '@/lib/auth' import { requireAdmin, logAdminAudit } from '@/lib/admin-auth' +import { createShortLink } from '@/lib/short-links' interface RouteContext { params: Promise<{ id: string }> @@ -47,6 +48,7 @@ export async function POST(request: NextRequest, ctx: RouteContext) { const baseUrl = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.ai' const url = `${baseUrl}/auth/verify?token=${token}` + const shortUrl = await createShortLink(token) await logAdminAudit( adminId, @@ -56,5 +58,5 @@ export async function POST(request: NextRequest, ctx: RouteContext) { investor.id, ) - return NextResponse.json({ url, expires_at: expiresAt.toISOString() }) + return NextResponse.json({ url, short_url: shortUrl, expires_at: expiresAt.toISOString() }) } diff --git a/pitch-deck/app/api/admin/investors/[id]/resend/route.ts b/pitch-deck/app/api/admin/investors/[id]/resend/route.ts index 6156ab7..92f9617 100644 --- a/pitch-deck/app/api/admin/investors/[id]/resend/route.ts +++ b/pitch-deck/app/api/admin/investors/[id]/resend/route.ts @@ -4,6 +4,7 @@ import { generateToken } from '@/lib/auth' import { requireAdmin, logAdminAudit } from '@/lib/admin-auth' import { sendMagicLinkEmail } from '@/lib/email' import { checkRateLimit, RATE_LIMITS } from '@/lib/rate-limit' +import { createShortLink } from '@/lib/short-links' interface RouteContext { params: Promise<{ id: string }> @@ -44,9 +45,8 @@ export async function POST(request: NextRequest, ctx: RouteContext) { [investor.id, token, expiresAt], ) - const baseUrl = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.ai' - const magicLinkUrl = `${baseUrl}/auth/verify?token=${token}` - await sendMagicLinkEmail(investor.email, investor.name, magicLinkUrl) + const shortUrl = await createShortLink(token) + await sendMagicLinkEmail(investor.email, investor.name, shortUrl) await logAdminAudit( adminId, diff --git a/pitch-deck/app/api/admin/investors/[id]/route.ts b/pitch-deck/app/api/admin/investors/[id]/route.ts index 343b489..a6b8146 100644 --- a/pitch-deck/app/api/admin/investors/[id]/route.ts +++ b/pitch-deck/app/api/admin/investors/[id]/route.ts @@ -19,7 +19,7 @@ export async function GET(request: NextRequest, ctx: RouteContext) { pool.query( `SELECT i.id, i.email, i.name, i.company, i.status, i.last_login_at, i.login_count, i.created_at, i.updated_at, i.first_activity_at, i.data_masked_at, - i.assigned_version_id, i.is_showcase, + i.assigned_version_id, i.is_showcase, i.preferred_lang, v.name AS version_name, v.status AS version_status FROM pitch_investors i LEFT JOIN pitch_versions v ON v.id = i.assigned_version_id @@ -68,14 +68,14 @@ export async function PATCH(request: NextRequest, ctx: RouteContext) { const { id } = await ctx.params const body = await request.json().catch(() => ({})) - const { name, company, assigned_version_id, is_showcase } = body + const { name, company, assigned_version_id, is_showcase, preferred_lang } = body - if (name === undefined && company === undefined && assigned_version_id === undefined && is_showcase === undefined) { - return NextResponse.json({ error: 'name, company, assigned_version_id, or is_showcase required' }, { status: 400 }) + if (name === undefined && company === undefined && assigned_version_id === undefined && is_showcase === undefined && preferred_lang === undefined) { + return NextResponse.json({ error: 'name, company, assigned_version_id, is_showcase, or preferred_lang required' }, { status: 400 }) } const before = await pool.query( - `SELECT name, company, assigned_version_id, is_showcase FROM pitch_investors WHERE id = $1`, + `SELECT name, company, assigned_version_id, is_showcase, preferred_lang FROM pitch_investors WHERE id = $1`, [id], ) if (before.rows.length === 0) { @@ -98,8 +98,8 @@ export async function PATCH(request: NextRequest, ctx: RouteContext) { // Use null to clear version assignment, undefined to leave unchanged const versionValue = assigned_version_id === undefined ? before.rows[0].assigned_version_id : (assigned_version_id || null) - const showcaseValue = is_showcase !== undefined ? Boolean(is_showcase) : before.rows[0].is_showcase + const langValue = preferred_lang === 'en' || preferred_lang === 'de' ? preferred_lang : before.rows[0].preferred_lang const { rows } = await pool.query( `UPDATE pitch_investors SET @@ -107,10 +107,11 @@ export async function PATCH(request: NextRequest, ctx: RouteContext) { company = COALESCE($2, company), assigned_version_id = $4, is_showcase = $5, + preferred_lang = $6, updated_at = NOW() WHERE id = $3 - RETURNING id, email, name, company, status, assigned_version_id, is_showcase`, - [name ?? null, company ?? null, id, versionValue, showcaseValue], + RETURNING id, email, name, company, status, assigned_version_id, is_showcase, preferred_lang`, + [name ?? null, company ?? null, id, versionValue, showcaseValue, langValue], ) const action = assigned_version_id !== undefined && assigned_version_id !== before.rows[0].assigned_version_id diff --git a/pitch-deck/app/api/admin/invite/route.ts b/pitch-deck/app/api/admin/invite/route.ts index c5aefe1..1de9957 100644 --- a/pitch-deck/app/api/admin/invite/route.ts +++ b/pitch-deck/app/api/admin/invite/route.ts @@ -3,6 +3,7 @@ import pool from '@/lib/db' import { generateToken } from '@/lib/auth' import { requireAdmin, logAdminAudit } from '@/lib/admin-auth' import { sendMagicLinkEmail } from '@/lib/email' +import { createShortLink } from '@/lib/short-links' import { checkRateLimit, RATE_LIMITS } from '@/lib/rate-limit' export async function POST(request: NextRequest) { @@ -11,7 +12,7 @@ export async function POST(request: NextRequest) { const adminId = guard.kind === 'admin' ? guard.admin.id : null const body = await request.json().catch(() => ({})) - const { email, name, company, greeting, message, closing } = body + const { email, name, company, greeting, message, closing, lang = 'de', send_email = true, version_id } = body if (!email || typeof email !== 'string') { return NextResponse.json({ error: 'Email required' }, { status: 400 }) @@ -25,17 +26,36 @@ export async function POST(request: NextRequest) { const normalizedEmail = email.toLowerCase().trim() + const normalizedLang = lang === 'en' ? 'en' : 'de' + + // Validate version if provided + const normalizedVersionId = version_id || null + if (normalizedVersionId) { + const ver = await pool.query( + `SELECT id, status FROM pitch_versions WHERE id = $1`, + [normalizedVersionId], + ) + if (ver.rows.length === 0) { + return NextResponse.json({ error: 'Version not found' }, { status: 404 }) + } + if (ver.rows[0].status !== 'committed') { + return NextResponse.json({ error: 'Can only assign committed versions' }, { status: 400 }) + } + } + // Upsert investor const { rows } = await pool.query( - `INSERT INTO pitch_investors (email, name, company) - VALUES ($1, $2, $3) + `INSERT INTO pitch_investors (email, name, company, preferred_lang, assigned_version_id) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email) DO UPDATE SET name = COALESCE(EXCLUDED.name, pitch_investors.name), company = COALESCE(EXCLUDED.company, pitch_investors.company), + preferred_lang = EXCLUDED.preferred_lang, + assigned_version_id = COALESCE(EXCLUDED.assigned_version_id, pitch_investors.assigned_version_id), status = CASE WHEN pitch_investors.status = 'revoked' THEN 'invited' ELSE pitch_investors.status END, updated_at = NOW() RETURNING id, status`, - [normalizedEmail, name || null, company || null], + [normalizedEmail, name || null, company || null, normalizedLang, normalizedVersionId], ) const investor = rows[0] @@ -53,13 +73,24 @@ export async function POST(request: NextRequest) { const baseUrl = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.ai' const magicLinkUrl = `${baseUrl}/auth/verify?token=${token}` + const shortUrl = await createShortLink(token) - await sendMagicLinkEmail(normalizedEmail, name || null, magicLinkUrl, greeting, message, closing) + if (send_email) { + await sendMagicLinkEmail(normalizedEmail, name || null, shortUrl, greeting, message, closing, normalizedLang) + } await logAdminAudit( adminId, 'investor_invited', - { email: normalizedEmail, name: name || null, company: company || null, expires_at: expiresAt.toISOString() }, + { + email: normalizedEmail, + name: name || null, + company: company || null, + expires_at: expiresAt.toISOString(), + lang: normalizedLang, + send_email: !!send_email, + version_id: normalizedVersionId, + }, request, investor.id, ) @@ -69,5 +100,7 @@ export async function POST(request: NextRequest) { investor_id: investor.id, email: normalizedEmail, expires_at: expiresAt.toISOString(), + magic_link_url: magicLinkUrl, + short_url: shortUrl, }) } diff --git a/pitch-deck/app/api/admin/migrate/route.ts b/pitch-deck/app/api/admin/migrate/route.ts index 8299282..18e2f1f 100644 --- a/pitch-deck/app/api/admin/migrate/route.ts +++ b/pitch-deck/app/api/admin/migrate/route.ts @@ -160,6 +160,16 @@ export async function POST(request: NextRequest) { `ALTER TABLE dataroom_documents ADD COLUMN IF NOT EXISTS description_en TEXT`, `ALTER TABLE dataroom_investor_uploads ADD COLUMN IF NOT EXISTS description_de TEXT`, `ALTER TABLE dataroom_investor_uploads ADD COLUMN IF NOT EXISTS description_en TEXT`, + // 007 — investor preferred language + `ALTER TABLE pitch_investors ADD COLUMN IF NOT EXISTS preferred_lang VARCHAR(5) NOT NULL DEFAULT 'de'`, + // 008 — short links for magic URLs + `CREATE TABLE IF NOT EXISTS pitch_short_links ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + short_code VARCHAR(10) UNIQUE NOT NULL, + token VARCHAR(128) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() + )`, + `CREATE INDEX IF NOT EXISTS idx_pitch_short_links_code ON pitch_short_links(short_code)`, ] for (const sql of statements) { diff --git a/pitch-deck/app/api/auth/me/route.ts b/pitch-deck/app/api/auth/me/route.ts index 55dd2ec..40b6fdd 100644 --- a/pitch-deck/app/api/auth/me/route.ts +++ b/pitch-deck/app/api/auth/me/route.ts @@ -14,7 +14,7 @@ export async function GET() { } const { rows } = await pool.query( - `SELECT id, email, name, company, status, last_login_at, login_count, created_at, is_showcase + `SELECT id, email, name, company, status, last_login_at, login_count, created_at, is_showcase, preferred_lang FROM pitch_investors WHERE id = $1`, [session.sub] ) diff --git a/pitch-deck/app/auth/verify/page.tsx b/pitch-deck/app/auth/verify/page.tsx index d363ffb..546481d 100644 --- a/pitch-deck/app/auth/verify/page.tsx +++ b/pitch-deck/app/auth/verify/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Suspense, useEffect, useState } from 'react' +import { Suspense, useEffect, useState, useCallback } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { motion } from 'framer-motion' @@ -9,7 +9,7 @@ function VerifyContent() { const router = useRouter() const token = searchParams.get('token') - const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying') + const [status, setStatus] = useState<'ready' | 'verifying' | 'success' | 'error'>('ready') const [errorMsg, setErrorMsg] = useState('') useEffect(() => { @@ -19,38 +19,37 @@ function VerifyContent() { return } - async function verify() { - try { - // If the investor already has a valid session, skip token verification - const sessionCheck = await fetch('/api/auth/me') - if (sessionCheck.ok) { - router.push('/') - return - } - - const res = await fetch('/api/auth/verify', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token }), - }) - - if (res.ok) { - setStatus('success') - setTimeout(() => router.push('/'), 1000) - } else { - const data = await res.json() - setStatus('error') - setErrorMsg(data.error || 'Verification failed.') - } - } catch { - setStatus('error') - setErrorMsg('Network error. Please try again.') - } - } - - verify() + // If the investor already has a valid session, skip the button entirely + fetch('/api/auth/me').then(res => { + if (res.ok) router.push('/') + }) }, [token, router]) + const handleAccess = useCallback(async () => { + if (!token || status === 'verifying') return + setStatus('verifying') + + try { + const res = await fetch('/api/auth/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }) + + if (res.ok) { + setStatus('success') + setTimeout(() => router.push('/'), 1000) + } else { + const data = await res.json() + setStatus('error') + setErrorMsg(data.error || 'Verification failed.') + } + } catch { + setStatus('error') + setErrorMsg('Network error. Please try again.') + } + }, [token, status, router]) + return (
+ {status === 'ready' && ( + <> +
+ + + +
+

Your pitch deck is ready

+

Click below to access it.

+ + + )} + {status === 'verifying' && ( <>
-

Verifying your access link...

+

Verifying your access...

)} @@ -115,11 +132,10 @@ export default function VerifyPage() { - {/* Privacy Notice Footer */}

- Datenschutzhinweis (Art. 13 DSGVO): Beim Zugriff werden technische Zugriffsdaten (IP-Adresse, Zeitpunkt, Browser) sowie – soweit eingeladen – personenbezogene Kontaktdaten (E-Mail, Name, Unternehmen) verarbeitet. Zweck: Zugangsverwaltung und Missbrauchsprävention. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse). Speicherdauer: max. 30 Tage nach letztem Zugriff; nicht aktivierte Zugänge nach 90 Tagen. Danach automatische Anonymisierung. Ihre Rechte gem. Art. 15–21 DSGVO (Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit, Widerspruch): Anfragen an pitch@breakpilot.ai. Beschwerderecht bei der Aufsichtsbehörde: LfDI Baden-Württemberg (www.baden-wuerttemberg.datenschutz.de).

+ Datenschutzhinweis (Art. 13 DSGVO): Beim Zugriff werden technische Zugriffsdaten (IP-Adresse, Zeitpunkt, Browser) sowie – soweit eingeladen – personenbezogene Kontaktdaten (E-Mail, Name, Unternehmen) verarbeitet. Zweck: Zugangsverwaltung und Missbrauchsprävention. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse). Speicherdauer: max. 30 Tage nach letztem Zugriff; nicht aktivierte Zugänge nach 90 Tagen. Danach automatische Anonymisierung. Ihre Rechte gem. Art. 15–21 DSGVO (Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit, Widerspruch): Anfragen an pitch@breakpilot.ai. Beschwerderecht bei der Aufsichtsbehörde: LfDI Baden-Württemberg (www.baden-wuerttemberg.datenschutz.de).

Verantwortlich: Benjamin Bönisch & Sharang Parnerkar · Kontakt: info@breakpilot.com

diff --git a/pitch-deck/app/p/[code]/route.ts b/pitch-deck/app/p/[code]/route.ts new file mode 100644 index 0000000..4f0f981 --- /dev/null +++ b/pitch-deck/app/p/[code]/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' + +interface Ctx { params: Promise<{ code: string }> } + +const BASE_URL = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.com' + +export async function GET(request: NextRequest, ctx: Ctx) { + const { code } = await ctx.params + + const { rows } = await pool.query( + `SELECT sl.token FROM pitch_short_links sl + JOIN pitch_magic_links ml ON ml.token = sl.token + WHERE sl.short_code = $1 AND ml.expires_at > NOW()`, + [code.toLowerCase()], + ) + + if (rows.length === 0) { + return NextResponse.redirect(`${BASE_URL}/auth?error=invalid`) + } + + return NextResponse.redirect(`${BASE_URL}/auth/verify?token=${rows[0].token}`) +} diff --git a/pitch-deck/app/page.tsx b/pitch-deck/app/page.tsx index 0c5d812..929cfdb 100644 --- a/pitch-deck/app/page.tsx +++ b/pitch-deck/app/page.tsx @@ -1,19 +1,29 @@ 'use client' -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect, useRef } from 'react' import { Language } from '@/lib/types' import { useAuth } from '@/lib/hooks/useAuth' import PitchDeck from '@/components/PitchDeck' export default function Home() { - const [lang, setLang] = useState('de') const { investor, loading, logout } = useAuth() + const [lang, setLang] = useState('de') + const [langReady, setLangReady] = useState(false) + const synced = useRef(false) + + useEffect(() => { + if (!loading && !synced.current) { + synced.current = true + if (investor?.preferred_lang === 'en') setLang('en') + setLangReady(true) + } + }, [loading, investor]) const toggleLanguage = useCallback(() => { setLang(prev => prev === 'de' ? 'en' : 'de') }, []) - if (loading) { + if (loading || !langReady) { return (
diff --git a/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx b/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx index 640bfb8..ced1dbc 100644 --- a/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx @@ -22,6 +22,7 @@ interface InvestorDetail { version_name: string | null version_status: string | null is_showcase: boolean + preferred_lang: 'de' | 'en' } sessions: Array<{ id: string @@ -115,11 +116,12 @@ export default function InvestorDetailPage() { setBusy(false) if (res.ok) { const d = await res.json() + const link = d.short_url || d.url try { - await navigator.clipboard.writeText(d.url) - flashToast('Magic link copied to clipboard') + await navigator.clipboard.writeText(link) + flashToast('Short link copied to clipboard') } catch { - flashToast(`Link (copy manually): ${d.url}`) + flashToast(`Link (copy manually): ${link}`) } } else { const err = await res.json().catch(() => ({})) @@ -324,6 +326,39 @@ export default function InvestorDetailPage() {
+ {/* Default language */} +
+
+
+
Default pitch language
+
Language the deck opens in when the investor clicks the link
+
+
+ {(['de', 'en'] as const).map(l => ( + + ))} +
+
+
+ {/* Showcase toggle */}
diff --git a/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx b/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx index c67dc2d..d95245a 100644 --- a/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx @@ -1,24 +1,64 @@ 'use client' -import { useState, useMemo } from 'react' +import { useState, useMemo, useEffect, useRef } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' -import { ArrowLeft, Eye, Send } from 'lucide-react' -import { DEFAULT_MESSAGE, DEFAULT_CLOSING, getDefaultGreeting } from '@/lib/email-templates' +import { ArrowLeft, Eye, Send, Link2, Copy, Check, Mail, MailX, Layers } from 'lucide-react' +import { + DEFAULT_MESSAGE, DEFAULT_CLOSING, + DEFAULT_MESSAGE_EN, DEFAULT_CLOSING_EN, + getDefaultGreeting, +} from '@/lib/email-templates' + +type Lang = 'de' | 'en' + +interface Version { + id: string + name: string + status: string + created_at: string +} export default function NewInvestorPage() { const router = useRouter() const [email, setEmail] = useState('') const [name, setName] = useState('') const [company, setCompany] = useState('') + const [lang, setLang] = useState('de') + const [sendEmail, setSendEmail] = useState(true) + const [versionId, setVersionId] = useState('') + const [versions, setVersions] = useState([]) const [greeting, setGreeting] = useState('') const [message, setMessage] = useState(DEFAULT_MESSAGE) const [closing, setClosing] = useState(DEFAULT_CLOSING) + const [messageEdited, setMessageEdited] = useState(false) + const [closingEdited, setClosingEdited] = useState(false) const [error, setError] = useState('') const [submitting, setSubmitting] = useState(false) + const [createdLink, setCreatedLink] = useState<{ short: string; full: string } | null>(null) + const [copied, setCopied] = useState(false) + const prevLang = useRef('de') - const effectiveGreeting = greeting || getDefaultGreeting(name || null) const ttl = process.env.NEXT_PUBLIC_MAGIC_LINK_TTL_HOURS || '72' + const effectiveGreeting = greeting || getDefaultGreeting(name || null, lang) + + useEffect(() => { + fetch('/api/admin/versions') + .then(r => r.json()) + .then(d => setVersions((d.versions ?? []).filter((v: Version) => v.status === 'committed'))) + .catch(() => {}) + }, []) + + // When language changes, swap defaults unless the user has manually edited + useEffect(() => { + if (lang === prevLang.current) return + prevLang.current = lang + const defaults = lang === 'en' + ? { message: DEFAULT_MESSAGE_EN, closing: DEFAULT_CLOSING_EN } + : { message: DEFAULT_MESSAGE, closing: DEFAULT_CLOSING } + if (!messageEdited) setMessage(defaults.message) + if (!closingEdited) setClosing(defaults.closing) + }, [lang, messageEdited, closingEdited]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -32,14 +72,24 @@ export default function NewInvestorPage() { email, name, company, - greeting: effectiveGreeting, - message, - closing, + lang, + send_email: sendEmail, + version_id: versionId || null, + ...(sendEmail && { + greeting: effectiveGreeting, + message, + closing, + }), }), }) if (res.ok) { - router.push('/pitch-admin/investors') - router.refresh() + const data = await res.json() + if (!sendEmail) { + setCreatedLink({ short: data.short_url, full: data.magic_link_url }) + } else { + router.push('/pitch-admin/investors') + router.refresh() + } } else { const data = await res.json().catch(() => ({})) setError(data.error || 'Invite failed') @@ -51,8 +101,66 @@ export default function NewInvestorPage() { } } + async function copyLink() { + if (!createdLink) return + await navigator.clipboard.writeText(createdLink.short) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + const closingHtml = useMemo(() => closing.replace(/\n/g, '
'), [closing]) + // Success state: link was generated without email + if (createdLink) { + return ( +
+ + Back to investors + + +
+
+ +
+
+

Investor created

+

No email was sent. Copy the magic link below to use in your outreach.

+
+ +
+
+

Short Link (use this in emails)

+

{createdLink?.short}

+
+
+

Full Link

+

{createdLink?.full}

+
+
+ +
+ + + Back to investors + +
+
+
+ ) + } + return (
Investor einladen

- Der Investor erhaelt eine Email mit einem persoenlichen Magic Link (einmalig, verfaellt nach {ttl}h). + {sendEmail + ? `Der Investor erhält eine Email mit einem persönlichen Magic Link (einmalig, verfällt nach ${ttl}h).` + : `Erstellt den Investor-Datensatz und generiert einen Magic Link — keine Email wird gesendet.`}

@@ -120,51 +230,137 @@ export default function NewInvestorPage() { />
+ {/* Pitch Version */} +
+ + + {versions.length === 0 && ( +

Keine committed Versionen vorhanden

+ )} +
+
- {/* Greeting */} -
- - setGreeting(e.target.value)} - className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2.5 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40" - placeholder={getDefaultGreeting(name || null)} - /> -

Leer lassen fuer automatische Anrede basierend auf dem Namen

+ {/* Language toggle + Send email toggle */} +
+ {/* Pitch language */} +
+

Pitch-Sprache

+
+ + +
+

Standardsprache beim Öffnen des Decks

+
+ + {/* Send email toggle */} +
+

Email senden

+
+ + +
+

Nein = nur Link generieren

+
- {/* Message */} -
- -