Files
breakpilot-core/pitch-deck/lib/email.ts
Benjamin Admin aed428312f
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 1m6s
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 48s
CI / test-python-voice (push) Successful in 39s
CI / test-bqas (push) Successful in 40s
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) <noreply@anthropic.com>
2026-04-16 08:34:23 +02:00

167 lines
9.0 KiB
TypeScript

import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_PORT === '465',
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
},
})
const fromName = process.env.SMTP_FROM_NAME || 'BreakPilot'
const fromAddr = process.env.SMTP_FROM_ADDR || 'noreply@breakpilot.ai'
export const DEFAULT_GREETING = 'Sehr geehrte Damen und Herren'
export const DEFAULT_MESSAGE =
'wir freuen uns, Ihnen einen exklusiven Zugang zu unserem interaktiven Investor Pitch Deck zu gewähren. Die Präsentation enthält alle relevanten Informationen zu unserem Unternehmen, unserem Produkt und unserer Finanzierungsstrategie.'
export const DEFAULT_CLOSING =
'Mit freundlichen Grüßen,\nBenjamin Boenisch & Sharang Parnerkar\nGründer — BreakPilot ComplAI'
export function getDefaultGreeting(name: string | null): string {
return name ? `Sehr geehrte(r) ${name}` : DEFAULT_GREETING
}
export async function sendMagicLinkEmail(
to: string,
investorName: string | null,
magicLinkUrl: string,
greeting?: string,
message?: string,
closing?: string,
): Promise<void> {
const effectiveGreeting = greeting || getDefaultGreeting(investorName)
const effectiveMessage = message || DEFAULT_MESSAGE
const effectiveClosing = closing || DEFAULT_CLOSING
const closingHtml = effectiveClosing.replace(/\n/g, '<br>')
const ttl = process.env.MAGIC_LINK_TTL_HOURS || '72'
await transporter.sendMail({
from: `"${fromName}" <${fromAddr}>`,
to,
subject: 'BreakPilot ComplAI — Ihr persönlicher Pitch-Deck-Zugang',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background:#0a0a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0a0a1a;padding:40px 20px;">
<tr>
<td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#111127;border-radius:12px;border:1px solid rgba(99,102,241,0.2);">
<!-- Header -->
<tr>
<td style="padding:40px 40px 20px;">
<h1 style="margin:0;font-size:24px;color:#e0e0ff;font-weight:600;">
BreakPilot ComplAI
</h1>
<p style="margin:8px 0 0;font-size:13px;color:rgba(255,255,255,0.4);">
Investor Pitch Deck
</p>
</td>
</tr>
<!-- Greeting + Message -->
<tr>
<td style="padding:20px 40px 0;">
<p style="margin:0 0 16px;font-size:16px;color:rgba(255,255,255,0.8);line-height:1.6;">
${effectiveGreeting},
</p>
<p style="margin:0 0 24px;font-size:15px;color:rgba(255,255,255,0.7);line-height:1.7;">
${effectiveMessage}
</p>
</td>
</tr>
<!-- Magic Link Explanation -->
<tr>
<td style="padding:0 40px 20px;">
<div style="background:rgba(99,102,241,0.08);border:1px solid rgba(99,102,241,0.15);border-radius:8px;padding:16px;">
<p style="margin:0 0 4px;font-size:11px;font-weight:600;color:rgba(99,102,241,0.8);text-transform:uppercase;letter-spacing:0.5px;">
Ihr persoenlicher Zugangslink
</p>
<p style="margin:0;font-size:13px;color:rgba(255,255,255,0.5);line-height:1.5;">
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>
</div>
</td>
</tr>
<!-- Button -->
<tr>
<td style="padding:0 40px 16px;" align="center">
<table cellpadding="0" cellspacing="0">
<tr>
<td style="background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:8px;padding:14px 40px;">
<a href="${magicLinkUrl}" style="color:#ffffff;font-size:16px;font-weight:600;text-decoration:none;display:inline-block;">
Pitch Deck oeffnen
</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Raw link -->
<tr>
<td style="padding:0 40px 24px;">
<p style="margin:0;font-size:11px;color:rgba(255,255,255,0.25);line-height:1.5;word-break:break-all;">
Falls der Button nicht funktioniert: ${magicLinkUrl}
</p>
</td>
</tr>
<!-- Closing -->
<tr>
<td style="padding:0 40px 24px;">
<p style="margin:0;font-size:15px;color:rgba(255,255,255,0.7);line-height:1.7;">
${closingHtml}
</p>
</td>
</tr>
<!-- Legal Footer DE -->
<tr>
<td style="padding:20px 40px 12px;border-top:1px solid rgba(255,255,255,0.05);">
<p style="margin:0 0 8px;font-size:10px;font-weight:600;color:rgba(255,255,255,0.3);text-transform:uppercase;letter-spacing:0.5px;">
Vertraulichkeit &amp; Haftungsausschluss
</p>
<p style="margin:0 0 6px;font-size:10px;color:rgba(255,255,255,0.18);line-height:1.5;">
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) Der Inhalt ist vertraulich zu behandeln und darf nicht an Dritte weitergegeben, kopiert oder zugaenglich gemacht werden. Ausgenommen sind Berater (Rechtsanwaelte, Steuerberater), die berufsrechtlich zur Verschwiegenheit verpflichtet sind. (b) Die Informationen duerfen ausschliesslich zur Bewertung einer moeglichen Beteiligung verwendet werden. (c) Diese Vertraulichkeitsverpflichtung gilt fuer drei (3) Jahre ab Uebermittlung, unabhaengig davon, ob eine Beteiligung zustande kommt.
</p>
<p style="margin:0 0 12px;font-size:10px;color:rgba(255,255,255,0.18);line-height:1.5;">
Dieses Dokument stellt weder ein Angebot zum Verkauf noch eine Aufforderung zur Abgabe eines Angebots zum Erwerb von Wertpapieren dar. Es handelt sich nicht um einen Wertpapierprospekt. Alle Finanzangaben sind Planzahlen und stellen keine Garantie fuer kuenftige Ergebnisse dar. Eine Beteiligung an einem jungen Unternehmen ist mit erheblichen Risiken verbunden, einschliesslich des Risikos eines Totalverlusts. Es gilt deutsches Recht. Gerichtsstand ist Konstanz, Deutschland.
</p>
</td>
</tr>
<!-- Legal Footer EN -->
<tr>
<td style="padding:0 40px 40px;">
<p style="margin:0 0 8px;font-size:10px;font-weight:600;color:rgba(255,255,255,0.2);text-transform:uppercase;letter-spacing:0.5px;">
Confidentiality &amp; Disclaimer
</p>
<p style="margin:0 0 6px;font-size:10px;color:rgba(255,255,255,0.13);line-height:1.5;">
This pitch deck is confidential and has been prepared exclusively for the personally invited recipient. By opening this link, the recipient agrees: (a) The content must be treated confidentially and may not be disclosed, copied or made accessible to third parties. Excluded are advisors (lawyers, tax advisors) professionally bound to secrecy. (b) The information may only be used for evaluating a possible participation. (c) This confidentiality obligation applies for three (3) years from transmission, regardless of whether a participation materializes.
</p>
<p style="margin:0;font-size:10px;color:rgba(255,255,255,0.13);line-height:1.5;">
This document constitutes neither an offer to sell nor a solicitation of an offer to acquire securities. It is not a securities prospectus. All financial figures are projections and do not constitute a guarantee of future results. An investment in a young company involves significant risks, including the risk of total loss. German law applies. Place of jurisdiction is Konstanz, Germany.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`,
})
}