This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Email-Templates/app/page.tsx
Kirubel-Kibru-Yaltopia f9fa3bc83b feat: add Gishen React Email demo with en/am templates
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>
2026-08-07 22:12:21 +03:00

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,
};