- Add in-memory email queue with rate limiting (MAX_EMAILS_PER_HOUR) - Bulk send to event attendees now queues and returns immediately - Frontend shows 'Emails are being sent in the background' - Legal pages, settings, and placeholders updates Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
export interface LegalPage {
|
|
slug: string;
|
|
title: string;
|
|
content: string;
|
|
lastUpdated?: string;
|
|
}
|
|
|
|
export interface LegalPageMeta {
|
|
slug: string;
|
|
title: string;
|
|
}
|
|
|
|
// Map file names to display titles
|
|
const titleMap: Record<string, { en: string; es: string }> = {
|
|
'privacy_policy': { en: 'Privacy Policy', es: 'Política de Privacidad' },
|
|
'privacy-policy': { en: 'Privacy Policy', es: 'Política de Privacidad' },
|
|
'terms_policy': { en: 'Terms & Conditions', es: 'Términos y Condiciones' },
|
|
'terms-policy': { en: 'Terms & Conditions', es: 'Términos y Condiciones' },
|
|
'refund_cancelation_policy': { en: 'Refund & Cancellation Policy', es: 'Política de Reembolso y Cancelación' },
|
|
'refund-cancelation-policy': { en: 'Refund & Cancellation Policy', es: 'Política de Reembolso y Cancelación' },
|
|
};
|
|
|
|
// Convert file name to URL-friendly slug
|
|
export function fileNameToSlug(fileName: string): string {
|
|
return fileName.replace('.md', '').replace(/_/g, '-');
|
|
}
|
|
|
|
// Convert slug back to file name
|
|
export function slugToFileName(slug: string): string {
|
|
return slug.replace(/-/g, '_') + '.md';
|
|
}
|
|
|
|
// Get the legal directory path
|
|
function getLegalDir(): string {
|
|
return path.join(process.cwd(), 'legal');
|
|
}
|
|
|
|
// Get all legal page slugs for static generation
|
|
export function getAllLegalSlugs(): string[] {
|
|
const legalDir = getLegalDir();
|
|
|
|
if (!fs.existsSync(legalDir)) {
|
|
return [];
|
|
}
|
|
|
|
const files = fs.readdirSync(legalDir);
|
|
return files
|
|
.filter(file => file.endsWith('.md'))
|
|
.map(file => fileNameToSlug(file));
|
|
}
|
|
|
|
// Get metadata for all legal pages (for navigation/footer)
|
|
export function getAllLegalPagesMeta(locale: string = 'en'): LegalPageMeta[] {
|
|
const legalDir = getLegalDir();
|
|
|
|
if (!fs.existsSync(legalDir)) {
|
|
return [];
|
|
}
|
|
|
|
const files = fs.readdirSync(legalDir);
|
|
return files
|
|
.filter(file => file.endsWith('.md'))
|
|
.map(file => {
|
|
const slug = fileNameToSlug(file);
|
|
const baseFileName = file.replace('.md', '');
|
|
const titles = titleMap[baseFileName];
|
|
const title = titles ? titles[locale as 'en' | 'es'] || titles.en : baseFileName.replace(/_/g, ' ');
|
|
|
|
return { slug, title };
|
|
});
|
|
}
|
|
|
|
// Get a specific legal page content from filesystem (fallback)
|
|
export function getLegalPageFromFilesystem(slug: string, locale: string = 'en'): LegalPage | null {
|
|
const legalDir = getLegalDir();
|
|
const fileName = slugToFileName(slug);
|
|
const filePath = path.join(legalDir, fileName);
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return null;
|
|
}
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const baseFileName = fileName.replace('.md', '');
|
|
const titles = titleMap[baseFileName] || titleMap[slug];
|
|
const title = titles ? titles[locale as 'en' | 'es'] || titles.en : baseFileName.replace(/_/g, ' ');
|
|
|
|
// Try to extract last updated date from content
|
|
const lastUpdatedMatch = content.match(/Last updated:\s*(.+)/i);
|
|
const lastUpdated = lastUpdatedMatch ? lastUpdatedMatch[1].trim() : undefined;
|
|
|
|
return {
|
|
slug,
|
|
title,
|
|
content,
|
|
lastUpdated,
|
|
};
|
|
}
|
|
|
|
// Get a specific legal page content - tries API first, falls back to filesystem
|
|
export async function getLegalPageAsync(slug: string, locale: string = 'en'): Promise<LegalPage | null> {
|
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
|
|
|
// Try to fetch from API with locale parameter
|
|
try {
|
|
const response = await fetch(`${apiUrl}/api/legal-pages/${slug}?locale=${locale}`, {
|
|
next: { revalidate: 60 }, // Cache for 60 seconds
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const page = data.page;
|
|
|
|
if (page) {
|
|
// Extract last updated from content or use updatedAt
|
|
const lastUpdatedMatch = page.contentMarkdown?.match(/Last updated:\s*(.+)/i);
|
|
const lastUpdated = lastUpdatedMatch ? lastUpdatedMatch[1].trim() : page.updatedAt;
|
|
|
|
return {
|
|
slug: page.slug,
|
|
title: page.title, // API already returns localized title with fallback
|
|
content: page.contentMarkdown, // API already returns localized content with fallback
|
|
lastUpdated,
|
|
};
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to fetch legal page from API, falling back to filesystem:', error);
|
|
}
|
|
|
|
// Fallback to filesystem
|
|
return getLegalPageFromFilesystem(slug, locale);
|
|
}
|
|
|
|
// Legacy sync function for backwards compatibility
|
|
export function getLegalPage(slug: string, locale: string = 'en'): LegalPage | null {
|
|
return getLegalPageFromFilesystem(slug, locale);
|
|
}
|