From aed428312faffd2cde8803cc696c5cc782ee3524 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 16 Apr 2026 08:34:23 +0200 Subject: [PATCH] feat(pitch-deck): bilingual email template + invite page with live preview Email template (email.ts): - Bilingual: German body + DE/EN legal footer - Customizable greeting, message body, and closing - Magic Link explanation box (hardcoded) - Confidentiality & Disclaimer footer (hardcoded, bilingual) Invite page (investors/new): - Name is now required, Company is optional - Editable fields: greeting, message, closing (with defaults) - Live email preview panel (right side) - Shows full email content before sending - German UI labels API (invite/route.ts): - Passes greeting, message, closing to email function Co-Authored-By: Claude Opus 4.6 (1M context) --- pitch-deck/app/api/admin/invite/route.ts | 4 +- .../(authed)/investors/new/page.tsx | 283 +++++++++++++----- pitch-deck/lib/email.ts | 105 +++++-- 3 files changed, 297 insertions(+), 95 deletions(-) diff --git a/pitch-deck/app/api/admin/invite/route.ts b/pitch-deck/app/api/admin/invite/route.ts index 602026e..c5aefe1 100644 --- a/pitch-deck/app/api/admin/invite/route.ts +++ b/pitch-deck/app/api/admin/invite/route.ts @@ -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 } = body + const { email, name, company, greeting, message, closing } = body if (!email || typeof email !== 'string') { return NextResponse.json({ error: 'Email required' }, { status: 400 }) @@ -54,7 +54,7 @@ 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) + await sendMagicLinkEmail(normalizedEmail, name || null, magicLinkUrl, greeting, message, closing) await logAdminAudit( adminId, 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 f7cd285..be74368 100644 --- a/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx @@ -1,18 +1,25 @@ 'use client' -import { useState } from 'react' +import { useState, useMemo } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' -import { ArrowLeft } from 'lucide-react' +import { ArrowLeft, Eye, Send } from 'lucide-react' +import { DEFAULT_MESSAGE, DEFAULT_CLOSING, getDefaultGreeting } from '@/lib/email' export default function NewInvestorPage() { const router = useRouter() const [email, setEmail] = useState('') const [name, setName] = useState('') const [company, setCompany] = useState('') + const [greeting, setGreeting] = useState('') + const [message, setMessage] = useState(DEFAULT_MESSAGE) + const [closing, setClosing] = useState(DEFAULT_CLOSING) const [error, setError] = useState('') const [submitting, setSubmitting] = useState(false) + const effectiveGreeting = greeting || getDefaultGreeting(name || null) + const ttl = process.env.NEXT_PUBLIC_MAGIC_LINK_TTL_HOURS || '72' + async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError('') @@ -21,7 +28,14 @@ export default function NewInvestorPage() { const res = await fetch('/api/admin/invite', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, name, company }), + body: JSON.stringify({ + email, + name, + company, + greeting: effectiveGreeting, + message, + closing, + }), }) if (res.ok) { router.push('/pitch-admin/investors') @@ -37,8 +51,10 @@ export default function NewInvestorPage() { } } + const closingHtml = useMemo(() => closing.replace(/\n/g, '
'), [closing]) + return ( -
+
Back to investors -

Invite Investor

+

Investor einladen

- A magic link will be emailed. Single-use, expires in {process.env.NEXT_PUBLIC_MAGIC_LINK_TTL_HOURS || '72'}h. + Der Investor erhaelt eine Email mit einem persoenlichen Magic Link (einmalig, verfaellt nach {ttl}h).

-
-
- - setEmail(e.target.value)} - required - 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="jane@vc.com" - /> -
- -
- - setName(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="Jane Doe" - /> -
- -
- - setCompany(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="Acme Ventures" - /> -
- - {error && ( -
- {error} +
+ {/* Left: Form */} + + {/* Email */} +
+ + setEmail(e.target.value)} + required + 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="investor@example.com" + />
- )} -
- - Cancel - - + {/* Name */} +
+ + setName(e.target.value)} + required + 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="Dr. Max Mustermann" + /> +
+ + {/* Company (optional) */} +
+ + setCompany(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="Muster Ventures GmbH" + /> +
+ +
+ + {/* 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

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