Skip to content

Commit 01aa311

Browse files
committed
improvement(emails): preview every template in a single gallery page
1 parent c57020d commit 01aa311

1 file changed

Lines changed: 115 additions & 28 deletions

File tree

apps/sim/app/api/emails/preview/route.ts

Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import type { NextRequest } from 'next/server'
22
import { NextResponse } from 'next/server'
33
import {
4+
renderAbandonedCheckoutEmail,
45
renderBatchInvitationEmail,
56
renderCreditPurchaseEmail,
7+
renderCreditsExhaustedEmail,
68
renderEnterpriseSubscriptionEmail,
9+
renderExistingAccountEmail,
710
renderFreeTierUpgradeEmail,
811
renderHelpConfirmationEmail,
912
renderInvitationEmail,
13+
renderLimitThresholdEmail,
14+
renderOnboardingFollowupEmail,
1015
renderOTPEmail,
1116
renderPasswordResetEmail,
1217
renderPaymentFailedEmail,
@@ -15,6 +20,7 @@ import {
1520
renderUsageLimitReachedEmail,
1621
renderUsageThresholdEmail,
1722
renderWelcomeEmail,
23+
renderWorkspaceAddedEmail,
1824
renderWorkspaceInvitationEmail,
1925
} from '@/components/emails'
2026
import { emailPreviewQuerySchema } from '@/lib/api/contracts/common'
@@ -24,11 +30,16 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2430
const emailTemplates = {
2531
// Auth emails
2632
otp: () => renderOTPEmail('123456', 'user@example.com', 'email-verification'),
33+
'otp-sign-in': () => renderOTPEmail('123456', 'user@example.com', 'sign-in'),
2734
'reset-password': () => renderPasswordResetEmail('John', 'https://sim.ai/reset?token=abc123'),
35+
'existing-account': () => renderExistingAccountEmail('John'),
2836
welcome: () => renderWelcomeEmail('John'),
37+
'onboarding-followup': () => renderOnboardingFollowupEmail('John'),
2938

3039
// Invitation emails
3140
invitation: () => renderInvitationEmail('Jane Doe', 'Acme Corp', 'https://sim.ai/invite/abc123'),
41+
'workspace-added': () =>
42+
renderWorkspaceAddedEmail('Jane Doe', 'Engineering', 'https://sim.ai/workspace/ws_123'),
3243
'batch-invitation': () =>
3344
renderBatchInvitationEmail(
3445
'Jane Doe',
@@ -87,6 +98,43 @@ const emailTemplates = {
8798
amount: 50,
8899
newBalance: 75,
89100
}),
101+
'credits-exhausted': () =>
102+
renderCreditsExhaustedEmail({
103+
userName: 'John',
104+
limit: 10,
105+
upgradeLink: 'https://sim.ai/settings/billing',
106+
}),
107+
'abandoned-checkout': () => renderAbandonedCheckoutEmail('John'),
108+
'limit-threshold-storage-warning': () =>
109+
renderLimitThresholdEmail({
110+
kind: 'warning',
111+
reason: 'storage',
112+
userName: 'John',
113+
usageLabel: '4.2 GB',
114+
limitLabel: '5 GB',
115+
percentUsed: 84,
116+
upgradeLink: 'https://sim.ai/settings/billing',
117+
}),
118+
'limit-threshold-tables-reached': () =>
119+
renderLimitThresholdEmail({
120+
kind: 'reached',
121+
reason: 'tables',
122+
userName: 'John',
123+
usageLabel: '50,000 rows',
124+
limitLabel: '50,000 rows',
125+
percentUsed: 100,
126+
upgradeLink: 'https://sim.ai/settings/billing',
127+
}),
128+
'limit-threshold-seats-reached': () =>
129+
renderLimitThresholdEmail({
130+
kind: 'reached',
131+
reason: 'seats',
132+
userName: 'John',
133+
usageLabel: '10 seats',
134+
limitLabel: '10 seats',
135+
percentUsed: 100,
136+
upgradeLink: 'https://sim.ai/settings/billing',
137+
}),
90138
'payment-failed': () =>
91139
renderPaymentFailedEmail({
92140
userName: 'John',
@@ -138,6 +186,40 @@ function isEmailTemplate(template: string): template is EmailTemplate {
138186
return template in emailTemplates
139187
}
140188

189+
const CATEGORIZED = {
190+
Auth: ['otp', 'otp-sign-in', 'reset-password', 'existing-account', 'welcome'],
191+
Invitations: ['invitation', 'batch-invitation', 'workspace-invitation', 'workspace-added'],
192+
Support: ['help-confirmation'],
193+
Billing: [
194+
'usage-threshold',
195+
'usage-limit-reached',
196+
'usage-limit-reached-org',
197+
'free-tier-upgrade',
198+
'credits-exhausted',
199+
'limit-threshold-storage-warning',
200+
'limit-threshold-tables-reached',
201+
'limit-threshold-seats-reached',
202+
'payment-failed',
203+
'credit-purchase',
204+
'plan-welcome-pro',
205+
'plan-welcome-team',
206+
'enterprise-subscription',
207+
],
208+
Notifications: ['schedule-disabled', 'schedule-disabled-auth'],
209+
'Plain (unbranded)': ['onboarding-followup', 'abandoned-checkout'],
210+
} satisfies Record<string, EmailTemplate[]>
211+
212+
/**
213+
* Category map for the gallery, with any template missing from {@link CATEGORIZED}
214+
* appended rather than dropped — so a newly registered template always shows up
215+
* even if nobody remembers to file it.
216+
*/
217+
const PREVIEW_CATEGORIES: Record<string, EmailTemplate[]> = (() => {
218+
const filed = new Set<string>(Object.values(CATEGORIZED).flat())
219+
const unfiled = (Object.keys(emailTemplates) as EmailTemplate[]).filter((t) => !filed.has(t))
220+
return unfiled.length > 0 ? { ...CATEGORIZED, Uncategorized: unfiled } : CATEGORIZED
221+
})()
222+
141223
export const GET = withRouteHandler(async (request: NextRequest) => {
142224
const { searchParams } = new URL(request.url)
143225
const queryValidation = emailPreviewQuerySchema.safeParse(
@@ -147,48 +229,53 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
147229
const { template } = queryValidation.data
148230

149231
if (!template) {
150-
const categories = {
151-
Auth: ['otp', 'reset-password', 'welcome'],
152-
Invitations: ['invitation', 'batch-invitation', 'workspace-invitation'],
153-
Support: ['help-confirmation'],
154-
Billing: [
155-
'usage-threshold',
156-
'enterprise-subscription',
157-
'free-tier-upgrade',
158-
'plan-welcome-pro',
159-
'plan-welcome-team',
160-
'credit-purchase',
161-
'payment-failed',
162-
'usage-limit-reached',
163-
'usage-limit-reached-org',
164-
],
165-
Notifications: ['schedule-disabled', 'schedule-disabled-auth'],
166-
}
167-
168-
const categoryHtml = Object.entries(categories)
232+
const categoryHtml = Object.entries(PREVIEW_CATEGORIES)
169233
.map(
170234
([category, templates]) => `
171-
<h2 style="margin-top: 24px; margin-bottom: 12px; font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">${category}</h2>
172-
<ul style="list-style: none; padding: 0; margin: 0;">
173-
${templates.map((t) => `<li style="margin: 8px 0;"><a href="?template=${t}" style="color: #33C482; text-decoration: none; font-size: 16px;">${t}</a></li>`).join('')}
174-
</ul>
175-
`
235+
<section>
236+
<h2>${category}</h2>
237+
<div class="grid">
238+
${templates
239+
.map(
240+
(t) => `
241+
<figure>
242+
<figcaption><span>${t}</span><a href="?template=${t}" target="_blank" rel="noreferrer">open ↗</a></figcaption>
243+
<iframe src="?template=${t}" title="${t}" loading="lazy"></iframe>
244+
</figure>`
245+
)
246+
.join('')}
247+
</div>
248+
</section>`
176249
)
177250
.join('')
178251

179252
return new NextResponse(
180253
`<!DOCTYPE html>
181254
<html>
182255
<head>
183-
<title>Email Previews</title>
256+
<meta charset="utf-8" />
257+
<meta name="viewport" content="width=device-width, initial-scale=1" />
258+
<title>Email Templates</title>
184259
<style>
185-
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
186-
h1 { color: #333; margin-bottom: 32px; }
187-
a:hover { text-decoration: underline; }
260+
:root { color-scheme: light; }
261+
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 40px 24px 80px; background: #fff; color: #1a1a1a; }
262+
h1 { font-size: 24px; font-weight: 600; margin: 0 0 4px; }
263+
.count { color: #7a7a7a; font-size: 14px; margin: 0 0 40px; }
264+
h2 { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: #7a7a7a; margin: 48px 0 16px; padding-bottom: 8px; border-bottom: 1px solid #d8d8d8; }
265+
section { max-width: 1400px; margin: 0 auto; }
266+
section > h2:first-child { margin-top: 0; }
267+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(640px, 1fr)); gap: 32px; }
268+
figure { margin: 0 0 32px; }
269+
figcaption { display: flex; justify-content: space-between; align-items: baseline; font-size: 13px; margin-bottom: 8px; }
270+
figcaption span { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #434343; }
271+
figcaption a { color: #7a7a7a; text-decoration: none; font-size: 12px; }
272+
figcaption a:hover { color: #1a1a1a; }
273+
iframe { width: 100%; height: 900px; border: 1px solid #d8d8d8; border-radius: 8px; background: #fff; display: block; }
188274
</style>
189275
</head>
190276
<body>
191277
<h1>Email Templates</h1>
278+
<p class="count">Every email Sim sends — ${Object.values(PREVIEW_CATEGORIES).flat().length} previews.</p>
192279
${categoryHtml}
193280
</body>
194281
</html>`,

0 commit comments

Comments
 (0)