d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
169 lines
5.0 KiB
TypeScript
169 lines
5.0 KiB
TypeScript
import type { NextRequest } from 'next/server'
|
|
import { NextResponse } from 'next/server'
|
|
import {
|
|
renderBatchInvitationEmail,
|
|
renderCreditPurchaseEmail,
|
|
renderEnterpriseSubscriptionEmail,
|
|
renderFreeTierUpgradeEmail,
|
|
renderHelpConfirmationEmail,
|
|
renderInvitationEmail,
|
|
renderOTPEmail,
|
|
renderPasswordResetEmail,
|
|
renderPaymentFailedEmail,
|
|
renderPlanWelcomeEmail,
|
|
renderUsageThresholdEmail,
|
|
renderWelcomeEmail,
|
|
renderWorkspaceInvitationEmail,
|
|
} from '@/components/emails'
|
|
import { emailPreviewQuerySchema } from '@/lib/api/contracts/common'
|
|
import { validationErrorResponse } from '@/lib/api/server'
|
|
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
|
|
|
const emailTemplates = {
|
|
// Auth emails
|
|
otp: () => renderOTPEmail('123456', 'user@example.com', 'email-verification'),
|
|
'reset-password': () => renderPasswordResetEmail('John', 'https://sim.ai/reset?token=abc123'),
|
|
welcome: () => renderWelcomeEmail('John'),
|
|
|
|
// Invitation emails
|
|
invitation: () => renderInvitationEmail('Jane Doe', 'Acme Corp', 'https://sim.ai/invite/abc123'),
|
|
'batch-invitation': () =>
|
|
renderBatchInvitationEmail(
|
|
'Jane Doe',
|
|
'Acme Corp',
|
|
'admin',
|
|
[
|
|
{ workspaceId: 'ws_123', workspaceName: 'Engineering', permission: 'write' },
|
|
{ workspaceId: 'ws_456', workspaceName: 'Design', permission: 'read' },
|
|
],
|
|
'https://sim.ai/invite/abc123'
|
|
),
|
|
'workspace-invitation': () =>
|
|
renderWorkspaceInvitationEmail(
|
|
'John Smith',
|
|
['Engineering Team'],
|
|
'https://sim.ai/workspace/invite/abc123'
|
|
),
|
|
|
|
// Support emails
|
|
'help-confirmation': () => renderHelpConfirmationEmail('feature_request', 2),
|
|
|
|
// Billing emails
|
|
'usage-threshold': () =>
|
|
renderUsageThresholdEmail({
|
|
userName: 'John',
|
|
planName: 'Pro',
|
|
percentUsed: 75,
|
|
currentUsage: 15,
|
|
limit: 20,
|
|
ctaLink: 'https://sim.ai/settings/billing',
|
|
}),
|
|
'enterprise-subscription': () => renderEnterpriseSubscriptionEmail('John'),
|
|
'free-tier-upgrade': () =>
|
|
renderFreeTierUpgradeEmail({
|
|
userName: 'John',
|
|
percentUsed: 90,
|
|
currentUsage: 9,
|
|
limit: 10,
|
|
upgradeLink: 'https://sim.ai/settings/billing',
|
|
}),
|
|
'plan-welcome-pro': () =>
|
|
renderPlanWelcomeEmail({
|
|
planName: 'Pro',
|
|
userName: 'John',
|
|
loginLink: 'https://sim.ai/login',
|
|
}),
|
|
'plan-welcome-team': () =>
|
|
renderPlanWelcomeEmail({
|
|
planName: 'Team',
|
|
userName: 'John',
|
|
loginLink: 'https://sim.ai/login',
|
|
}),
|
|
'credit-purchase': () =>
|
|
renderCreditPurchaseEmail({
|
|
userName: 'John',
|
|
amount: 50,
|
|
newBalance: 75,
|
|
}),
|
|
'payment-failed': () =>
|
|
renderPaymentFailedEmail({
|
|
userName: 'John',
|
|
amountDue: 20,
|
|
lastFourDigits: '4242',
|
|
billingPortalUrl: 'https://sim.ai/settings/billing',
|
|
failureReason: 'Card declined',
|
|
}),
|
|
} as const
|
|
|
|
type EmailTemplate = keyof typeof emailTemplates
|
|
|
|
function isEmailTemplate(template: string): template is EmailTemplate {
|
|
return template in emailTemplates
|
|
}
|
|
|
|
export const GET = withRouteHandler(async (request: NextRequest) => {
|
|
const { searchParams } = new URL(request.url)
|
|
const queryValidation = emailPreviewQuerySchema.safeParse(
|
|
Object.fromEntries(searchParams.entries())
|
|
)
|
|
if (!queryValidation.success) return validationErrorResponse(queryValidation.error)
|
|
const { template } = queryValidation.data
|
|
|
|
if (!template) {
|
|
const categories = {
|
|
Auth: ['otp', 'reset-password', 'welcome'],
|
|
Invitations: ['invitation', 'batch-invitation', 'workspace-invitation'],
|
|
Support: ['help-confirmation'],
|
|
Billing: [
|
|
'usage-threshold',
|
|
'enterprise-subscription',
|
|
'free-tier-upgrade',
|
|
'plan-welcome-pro',
|
|
'plan-welcome-team',
|
|
'credit-purchase',
|
|
'payment-failed',
|
|
],
|
|
}
|
|
|
|
const categoryHtml = Object.entries(categories)
|
|
.map(
|
|
([category, templates]) => `
|
|
<h2 style="margin-top: 24px; margin-bottom: 12px; font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">${category}</h2>
|
|
<ul style="list-style: none; padding: 0; margin: 0;">
|
|
${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('')}
|
|
</ul>
|
|
`
|
|
)
|
|
.join('')
|
|
|
|
return new NextResponse(
|
|
`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Email Previews</title>
|
|
<style>
|
|
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
|
|
h1 { color: #333; margin-bottom: 32px; }
|
|
a:hover { text-decoration: underline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Email Templates</h1>
|
|
${categoryHtml}
|
|
</body>
|
|
</html>`,
|
|
{ headers: { 'Content-Type': 'text/html' } }
|
|
)
|
|
}
|
|
|
|
if (!isEmailTemplate(template)) {
|
|
return NextResponse.json({ error: `Unknown template: ${template}` }, { status: 400 })
|
|
}
|
|
|
|
const html = await emailTemplates[template]()
|
|
|
|
return new NextResponse(html, {
|
|
headers: { 'Content-Type': 'text/html' },
|
|
})
|
|
})
|