diff --git a/src/app/api/dev/email-preview/route.ts b/src/app/api/dev/email-preview/route.ts new file mode 100644 index 0000000..d7bd95d --- /dev/null +++ b/src/app/api/dev/email-preview/route.ts @@ -0,0 +1,121 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + if (process.env.NODE_ENV === "production") { + return NextResponse.json({ error: "Not available" }, { status: 404 }); + } + + // Dynamically import to access the private emailShell via the public wrappers + // We'll just render inline for preview purposes + const { searchParams } = req.nextUrl; + const template = searchParams.get("template") || "verification"; + + const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://echoocr.com"; + + const BRAND = { + dark: "#2d2b28", + accent: "#3d3a36", + muted: "#8a8580", + bg: "#f7f5f2", + cardBg: "#ffffff", + border: "#e8e4df", + cta: "#2d2b28", + ctaText: "#fafaf8", + link: "#5c5650", + font: "'Quicksand', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", + }; + + const templates: Record = { + verification: { + preheader: "Confirm your email to start scanning cards with Echo.", + heading: "Verify your email address", + body: ` +

Welcome to Echo! Before you can start scanning response cards, we need to verify your email address.

+

Click the button below to confirm your email and get started.

+ `, + ctaLabel: "Verify Email Address", + ctaUrl: "#", + footer: "If you didn't create an Echo account, you can safely ignore this email. This link expires in 24 hours.", + }, + invitation: { + preheader: "Sarah invited you to join Grace Community Church on Echo.", + heading: "You're invited to Grace Community Church", + body: ` +

Sarah Johnson has invited you to join Grace Community Church on Echo — AI-powered response card scanning for churches and ministries.

+

Accept the invitation below to join the team and start collaborating.

+ `, + ctaLabel: "Accept Invitation", + ctaUrl: "#", + footer: "This invitation expires in 7 days. If you weren't expecting this email, you can safely ignore it.", + }, + }; + + const t = templates[template] || templates.verification; + + const html = ` + + + + + ${t.heading} + + + +
${t.preheader}
+ + + + +
+ + + + +
+ + + + + + +
+ + + Echo +
+
+
+ + + + +
+

${t.heading}

+
${t.body}
+ + + + +
+ + ${t.ctaLabel} + +
+

${t.footer}

+
+ + + + +
+

AI-powered response card scanning for churches

+

echoocr.com

+
+
+ +`; + + return new NextResponse(html, { + headers: { "Content-Type": "text/html" }, + }); +} diff --git a/src/lib/email-sender.ts b/src/lib/email-sender.ts index a9ba488..b3069ab 100644 --- a/src/lib/email-sender.ts +++ b/src/lib/email-sender.ts @@ -9,6 +9,147 @@ const DEFAULT_SENDER = { email: process.env.EMAIL_FROM_ADDRESS || "mars@noreply.stillwell.cloud", }; +const SITE_URL = + process.env.NEXT_PUBLIC_SITE_URL || "https://echoocr.com"; + +/* ------------------------------------------------------------------ */ +/* Shared template shell */ +/* ------------------------------------------------------------------ */ + +const BRAND = { + dark: "#2d2b28", + accent: "#3d3a36", + muted: "#8a8580", + bg: "#f7f5f2", + cardBg: "#ffffff", + border: "#e8e4df", + cta: "#2d2b28", + ctaText: "#fafaf8", + link: "#5c5650", + font: "'Quicksand', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", +}; + +function emailShell({ + preheader, + heading, + body, + ctaLabel, + ctaUrl, + footer, +}: { + preheader: string; + heading: string; + body: string; + ctaLabel: string; + ctaUrl: string; + footer: string; +}) { + return ` + + + + + + + ${heading} + + + + + +
${preheader}͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ 
+ + + + + +
+ + + + + + +
+ + + + + + +
+ + + Echo +
+
+
+ + + + + + +
+ + +

+ ${heading} +

+ + +
+ ${body} +
+ + + + + + +
+ + + ${ctaLabel} + + +
+ + +

+ ${footer} +

+ +
+ + + + + + +
+

+ AI-powered response card scanning for churches +

+

+ echoocr.com +

+
+ +
+ +`; +} + +/* ------------------------------------------------------------------ */ +/* Core send function */ +/* ------------------------------------------------------------------ */ + export async function sendEmail(to: string, subject: string, html: string) { await brevo.transactionalEmails.sendTransacEmail({ sender: DEFAULT_SENDER, @@ -18,9 +159,13 @@ export async function sendEmail(to: string, subject: string, html: string) { }); } +/* ------------------------------------------------------------------ */ +/* Verification email */ +/* ------------------------------------------------------------------ */ + export async function generateVerificationToken(email: string): Promise { const token = crypto.randomBytes(32).toString("hex"); - const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24h + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); await prisma.verificationToken.deleteMany({ where: { identifier: email }, @@ -41,24 +186,26 @@ export async function sendVerificationEmail(email: string, baseUrl: string) { const token = await generateVerificationToken(email); const verifyUrl = `${baseUrl}/api/auth/verify-email/confirm?token=${token}`; - const html = ` -
-

Verify your email address

-

Click the button below to verify your email for Echo OCR.

- - Verify Email - -

- If you didn't create an account, you can ignore this email. - This link expires in 24 hours. -

-
- `; + const html = emailShell({ + preheader: "Confirm your email to start scanning cards with Echo.", + heading: "Verify your email address", + body: ` +

Welcome to Echo! Before you can start scanning response cards, we need to verify your email address.

+

Click the button below to confirm your email and get started.

+ `, + ctaLabel: "Verify Email Address", + ctaUrl: verifyUrl, + footer: + "If you didn't create an Echo account, you can safely ignore this email. This link expires in 24 hours.", + }); - await sendEmail(email, "Verify your email — Echo OCR", html); + await sendEmail(email, "Verify your email — Echo", html); } +/* ------------------------------------------------------------------ */ +/* Invitation email */ +/* ------------------------------------------------------------------ */ + export async function sendInvitationEmail( email: string, baseUrl: string, @@ -68,19 +215,25 @@ export async function sendInvitationEmail( ) { const inviteUrl = `${baseUrl}/invite/${inviteToken}`; - const html = ` -
-

You're invited to ${orgName}

-

${inviterName ? `${inviterName} has` : "You've been"} invited you to join ${orgName} on Echo OCR.

- - Accept Invitation - -

- This invitation expires in 7 days. If you weren't expecting this, you can ignore it. -

-
- `; + const inviterLine = inviterName + ? `${inviterName} has invited you to join` + : "You've been invited to join"; - await sendEmail(email, `You're invited to ${orgName} — Echo OCR`, html); + const html = emailShell({ + preheader: `${inviterName || "Your team"} invited you to join ${orgName} on Echo.`, + heading: `You're invited to ${orgName}`, + body: ` +

${inviterLine} ${orgName} on Echo — AI-powered response card scanning for churches and ministries.

+

Accept the invitation below to join the team and start collaborating.

+ `, + ctaLabel: "Accept Invitation", + ctaUrl: inviteUrl, + footer: `This invitation expires in 7 days. If you weren't expecting this email, you can safely ignore it.`, + }); + + await sendEmail( + email, + `You're invited to ${orgName} — Echo`, + html + ); }