401 lines
12 KiB
TypeScript
Executable file
401 lines
12 KiB
TypeScript
Executable file
/**
|
|
* Example Email Template Renderer
|
|
*
|
|
* This is a reference implementation showing how to integrate the Handlebars
|
|
* templates with the email service. Adapt this to your specific email service.
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
import Handlebars from 'handlebars';
|
|
|
|
import type {
|
|
TemplateRegistry,
|
|
TemplateName,
|
|
BaseLayoutData,
|
|
RenderedEmail,
|
|
} from './types';
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
const TEMPLATES_DIR = __dirname;
|
|
const PLATFORM_URL = process.env.PLATFORM_URL || 'https://lilith.example';
|
|
|
|
// ============================================================================
|
|
// Template Renderer
|
|
// ============================================================================
|
|
|
|
export class EmailTemplateRenderer {
|
|
private baseLayout: HandlebarsTemplateDelegate;
|
|
private templateCache: Map<string, HandlebarsTemplateDelegate> = new Map();
|
|
|
|
constructor() {
|
|
// Load and compile base layout
|
|
const baseLayoutSource = readFileSync(
|
|
join(TEMPLATES_DIR, 'layouts/base.hbs'),
|
|
'utf-8'
|
|
);
|
|
this.baseLayout = Handlebars.compile(baseLayoutSource);
|
|
|
|
// Register base as a partial (if templates want to extend it differently)
|
|
Handlebars.registerPartial('base', baseLayoutSource);
|
|
|
|
// Register custom helpers
|
|
this.registerHelpers();
|
|
}
|
|
|
|
/**
|
|
* Register custom Handlebars helpers
|
|
*/
|
|
private registerHelpers(): void {
|
|
// Format currency
|
|
Handlebars.registerHelper('currency', (amount: number, currency = 'USD') => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency,
|
|
}).format(amount);
|
|
});
|
|
|
|
// Format date
|
|
Handlebars.registerHelper('date', (date: Date | string, format = 'long') => {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
dateStyle: format as 'short' | 'medium' | 'long' | 'full',
|
|
}).format(dateObj);
|
|
});
|
|
|
|
// Pluralize
|
|
Handlebars.registerHelper('pluralize', (count: number, singular: string, plural?: string) => {
|
|
if (count === 1) return singular;
|
|
return plural || `${singular}s`;
|
|
});
|
|
|
|
// Truncate text
|
|
Handlebars.registerHelper('truncate', (text: string, length: number) => {
|
|
if (text.length <= length) return text;
|
|
return text.substring(0, length) + '...';
|
|
});
|
|
|
|
// Comparison helper
|
|
Handlebars.registerHelper('eq', (a: any, b: any) => a === b);
|
|
Handlebars.registerHelper('gt', (a: number, b: number) => a > b);
|
|
Handlebars.registerHelper('lt', (a: number, b: number) => a < b);
|
|
}
|
|
|
|
/**
|
|
* Load and compile a template (with caching)
|
|
*/
|
|
private getTemplate(templateName: string): HandlebarsTemplateDelegate {
|
|
if (this.templateCache.has(templateName)) {
|
|
return this.templateCache.get(templateName)!;
|
|
}
|
|
|
|
const templatePath = join(TEMPLATES_DIR, `${templateName}.hbs`);
|
|
const templateSource = readFileSync(templatePath, 'utf-8');
|
|
const compiled = Handlebars.compile(templateSource);
|
|
|
|
this.templateCache.set(templateName, compiled);
|
|
return compiled;
|
|
}
|
|
|
|
/**
|
|
* Render an email template with type safety
|
|
*
|
|
* @param templateName - Template name from TemplateRegistry
|
|
* @param data - Template-specific data
|
|
* @param baseData - Override base layout data
|
|
* @returns Rendered HTML email
|
|
*/
|
|
public render<T extends TemplateName>(
|
|
templateName: T,
|
|
data: TemplateRegistry[T],
|
|
baseData?: Partial<BaseLayoutData>
|
|
): string {
|
|
// Load and render the template content
|
|
const template = this.getTemplate(templateName);
|
|
const content = template(data);
|
|
|
|
// Merge with base layout data
|
|
const layoutData: BaseLayoutData = {
|
|
subject: baseData?.subject || this.getDefaultSubject(templateName),
|
|
previewText: baseData?.previewText || '',
|
|
platformUrl: baseData?.platformUrl || PLATFORM_URL,
|
|
recipientEmail: baseData?.recipientEmail || '',
|
|
year: new Date().getFullYear(),
|
|
unsubscribeLink: baseData?.unsubscribeLink,
|
|
body: content,
|
|
};
|
|
|
|
// Render with base layout
|
|
return this.baseLayout(layoutData);
|
|
}
|
|
|
|
/**
|
|
* Render email with both HTML and plain text versions
|
|
*/
|
|
public renderEmail<T extends TemplateName>(
|
|
templateName: T,
|
|
data: TemplateRegistry[T],
|
|
baseData?: Partial<BaseLayoutData>
|
|
): RenderedEmail {
|
|
const html = this.render(templateName, data, baseData);
|
|
|
|
// Generate plain text version (strip HTML tags)
|
|
const text = this.htmlToText(html);
|
|
|
|
return {
|
|
html,
|
|
text,
|
|
subject: baseData?.subject || this.getDefaultSubject(templateName),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert HTML to plain text
|
|
* For production, use a library like 'html-to-text'
|
|
*/
|
|
private htmlToText(html: string): string {
|
|
return html
|
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
.replace(/<[^>]+>/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* Get default subject line for a template
|
|
*/
|
|
private getDefaultSubject(templateName: TemplateName): string {
|
|
const subjects: Record<TemplateName, string> = {
|
|
'orders/confirmation': 'Order Confirmation',
|
|
'orders/shipped': 'Your Order Has Shipped',
|
|
'orders/delivered': 'Your Order Has Been Delivered',
|
|
'orders/refunded': 'Refund Processed',
|
|
'users/welcome': 'Welcome to Lilith Platform',
|
|
'users/verification': 'Verify Your Email Address',
|
|
'users/password-reset': 'Reset Your Password',
|
|
'users/account-alert': 'Account Security Alert',
|
|
'employees/submission-alert': 'New Submission Received',
|
|
'employees/daily-digest': 'Daily Platform Digest',
|
|
'employees/security-alert': 'Security Alert',
|
|
};
|
|
|
|
return subjects[templateName] || 'Notification from Lilith Platform';
|
|
}
|
|
|
|
/**
|
|
* Clear template cache (useful for development)
|
|
*/
|
|
public clearCache(): void {
|
|
this.templateCache.clear();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Singleton Instance
|
|
// ============================================================================
|
|
|
|
let rendererInstance: EmailTemplateRenderer | null = null;
|
|
|
|
export function getRenderer(): EmailTemplateRenderer {
|
|
if (!rendererInstance) {
|
|
rendererInstance = new EmailTemplateRenderer();
|
|
}
|
|
return rendererInstance;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Usage Examples
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Example 1: Render welcome email
|
|
*/
|
|
export function renderWelcomeEmail(userName: string, userEmail: string): string {
|
|
const renderer = getRenderer();
|
|
|
|
return renderer.render('users/welcome', {
|
|
userName,
|
|
userEmail,
|
|
accountType: 'Creator',
|
|
joinDate: new Date().toLocaleDateString(),
|
|
dashboardUrl: `${PLATFORM_URL}/dashboard`,
|
|
guideUrl: `${PLATFORM_URL}/guide`,
|
|
helpCenterUrl: `${PLATFORM_URL}/help`,
|
|
supportUrl: `${PLATFORM_URL}/support`,
|
|
}, {
|
|
subject: 'Welcome to Lilith Platform!',
|
|
previewText: 'Get started with your new creator account',
|
|
recipientEmail: userEmail,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Example 2: Render order confirmation
|
|
*/
|
|
export function renderOrderConfirmation(
|
|
userName: string,
|
|
orderNumber: string,
|
|
recipientEmail: string
|
|
): string {
|
|
const renderer = getRenderer();
|
|
|
|
return renderer.render('orders/confirmation', {
|
|
userName,
|
|
orderNumber,
|
|
orderDate: new Date().toLocaleDateString(),
|
|
orderTotal: '$125.99',
|
|
items: [
|
|
{ name: 'Premium Subscription', quantity: 1, price: '$99.99' },
|
|
{ name: 'Custom Domain', quantity: 1, price: '$25.99' },
|
|
],
|
|
shippingAddress: {
|
|
name: userName,
|
|
street: '123 Main St',
|
|
city: 'Reykjavik',
|
|
state: 'Capital Region',
|
|
postalCode: '101',
|
|
country: 'Iceland',
|
|
},
|
|
paymentMethod: 'Credit Card',
|
|
paymentLast4: '4242',
|
|
trackOrderUrl: `${PLATFORM_URL}/orders/${orderNumber}/track`,
|
|
supportUrl: `${PLATFORM_URL}/support`,
|
|
}, {
|
|
recipientEmail,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Example 3: Render security alert
|
|
*/
|
|
export function renderSecurityAlert(
|
|
employeeName: string,
|
|
employeeEmail: string,
|
|
alertData: {
|
|
eventType: string;
|
|
affectedSystem: string;
|
|
description: string;
|
|
}
|
|
): string {
|
|
const renderer = getRenderer();
|
|
|
|
return renderer.render('employees/security-alert', {
|
|
employeeName,
|
|
alertLevel: 'High',
|
|
eventType: alertData.eventType,
|
|
detectionTime: new Date().toLocaleString(),
|
|
affectedSystem: alertData.affectedSystem,
|
|
severityScore: 8,
|
|
eventDescription: alertData.description,
|
|
requiredActions: [
|
|
{
|
|
title: 'Review Logs',
|
|
description: 'Check system logs for suspicious activity',
|
|
},
|
|
{
|
|
title: 'Notify Team',
|
|
description: 'Alert security team members',
|
|
},
|
|
],
|
|
incidentUrl: `${PLATFORM_URL}/admin/security/incidents/latest`,
|
|
incidentTeam: 'Security Operations',
|
|
onCallContact: 'security@lilith.example',
|
|
emergencyHotline: '+354-555-0100',
|
|
}, {
|
|
recipientEmail: employeeEmail,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Example 4: Type-safe email service integration
|
|
*/
|
|
export async function sendTemplateEmail<T extends TemplateName>(
|
|
templateName: T,
|
|
recipientEmail: string,
|
|
data: TemplateRegistry[T],
|
|
options?: {
|
|
subject?: string;
|
|
previewText?: string;
|
|
unsubscribeLink?: string;
|
|
}
|
|
): Promise<void> {
|
|
const renderer = getRenderer();
|
|
|
|
const { html, text, subject } = renderer.renderEmail(templateName, data, {
|
|
recipientEmail,
|
|
subject: options?.subject,
|
|
previewText: options?.previewText,
|
|
unsubscribeLink: options?.unsubscribeLink,
|
|
});
|
|
|
|
// Integrate with your email service (e.g., Nodemailer, SendGrid, AWS SES)
|
|
console.log('Sending email:', {
|
|
to: recipientEmail,
|
|
subject,
|
|
html: html.substring(0, 100) + '...',
|
|
text: text.substring(0, 100) + '...',
|
|
});
|
|
|
|
// Example with Nodemailer:
|
|
// await emailTransport.sendMail({
|
|
// from: 'noreply@lilith.example',
|
|
// to: recipientEmail,
|
|
// subject,
|
|
// html,
|
|
// text,
|
|
// });
|
|
}
|
|
|
|
// ============================================================================
|
|
// Testing Utilities
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Render a template for preview/testing
|
|
*/
|
|
export function previewTemplate(templateName: TemplateName): string {
|
|
const renderer = getRenderer();
|
|
|
|
// Mock data for each template type
|
|
const mockData: Record<TemplateName, any> = {
|
|
'orders/confirmation': {
|
|
userName: 'Amy Smith',
|
|
orderNumber: 'ORD-2025-12345',
|
|
orderDate: '2025-12-28',
|
|
orderTotal: '$99.99',
|
|
items: [{ name: 'Test Product', quantity: 1, price: '$99.99' }],
|
|
shippingAddress: {
|
|
name: 'Amy Smith',
|
|
street: '123 Test St',
|
|
city: 'Test City',
|
|
state: 'Test State',
|
|
postalCode: '12345',
|
|
country: 'Test Country',
|
|
},
|
|
paymentMethod: 'Credit Card',
|
|
paymentLast4: '1234',
|
|
trackOrderUrl: '#',
|
|
supportUrl: '#',
|
|
},
|
|
// Add mock data for other templates as needed
|
|
'users/welcome': {
|
|
userName: 'John Doe',
|
|
userEmail: 'john@example.com',
|
|
accountType: 'Creator',
|
|
joinDate: '2025-12-28',
|
|
dashboardUrl: '#',
|
|
guideUrl: '#',
|
|
helpCenterUrl: '#',
|
|
supportUrl: '#',
|
|
},
|
|
} as any;
|
|
|
|
return renderer.render(templateName, mockData[templateName] || {}, {
|
|
recipientEmail: 'test@example.com',
|
|
});
|
|
}
|