feat(pitch-deck): English email templates, investor language preference, link-only invite mode
Build pitch-deck / build-push-deploy (push) Successful in 1m55s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 35s
CI / test-bqas (push) Successful in 35s
Build pitch-deck / build-push-deploy (push) Successful in 1m55s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 35s
CI / test-bqas (push) Successful in 35s
- Add English email template variants (greeting, message, closing, subject, CTA copy) - Add `preferred_lang` column to `pitch_investors` — stored per investor, deck opens in that language by default - Invite form: DE/EN language toggle that switches email defaults and pitch language setting - Invite form: "Send email" toggle — when off, creates investor + returns magic link without sending email (for cold outreach attachment) - `app/page.tsx`: initializes pitch language from investor's `preferred_lang` before first render (no flash) - Migration 007 added to `/api/admin/migrate` route for production rollout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,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 } = body
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
return NextResponse.json({ error: 'Email required' }, { status: 400 })
|
||||
@@ -25,17 +25,20 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const normalizedEmail = email.toLowerCase().trim()
|
||||
|
||||
const normalizedLang = lang === 'en' ? 'en' : 'de'
|
||||
|
||||
// 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)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
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,
|
||||
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],
|
||||
)
|
||||
|
||||
const investor = rows[0]
|
||||
@@ -54,12 +57,21 @@ export async function POST(request: NextRequest) {
|
||||
const baseUrl = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.ai'
|
||||
const magicLinkUrl = `${baseUrl}/auth/verify?token=${token}`
|
||||
|
||||
await sendMagicLinkEmail(normalizedEmail, name || null, magicLinkUrl, greeting, message, closing)
|
||||
if (send_email) {
|
||||
await sendMagicLinkEmail(normalizedEmail, name || null, magicLinkUrl, 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,
|
||||
},
|
||||
request,
|
||||
investor.id,
|
||||
)
|
||||
@@ -69,5 +81,6 @@ export async function POST(request: NextRequest) {
|
||||
investor_id: investor.id,
|
||||
email: normalizedEmail,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
magic_link_url: magicLinkUrl,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -160,6 +160,8 @@ 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'`,
|
||||
]
|
||||
|
||||
for (const sql of statements) {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
+13
-3
@@ -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<Language>('de')
|
||||
const { investor, loading, logout } = useAuth()
|
||||
const [lang, setLang] = useState<Language>('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 (
|
||||
<div className="h-screen flex items-center justify-center">
|
||||
<div className="w-12 h-12 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
'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 } from 'lucide-react'
|
||||
import {
|
||||
DEFAULT_MESSAGE, DEFAULT_CLOSING,
|
||||
DEFAULT_MESSAGE_EN, DEFAULT_CLOSING_EN,
|
||||
getDefaultGreeting,
|
||||
} from '@/lib/email-templates'
|
||||
|
||||
type Lang = 'de' | 'en'
|
||||
|
||||
export default function NewInvestorPage() {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [company, setCompany] = useState('')
|
||||
const [lang, setLang] = useState<Lang>('de')
|
||||
const [sendEmail, setSendEmail] = useState(true)
|
||||
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<string | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const prevLang = useRef<Lang>('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)
|
||||
|
||||
// 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 +56,23 @@ export default function NewInvestorPage() {
|
||||
email,
|
||||
name,
|
||||
company,
|
||||
greeting: effectiveGreeting,
|
||||
message,
|
||||
closing,
|
||||
lang,
|
||||
send_email: sendEmail,
|
||||
...(sendEmail && {
|
||||
greeting: effectiveGreeting,
|
||||
message,
|
||||
closing,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
router.push('/pitch-admin/investors')
|
||||
router.refresh()
|
||||
const data = await res.json()
|
||||
if (!sendEmail) {
|
||||
setCreatedLink(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 +84,60 @@ export default function NewInvestorPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
if (!createdLink) return
|
||||
await navigator.clipboard.writeText(createdLink)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const closingHtml = useMemo(() => closing.replace(/\n/g, '<br>'), [closing])
|
||||
|
||||
// Success state: link was generated without email
|
||||
if (createdLink) {
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<Link
|
||||
href="/pitch-admin/investors"
|
||||
className="inline-flex items-center gap-2 text-sm text-white/50 hover:text-white/80 mb-6"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Back to investors
|
||||
</Link>
|
||||
|
||||
<div className="bg-white/[0.04] border border-white/[0.08] rounded-2xl p-8 text-center space-y-6">
|
||||
<div className="w-14 h-14 rounded-full bg-green-500/15 border border-green-500/30 flex items-center justify-center mx-auto">
|
||||
<Link2 className="w-7 h-7 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white mb-1">Investor created</h2>
|
||||
<p className="text-sm text-white/50">No email was sent. Copy the magic link below to use in your outreach.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-black/40 border border-white/10 rounded-xl p-4 text-left">
|
||||
<p className="text-[10px] font-semibold text-white/40 uppercase tracking-wider mb-2">Magic Link</p>
|
||||
<p className="text-xs text-indigo-300 break-all leading-relaxed">{createdLink}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={copyLink}
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white text-sm font-medium px-5 py-2.5 rounded-lg shadow-lg shadow-indigo-500/20"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? 'Copied!' : 'Copy link'}
|
||||
</button>
|
||||
<Link
|
||||
href="/pitch-admin/investors"
|
||||
className="text-sm text-white/60 hover:text-white px-4 py-2"
|
||||
>
|
||||
Back to investors
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl">
|
||||
<Link
|
||||
@@ -64,7 +149,9 @@ export default function NewInvestorPage() {
|
||||
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">Investor einladen</h1>
|
||||
<p className="text-sm text-white/50 mb-6">
|
||||
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.`}
|
||||
</p>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
@@ -122,49 +209,114 @@ export default function NewInvestorPage() {
|
||||
|
||||
<hr className="border-white/[0.06]" />
|
||||
|
||||
{/* Greeting */}
|
||||
<div>
|
||||
<label htmlFor="greeting" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
|
||||
Anrede
|
||||
</label>
|
||||
<input
|
||||
id="greeting"
|
||||
type="text"
|
||||
value={greeting}
|
||||
onChange={(e) => 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)}
|
||||
/>
|
||||
<p className="text-[10px] text-white/25 mt-1">Leer lassen fuer automatische Anrede basierend auf dem Namen</p>
|
||||
{/* Language toggle + Send email toggle */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Pitch language */}
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium text-white/60 uppercase tracking-wider mb-2">Pitch-Sprache</p>
|
||||
<div className="flex rounded-lg border border-white/10 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang('de')}
|
||||
className={`flex-1 py-2 text-sm font-medium transition-colors ${lang === 'de' ? 'bg-indigo-600 text-white' : 'bg-black/20 text-white/50 hover:text-white/80'}`}
|
||||
>
|
||||
🇩🇪 DE
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang('en')}
|
||||
className={`flex-1 py-2 text-sm font-medium transition-colors ${lang === 'en' ? 'bg-indigo-600 text-white' : 'bg-black/20 text-white/50 hover:text-white/80'}`}
|
||||
>
|
||||
🇬🇧 EN
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/25 mt-1">Standardsprache beim Öffnen des Decks</p>
|
||||
</div>
|
||||
|
||||
{/* Send email toggle */}
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium text-white/60 uppercase tracking-wider mb-2">Email senden</p>
|
||||
<div className="flex rounded-lg border border-white/10 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSendEmail(true)}
|
||||
className={`flex-1 py-2 text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${sendEmail ? 'bg-indigo-600 text-white' : 'bg-black/20 text-white/50 hover:text-white/80'}`}
|
||||
>
|
||||
<Mail className="w-3.5 h-3.5" /> Ja
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSendEmail(false)}
|
||||
className={`flex-1 py-2 text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${!sendEmail ? 'bg-amber-600 text-white' : 'bg-black/20 text-white/50 hover:text-white/80'}`}
|
||||
>
|
||||
<MailX className="w-3.5 h-3.5" /> Nein
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/25 mt-1">Nein = nur Link generieren</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
|
||||
Nachricht
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 resize-y"
|
||||
/>
|
||||
</div>
|
||||
{/* Email customization — only shown when sendEmail=true */}
|
||||
{sendEmail && (
|
||||
<>
|
||||
<hr className="border-white/[0.06]" />
|
||||
|
||||
{/* Closing */}
|
||||
<div>
|
||||
<label htmlFor="closing" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
|
||||
Grussformel
|
||||
</label>
|
||||
<textarea
|
||||
id="closing"
|
||||
value={closing}
|
||||
onChange={(e) => setClosing(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 resize-y"
|
||||
/>
|
||||
</div>
|
||||
{/* Greeting */}
|
||||
<div>
|
||||
<label htmlFor="greeting" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
|
||||
Anrede / Greeting
|
||||
</label>
|
||||
<input
|
||||
id="greeting"
|
||||
type="text"
|
||||
value={greeting}
|
||||
onChange={(e) => 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, lang)}
|
||||
/>
|
||||
<p className="text-[10px] text-white/25 mt-1">
|
||||
{lang === 'de' ? 'Leer lassen für automatische Anrede basierend auf dem Namen' : 'Leave empty for automatic greeting based on name'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
|
||||
Nachricht / Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={message}
|
||||
onChange={(e) => { setMessage(e.target.value); setMessageEdited(true) }}
|
||||
rows={4}
|
||||
className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Closing */}
|
||||
<div>
|
||||
<label htmlFor="closing" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
|
||||
Grussformel / Closing
|
||||
</label>
|
||||
<textarea
|
||||
id="closing"
|
||||
value={closing}
|
||||
onChange={(e) => { setClosing(e.target.value); setClosingEdited(true) }}
|
||||
rows={3}
|
||||
className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 resize-y"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!sendEmail && (
|
||||
<div className="flex items-start gap-3 bg-amber-500/[0.08] border border-amber-500/20 rounded-xl p-4">
|
||||
<MailX className="w-4 h-4 text-amber-400 mt-0.5 shrink-0" />
|
||||
<p className="text-xs text-amber-300/80 leading-relaxed">
|
||||
Kein Email wird gesendet. Nach dem Erstellen erhältst du den Magic Link zum manuellen Einfügen in deine Outreach-Email.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-rose-300 bg-rose-500/10 border border-rose-500/20 rounded-lg px-3 py-2">
|
||||
@@ -184,80 +336,108 @@ export default function NewInvestorPage() {
|
||||
disabled={submitting}
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white text-sm font-medium px-5 py-2.5 rounded-lg disabled:opacity-50 shadow-lg shadow-indigo-500/20"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
{submitting ? 'Wird gesendet...' : 'Einladung senden'}
|
||||
{sendEmail ? <Send className="w-4 h-4" /> : <Link2 className="w-4 h-4" />}
|
||||
{submitting
|
||||
? (sendEmail ? 'Wird gesendet...' : 'Wird erstellt...')
|
||||
: (sendEmail ? 'Einladung senden' : 'Erstellen & Link generieren')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Right: Email Preview */}
|
||||
<div className="self-start">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Eye className="w-4 h-4 text-white/40" />
|
||||
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider">Email-Vorschau</h3>
|
||||
</div>
|
||||
<div className="bg-[#0a0a1a] border border-white/[0.08] rounded-2xl p-4 overflow-y-auto max-h-[80vh]">
|
||||
{/* Email Card */}
|
||||
<div className="bg-[#111127] border border-indigo-500/20 rounded-xl overflow-hidden">
|
||||
{/* Right: Email Preview (only when sendEmail=true) */}
|
||||
{sendEmail ? (
|
||||
<div className="self-start">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Eye className="w-4 h-4 text-white/40" />
|
||||
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider">Email-Vorschau</h3>
|
||||
</div>
|
||||
<div className="bg-[#0a0a1a] border border-white/[0.08] rounded-2xl p-4 overflow-y-auto max-h-[80vh]">
|
||||
<div className="bg-[#111127] border border-indigo-500/20 rounded-xl overflow-hidden">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-6 pb-3">
|
||||
<p className="text-lg font-semibold text-[#e0e0ff]">BreakPilot ComplAI</p>
|
||||
<p className="text-xs text-white/40 mt-1">Investor Pitch Deck</p>
|
||||
</div>
|
||||
<div className="px-6 pt-6 pb-3">
|
||||
<p className="text-lg font-semibold text-[#e0e0ff]">BreakPilot ComplAI</p>
|
||||
<p className="text-xs text-white/40 mt-1">Investor Pitch Deck</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-3">
|
||||
<p className="text-sm text-white/80 mb-3">{effectiveGreeting},</p>
|
||||
<p className="text-sm text-white/70 leading-relaxed mb-4">{message}</p>
|
||||
<div className="px-6 py-3">
|
||||
<p className="text-sm text-white/80 mb-3">{effectiveGreeting},</p>
|
||||
<p className="text-sm text-white/70 leading-relaxed mb-4" dangerouslySetInnerHTML={{ __html: message }} />
|
||||
|
||||
{/* Magic Link Box */}
|
||||
<div className="bg-indigo-500/[0.08] border border-indigo-500/[0.15] rounded-lg p-3 mb-4">
|
||||
<p className="text-[10px] font-semibold text-indigo-400/80 uppercase tracking-wider mb-1">
|
||||
Ihr persoenlicher Zugangslink
|
||||
<div className="bg-indigo-500/[0.08] border border-indigo-500/[0.15] rounded-lg p-3 mb-4">
|
||||
<p className="text-[10px] font-semibold text-indigo-400/80 uppercase tracking-wider mb-1">
|
||||
{lang === 'en' ? 'Your Personal Access Link' : 'Ihr persönlicher Zugangslink'}
|
||||
</p>
|
||||
<p className="text-xs text-white/50 leading-relaxed">
|
||||
{lang === 'en'
|
||||
? `The link below is single-use and expires after ${ttl} hours. It grants you exclusive access to our interactive pitch deck — including an AI assistant for your questions.`
|
||||
: `Der untenstehende Link ist einmalig und verfällt nach ${ttl} Stunden. Er gewährt Ihnen exklusiven Zugang zu unserem interaktiven Pitch Deck — inklusive KI-Assistent für Ihre Fragen.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-3">
|
||||
<span className="inline-block bg-gradient-to-r from-indigo-500 to-purple-600 text-white text-sm font-semibold px-8 py-2.5 rounded-lg">
|
||||
{lang === 'en' ? 'Open Pitch Deck' : 'Pitch Deck öffnen'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-white/25 mb-4 break-all">
|
||||
{lang === 'en' ? "If the button doesn't work: " : 'Falls der Button nicht funktioniert: '}
|
||||
https://pitch.breakpilot.ai/auth/verify?token=...
|
||||
</p>
|
||||
<p className="text-xs text-white/50 leading-relaxed">
|
||||
Der untenstehende Link ist einmalig und verfaellt nach {ttl} Stunden. Er gewaehrt Ihnen exklusiven Zugang zu unserem interaktiven Pitch Deck — inklusive KI-Assistent fuer Ihre Fragen.
|
||||
|
||||
<p className="text-sm text-white/70 leading-relaxed" dangerouslySetInnerHTML={{ __html: closingHtml }} />
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-white/[0.05]">
|
||||
<p className="text-[9px] font-semibold text-white/30 uppercase tracking-wider mb-1">
|
||||
Vertraulichkeit & Haftungsausschluss
|
||||
</p>
|
||||
<p className="text-[9px] text-white/[0.18] leading-relaxed mb-2">
|
||||
Dieses Pitch Deck ist vertraulich und wurde ausschließlich für den namentlich eingeladenen Empfänger erstellt. Keine Weitergabe. 3-Jahres-Pflicht. Kein Angebot. Planzahlen ohne Garantie. Totalverlustrisiko. Deutsches Recht, Konstanz.
|
||||
</p>
|
||||
<p className="text-[9px] font-semibold text-white/20 uppercase tracking-wider mb-1">
|
||||
Confidentiality & Disclaimer
|
||||
</p>
|
||||
<p className="text-[9px] text-white/[0.13] leading-relaxed">
|
||||
Confidential. Purpose-limited. 3-year obligation. Not an offer. Projections only. Risk of total loss. German law, Konstanz jurisdiction.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Button Preview */}
|
||||
<div className="text-center mb-3">
|
||||
<span className="inline-block bg-gradient-to-r from-indigo-500 to-purple-600 text-white text-sm font-semibold px-8 py-2.5 rounded-lg">
|
||||
Pitch Deck oeffnen
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-white/25 mb-4 break-all">
|
||||
Falls der Button nicht funktioniert: https://pitch.breakpilot.ai/auth/verify?token=...
|
||||
</p>
|
||||
|
||||
{/* Closing */}
|
||||
<p className="text-sm text-white/70 leading-relaxed" dangerouslySetInnerHTML={{ __html: closingHtml }} />
|
||||
</div>
|
||||
|
||||
{/* Legal Footer */}
|
||||
<div className="px-6 py-4 border-t border-white/[0.05]">
|
||||
<p className="text-[9px] font-semibold text-white/30 uppercase tracking-wider mb-1">
|
||||
Vertraulichkeit & Haftungsausschluss
|
||||
</p>
|
||||
<p className="text-[9px] text-white/[0.18] leading-relaxed mb-2">
|
||||
Dieses Pitch Deck ist vertraulich und wurde ausschliesslich fuer den namentlich eingeladenen Empfaenger erstellt. Durch das Oeffnen des Links erklaert sich der Empfaenger einverstanden: (a) Vertrauliche Behandlung, keine Weitergabe an Dritte. (b) Nutzung ausschliesslich zur Bewertung einer Beteiligung. (c) Vertraulichkeitspflicht fuer 3 Jahre.
|
||||
</p>
|
||||
<p className="text-[9px] text-white/[0.18] leading-relaxed mb-3">
|
||||
Kein Angebot, kein Prospekt. Planzahlen ohne Garantie. Totalverlustrisiko. Deutsches Recht, Gerichtsstand Konstanz.
|
||||
</p>
|
||||
<p className="text-[9px] font-semibold text-white/20 uppercase tracking-wider mb-1">
|
||||
Confidentiality & Disclaimer
|
||||
</p>
|
||||
<p className="text-[9px] text-white/[0.13] leading-relaxed">
|
||||
Confidential. Purpose-limited. 3-year obligation. Not an offer. Projections only. Risk of total loss. German law, Konstanz jurisdiction.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="self-start">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Link2 className="w-4 h-4 text-white/40" />
|
||||
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider">Link-Modus</h3>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-6 space-y-4">
|
||||
<p className="text-sm text-white/60 leading-relaxed">
|
||||
Im Link-Modus wird kein Email gesendet. Stattdessen:
|
||||
</p>
|
||||
<ol className="space-y-3 text-sm text-white/50">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="w-5 h-5 rounded-full bg-indigo-500/20 text-indigo-400 text-xs flex items-center justify-center shrink-0 mt-0.5">1</span>
|
||||
Investor-Datensatz wird erstellt (Email, Name, Unternehmen)
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="w-5 h-5 rounded-full bg-indigo-500/20 text-indigo-400 text-xs flex items-center justify-center shrink-0 mt-0.5">2</span>
|
||||
Persönlicher Magic Link wird generiert (einmalig, {ttl}h gültig)
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="w-5 h-5 rounded-full bg-indigo-500/20 text-indigo-400 text-xs flex items-center justify-center shrink-0 mt-0.5">3</span>
|
||||
Link erscheint hier zum Kopieren — füge ihn in deine Cold-Email ein
|
||||
</li>
|
||||
</ol>
|
||||
<div className="mt-4 bg-amber-500/[0.06] border border-amber-500/15 rounded-lg p-3">
|
||||
<p className="text-xs text-amber-300/70">
|
||||
Pitch-Sprache: <strong>{lang === 'en' ? 'Englisch' : 'Deutsch'}</strong> — das Deck öffnet sich beim ersten Klick in dieser Sprache.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user