diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26be9e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +.next +out +emails-export +.DS_Store +*.log +.env +.env.local diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4098d2a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2026-08-07 + +### Added + +- React Email + Next.js preview app for Gishen Pharmacy +- Brand tokens and logo aligned with Gishen-B2B / Gishen-Admin +- English and Amharic copy for all templates +- Auth templates: welcome, password-reset, email-verify, user-invite +- Order templates: confirmation, packing, out-for-delivery, delivered, cancelled +- B2B templates: org-invite, join-code, approval-needed, statement-ready, credit-alert, organisation-verified +- Prescription templates: submitted, decision, ready +- Admin ops templates: low-stock, dispatch-assigned, procurement-alert +- Marketing: promotional +- Analytics: shop-summary (`daily` | `weekly` | `monthly` | `quarterly` | `yearly`) +- HTML render API at `/api/email/[template]` +- Design and implementation docs under `docs/superpowers/` + +[0.1.0]: https://gitea.yaltopia.com/Gishen/Gishen-Email-Templates/releases/tag/v0.1.0 diff --git a/README.md b/README.md index e69de29..7e4b3b2 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,89 @@ +# Gishen Email Templates + +React Email templates and a Next.js preview app for **Gishen Pharmacy** — shared across Ecom, Admin, and B2B. + +Brand colors and logo are taken from `Gishen-B2B` / `Gishen-Admin` (`#A8C73A`, `#141414`, `#E28A1A`, cream `#F6F7F2`). + +## Features + +- 23 templates (auth, orders, B2B, prescriptions, admin ops, marketing, shop analytics) +- English + Amharic (`locale=en|am`) +- Preview UI with category navigation, locale toggle, HTML download +- Render API for demos / consumer apps +- `shop-summary` with `period=daily|weekly|monthly|quarterly|yearly` + +## Getting started + +```bash +npm install +npm run preview +``` + +Open [http://localhost:3000](http://localhost:3000). + +### React Email CLI + +```bash +npm run email +``` + +### Export HTML + +```bash +npm run build +``` + +## Render API + +``` +GET /api/email/[template]?locale=en|am +GET /api/email/shop-summary?locale=en&period=weekly +``` + +Returns `text/html`. + +## Templates + +| ID | Category | +|----|----------| +| `welcome` | Auth | +| `password-reset` | Auth | +| `email-verify` | Auth | +| `user-invite` | Auth | +| `order-confirmation` | Orders | +| `order-packing` | Orders | +| `order-out-for-delivery` | Orders | +| `order-delivered` | Orders | +| `order-cancelled` | Orders | +| `org-invite` | B2B | +| `join-code` | B2B | +| `approval-needed` | B2B | +| `statement-ready` | B2B | +| `credit-alert` | B2B | +| `organisation-verified` | B2B | +| `prescription-submitted` | Prescriptions | +| `prescription-decision` | Prescriptions | +| `prescription-ready` | Prescriptions | +| `low-stock` | Admin ops | +| `dispatch-assigned` | Admin ops | +| `procurement-alert` | Admin ops | +| `promotional` | Marketing | +| `shop-summary` | Analytics | + +## Consuming from Gishen apps + +1. **Demo / staging:** call the render API and send the HTML through your ESP. +2. **Production (later):** import templates from this package or copy exported HTML into your notification worker. + +Do not hardcode brand colours in Ecom/Admin/B2B mailers — keep them here. + +## Related repos + +- [Gishen-Ecom](https://gitea.yaltopia.com/Gishen/Gishen-Ecom) +- [Gishen-Admin](https://gitea.yaltopia.com/Gishen/Gishen-Admin) +- [Gishen-B2B](https://gitea.yaltopia.com/Gishen/Gishen-B2B) +- Structure inspired by Amba-Emails / Yaltopia Ticket Email + +## License + +Proprietary — Yaltopia / Gishen. diff --git a/app/api/email/[template]/route.ts b/app/api/email/[template]/route.ts new file mode 100644 index 0000000..ff6bfe0 --- /dev/null +++ b/app/api/email/[template]/route.ts @@ -0,0 +1,38 @@ +import React from 'react'; +import { render } from '@react-email/render'; +import { NextResponse } from 'next/server'; +import { templateMap } from '../../../../emails/registry'; +import { resolveLocale } from '../../../../emails/i18n/types'; +import type { ShopSummaryPeriod } from '../../../../emails/shop-summary'; + +export async function GET( + request: Request, + context: { params: Promise<{ template: string }> } +) { + const { template } = await context.params; + const entry = templateMap[template]; + + if (!entry) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }); + } + + const { searchParams } = new URL(request.url); + const locale = resolveLocale(searchParams.get('locale') || undefined); + const period = (searchParams.get('period') || + entry.defaultProps?.period || + 'weekly') as ShopSummaryPeriod; + + const html = await render( + React.createElement(entry.component, { + locale, + ...(entry.id === 'shop-summary' ? { period } : {}), + ...entry.defaultProps, + }) + ); + + return new NextResponse(html, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..c1a3e54 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react'; +import { theme } from '../emails/theme'; + +export const metadata = { + title: 'Gishen Email Templates', + description: 'Preview transactional emails for Gishen Pharmacy', +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..90ecbff --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,315 @@ +'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('en'); + const [period, setPeriod] = useState('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>( + (acc, item) => { + if (!acc[item.category]) acc[item.category] = []; + acc[item.category].push(item); + return acc; + }, + {} as Record + ); + }, []); + + 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('

Failed to render template.

'); + } 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 ( +
+
+
+

Gishen Pharmacy

+

Email Templates

+

+ Shared previews for Ecom, Admin, and B2B · brand colors & logo from Gishen apps +

+
+
+ + {selectedId === 'shop-summary' && ( + + )} + +
+
+ +
+ + +
+
+ {selected.name} + + /api/email/{selected.id}?locale={locale} + {selectedId === 'shop-summary' ? `&period=${period}` : ''} + +
+ {loading ? ( +
Rendering…
+ ) : ( +