From f9fa3bc83bd515c45d0833d6d8e21ea01a92cbb8 Mon Sep 17 00:00:00 2001 From: Kirubel-Kibru-Yaltopia Date: Fri, 7 Aug 2026 22:12:21 +0300 Subject: [PATCH] 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 --- .gitignore | 8 + CHANGELOG.md | 25 + README.md | 89 + app/api/email/[template]/route.ts | 38 + app/layout.tsx | 24 + app/page.tsx | 315 + .../2026-08-07-gishen-email-templates.md | 141 + emails/approval-needed.tsx | 46 + emails/components/Button.tsx | 34 + emails/components/Card.tsx | 19 + emails/components/EmailLayout.tsx | 128 + emails/components/MetricGrid.tsx | 44 + emails/components/OrderSummary.tsx | 78 + emails/components/StatusBanner.tsx | 65 + emails/components/styles.ts | 27 + emails/credit-alert.tsx | 47 + emails/dispatch-assigned.tsx | 46 + emails/email-verify.tsx | 41 + emails/i18n/common.ts | 29 + emails/i18n/templates/index.ts | 412 + emails/i18n/types.ts | 5 + emails/join-code.tsx | 44 + emails/low-stock.tsx | 48 + emails/order-cancelled.tsx | 59 + emails/order-confirmation.tsx | 59 + emails/order-delivered.tsx | 59 + emails/order-out-for-delivery.tsx | 63 + emails/order-packing.tsx | 59 + emails/org-invite.tsx | 43 + emails/organisation-verified.tsx | 42 + emails/password-reset.tsx | 41 + emails/prescription-decision.tsx | 50 + emails/prescription-ready.tsx | 44 + emails/prescription-submitted.tsx | 42 + emails/procurement-alert.tsx | 46 + emails/promotional.tsx | 47 + emails/registry.ts | 79 + emails/shop-summary.tsx | 70 + emails/statement-ready.tsx | 46 + emails/theme.ts | 36 + emails/user-invite.tsx | 43 + emails/utils/interpolate.ts | 3 + emails/utils/logoDataUri.ts | 1 + emails/welcome.tsx | 40 + next-env.d.ts | 5 + next.config.js | 6 + package-lock.json | 7547 +++++++++++++++++ package.json | 25 + public/brand/gishen-logo.png | Bin 0 -> 27604 bytes tsconfig.json | 24 + 50 files changed, 10332 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 app/api/email/[template]/route.ts create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 docs/superpowers/plans/2026-08-07-gishen-email-templates.md create mode 100644 emails/approval-needed.tsx create mode 100644 emails/components/Button.tsx create mode 100644 emails/components/Card.tsx create mode 100644 emails/components/EmailLayout.tsx create mode 100644 emails/components/MetricGrid.tsx create mode 100644 emails/components/OrderSummary.tsx create mode 100644 emails/components/StatusBanner.tsx create mode 100644 emails/components/styles.ts create mode 100644 emails/credit-alert.tsx create mode 100644 emails/dispatch-assigned.tsx create mode 100644 emails/email-verify.tsx create mode 100644 emails/i18n/common.ts create mode 100644 emails/i18n/templates/index.ts create mode 100644 emails/i18n/types.ts create mode 100644 emails/join-code.tsx create mode 100644 emails/low-stock.tsx create mode 100644 emails/order-cancelled.tsx create mode 100644 emails/order-confirmation.tsx create mode 100644 emails/order-delivered.tsx create mode 100644 emails/order-out-for-delivery.tsx create mode 100644 emails/order-packing.tsx create mode 100644 emails/org-invite.tsx create mode 100644 emails/organisation-verified.tsx create mode 100644 emails/password-reset.tsx create mode 100644 emails/prescription-decision.tsx create mode 100644 emails/prescription-ready.tsx create mode 100644 emails/prescription-submitted.tsx create mode 100644 emails/procurement-alert.tsx create mode 100644 emails/promotional.tsx create mode 100644 emails/registry.ts create mode 100644 emails/shop-summary.tsx create mode 100644 emails/statement-ready.tsx create mode 100644 emails/theme.ts create mode 100644 emails/user-invite.tsx create mode 100644 emails/utils/interpolate.ts create mode 100644 emails/utils/logoDataUri.ts create mode 100644 emails/welcome.tsx create mode 100644 next-env.d.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/brand/gishen-logo.png create mode 100644 tsconfig.json 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…
+ ) : ( +