Ship 23 transactional templates, shared brand tokens/logo, preview UI, render API, CHANGELOG, and docs for Ecom/Admin/B2B consumers. Co-authored-by: Cursor <cursoragent@cursor.com>
316 lines
8.0 KiB
TypeScript
316 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { templateCatalog, type TemplateCategory } from '../emails/registry';
|
|
import { theme } from '../emails/theme';
|
|
import type { Locale } from '../emails/i18n/types';
|
|
import type { ShopSummaryPeriod } from '../emails/shop-summary';
|
|
|
|
const periods: ShopSummaryPeriod[] = [
|
|
'daily',
|
|
'weekly',
|
|
'monthly',
|
|
'quarterly',
|
|
'yearly',
|
|
];
|
|
|
|
export default function Home() {
|
|
const [selectedId, setSelectedId] = useState(templateCatalog[0].id);
|
|
const [locale, setLocale] = useState<Locale>('en');
|
|
const [period, setPeriod] = useState<ShopSummaryPeriod>('weekly');
|
|
const [htmlContent, setHtmlContent] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const selected = useMemo(
|
|
() => templateCatalog.find((t) => t.id === selectedId) || templateCatalog[0],
|
|
[selectedId]
|
|
);
|
|
|
|
const grouped = useMemo(() => {
|
|
return templateCatalog.reduce<Record<TemplateCategory, typeof templateCatalog>>(
|
|
(acc, item) => {
|
|
if (!acc[item.category]) acc[item.category] = [];
|
|
acc[item.category].push(item);
|
|
return acc;
|
|
},
|
|
{} as Record<TemplateCategory, typeof templateCatalog>
|
|
);
|
|
}, []);
|
|
|
|
const loadTemplate = async (id: string, nextLocale: Locale, nextPeriod: ShopSummaryPeriod) => {
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams({ locale: nextLocale });
|
|
if (id === 'shop-summary') params.set('period', nextPeriod);
|
|
const response = await fetch(`/api/email/${id}?${params.toString()}`);
|
|
const html = await response.text();
|
|
setHtmlContent(html);
|
|
} catch (error) {
|
|
console.error(error);
|
|
setHtmlContent('<p>Failed to render template.</p>');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadTemplate(selectedId, locale, period);
|
|
}, [selectedId, locale, period]);
|
|
|
|
const handleDownload = () => {
|
|
const blob = new Blob([htmlContent], { type: 'text/html' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${selectedId}-${locale}.html`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
return (
|
|
<div style={page}>
|
|
<header style={header}>
|
|
<div>
|
|
<p style={eyebrow}>Gishen Pharmacy</p>
|
|
<h1 style={title}>Email Templates</h1>
|
|
<p style={subtitle}>
|
|
Shared previews for Ecom, Admin, and B2B · brand colors & logo from Gishen apps
|
|
</p>
|
|
</div>
|
|
<div style={controls}>
|
|
<label style={controlLabel}>
|
|
Locale
|
|
<select
|
|
value={locale}
|
|
onChange={(e) => setLocale(e.target.value as Locale)}
|
|
style={select}
|
|
>
|
|
<option value="en">English</option>
|
|
<option value="am">አማርኛ</option>
|
|
</select>
|
|
</label>
|
|
{selectedId === 'shop-summary' && (
|
|
<label style={controlLabel}>
|
|
Period
|
|
<select
|
|
value={period}
|
|
onChange={(e) => setPeriod(e.target.value as ShopSummaryPeriod)}
|
|
style={select}
|
|
>
|
|
{periods.map((p) => (
|
|
<option key={p} value={p}>
|
|
{p}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
<button type="button" onClick={handleDownload} style={downloadBtn} disabled={loading}>
|
|
Download HTML
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div style={layout}>
|
|
<aside style={sidebar}>
|
|
{(Object.keys(grouped) as TemplateCategory[]).map((category) => (
|
|
<div key={category} style={categoryBlock}>
|
|
<h2 style={categoryTitle}>{category}</h2>
|
|
<nav style={nav}>
|
|
{grouped[category].map((template) => {
|
|
const active = template.id === selected.id;
|
|
return (
|
|
<button
|
|
key={template.id}
|
|
type="button"
|
|
onClick={() => setSelectedId(template.id)}
|
|
style={{
|
|
...navButton,
|
|
...(active ? activeNavButton : {}),
|
|
}}
|
|
>
|
|
{template.name}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
))}
|
|
</aside>
|
|
|
|
<main style={previewPane}>
|
|
<div style={previewHeader}>
|
|
<strong>{selected.name}</strong>
|
|
<span style={meta}>
|
|
/api/email/{selected.id}?locale={locale}
|
|
{selectedId === 'shop-summary' ? `&period=${period}` : ''}
|
|
</span>
|
|
</div>
|
|
{loading ? (
|
|
<div style={loadingState}>Rendering…</div>
|
|
) : (
|
|
<iframe title={selected.name} srcDoc={htmlContent} style={iframe} />
|
|
)}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const page = {
|
|
minHeight: '100vh',
|
|
background: `linear-gradient(180deg, ${theme.colors.cream} 0%, #E8EDD8 100%)`,
|
|
};
|
|
|
|
const header = {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
gap: '24px',
|
|
alignItems: 'flex-end',
|
|
padding: '28px 32px 20px',
|
|
borderBottom: `1px solid ${theme.colors.line}`,
|
|
background: theme.colors.surface,
|
|
flexWrap: 'wrap' as const,
|
|
};
|
|
|
|
const eyebrow = {
|
|
margin: 0,
|
|
color: theme.colors.brandDeep,
|
|
fontWeight: 700,
|
|
letterSpacing: '0.04em',
|
|
textTransform: 'uppercase' as const,
|
|
fontSize: '12px',
|
|
};
|
|
|
|
const title = {
|
|
margin: '6px 0 8px',
|
|
fontSize: '32px',
|
|
color: theme.colors.forest,
|
|
};
|
|
|
|
const subtitle = {
|
|
margin: 0,
|
|
color: theme.colors.muted,
|
|
maxWidth: '520px',
|
|
};
|
|
|
|
const controls = {
|
|
display: 'flex',
|
|
gap: '12px',
|
|
alignItems: 'flex-end',
|
|
flexWrap: 'wrap' as const,
|
|
};
|
|
|
|
const controlLabel = {
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
gap: '6px',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
color: theme.colors.muted,
|
|
};
|
|
|
|
const select = {
|
|
minWidth: '140px',
|
|
padding: '10px 12px',
|
|
borderRadius: '8px',
|
|
border: `1px solid ${theme.colors.line}`,
|
|
background: theme.colors.surface,
|
|
color: theme.colors.forest,
|
|
};
|
|
|
|
const downloadBtn = {
|
|
padding: '11px 16px',
|
|
borderRadius: '8px',
|
|
border: 'none',
|
|
background: theme.colors.forest,
|
|
color: theme.colors.brand,
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
};
|
|
|
|
const layout = {
|
|
display: 'grid',
|
|
gridTemplateColumns: '280px 1fr',
|
|
minHeight: 'calc(100vh - 140px)',
|
|
};
|
|
|
|
const sidebar = {
|
|
padding: '20px 16px 40px',
|
|
borderRight: `1px solid ${theme.colors.line}`,
|
|
background: 'rgba(255,255,255,0.7)',
|
|
overflowY: 'auto' as const,
|
|
maxHeight: 'calc(100vh - 140px)',
|
|
};
|
|
|
|
const categoryBlock = {
|
|
marginBottom: '18px',
|
|
};
|
|
|
|
const categoryTitle = {
|
|
margin: '0 0 8px',
|
|
fontSize: '12px',
|
|
textTransform: 'uppercase' as const,
|
|
letterSpacing: '0.06em',
|
|
color: theme.colors.muted,
|
|
};
|
|
|
|
const nav = {
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
gap: '4px',
|
|
};
|
|
|
|
const navButton = {
|
|
textAlign: 'left' as const,
|
|
padding: '10px 12px',
|
|
borderRadius: '8px',
|
|
border: 'none',
|
|
background: 'transparent',
|
|
color: theme.colors.forest,
|
|
cursor: 'pointer',
|
|
fontSize: '14px',
|
|
};
|
|
|
|
const activeNavButton = {
|
|
background: theme.colors.brand,
|
|
color: theme.colors.forest,
|
|
fontWeight: 700,
|
|
};
|
|
|
|
const previewPane = {
|
|
padding: '20px',
|
|
};
|
|
|
|
const previewHeader = {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
gap: '12px',
|
|
alignItems: 'center',
|
|
marginBottom: '12px',
|
|
flexWrap: 'wrap' as const,
|
|
};
|
|
|
|
const meta = {
|
|
fontSize: '12px',
|
|
color: theme.colors.muted,
|
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
};
|
|
|
|
const iframe = {
|
|
width: '100%',
|
|
height: 'calc(100vh - 220px)',
|
|
border: `1px solid ${theme.colors.line}`,
|
|
borderRadius: '12px',
|
|
background: theme.colors.surface,
|
|
};
|
|
|
|
const loadingState = {
|
|
height: 'calc(100vh - 220px)',
|
|
display: 'grid',
|
|
placeItems: 'center',
|
|
border: `1px dashed ${theme.colors.line}`,
|
|
borderRadius: '12px',
|
|
color: theme.colors.muted,
|
|
};
|