From cf663e1451e1c122458572542e6c19591cad6997 Mon Sep 17 00:00:00 2001 From: yeabwang Date: Mon, 13 Jul 2026 11:13:22 +0800 Subject: [PATCH] feat(demo): add invoice document with Cloud PDF export example --- .../demo/src/sections/TemplateShowcase.tsx | 11 +- .../demo/src/templates/InvoiceReceipt.tsx | 334 ++++++++++++++++++ packages/demo/src/templates/index.ts | 16 +- 3 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 packages/demo/src/templates/InvoiceReceipt.tsx diff --git a/packages/demo/src/sections/TemplateShowcase.tsx b/packages/demo/src/sections/TemplateShowcase.tsx index c850b537..ef2b0669 100644 --- a/packages/demo/src/sections/TemplateShowcase.tsx +++ b/packages/demo/src/sections/TemplateShowcase.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { templates } from "../templates"; +import { templates, type TemplateEntry } from "../templates"; import { useRenderTemplate } from "../hooks/useRenderTemplate"; import SectionHeader from "../components/SectionHeader"; import TabBar from "../components/TabBar"; @@ -22,6 +22,13 @@ const categoryLabels: Record = { transactional: "Transactional", notification: "Notification", security: "Security", + document: "Document", +}; + +const modeLabels: Record, string> = { + email: "Email", + web: "Web", + document: "Document", }; export default function TemplateShowcase() { @@ -215,7 +222,7 @@ export default function TemplateShowcase() { {selected.description} - Rendering as Email mode + Rendering as {modeLabels[selected.mode ?? "email"]} mode diff --git a/packages/demo/src/templates/InvoiceReceipt.tsx b/packages/demo/src/templates/InvoiceReceipt.tsx new file mode 100644 index 00000000..bd9ca5a0 --- /dev/null +++ b/packages/demo/src/templates/InvoiceReceipt.tsx @@ -0,0 +1,334 @@ +import type { ReactElement } from "react"; +import type { TableValues } from "@unlayer/react-elements"; +import { + Document, + Row, + Column, + Paragraph, + Heading, + Divider, + Table, + ColumnLayouts, +} from "@unlayer/react-elements"; + +// import { renderToHtml, renderToJson } from "@unlayer/react-elements"; +// const tree = InvoiceReceipt(); +// const html = renderToHtml(tree, { title: "Invoice INV-2041" }); +// +// Cloud PDF export (server only): +// const design = renderToJson(tree); +// const apiKey = process.env.UNLAYER_API_KEY; +// if (!apiKey) throw new Error("UNLAYER_API_KEY is required"); +// const authorization = Buffer.from(`${apiKey}:`).toString("base64"); +// const response = await fetch("https://api.unlayer.com/v2/export/pdf", { +// method: "POST", +// headers: { +// Authorization: `Basic ${authorization}`, +// "Content-Type": "application/json", +// }, +// body: JSON.stringify({ design, displayMode: "web" }), +// }); +// if (!response.ok) throw new Error(`PDF export failed: ${response.status}`); +// const result = await response.json(); +// if (typeof result?.data?.url !== "string") throw new Error("PDF export returned no URL"); +// const pdfUrl = result.data.url; +// +// The PDF endpoint accepts displayMode "web", not "document". + +const sansFont = { + label: "Sans Serif", + value: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif", +}; + +const lineItemsTable: Partial = { + columns: 4, + rows: 3, + enableHeader: true, + table: { + headers: [{ + height: 0, + cells: [ + { text: "DESCRIPTION", width: 46, textAlign: "left" }, + { text: "QTY", width: 14, textAlign: "right" }, + { text: "UNIT PRICE", width: 20, textAlign: "right" }, + { text: "AMOUNT", width: 20, textAlign: "right" }, + ], + }], + rows: [ + { + height: 0, + cells: [ + { text: "Design system audit", width: 46, textAlign: "left" }, + { text: "1", width: 14, textAlign: "right" }, + { text: "$2,400.00", width: 20, textAlign: "right" }, + { text: "$2,400.00", width: 20, textAlign: "right" }, + ], + }, + { + height: 0, + cells: [ + { text: "Component library development", width: 46, textAlign: "left" }, + { text: "40", width: 14, textAlign: "right" }, + { text: "$120.00", width: 20, textAlign: "right" }, + { text: "$4,800.00", width: 20, textAlign: "right" }, + ], + }, + { + height: 0, + cells: [ + { text: "Accessibility review", width: 46, textAlign: "left" }, + { text: "1", width: 14, textAlign: "right" }, + { text: "$950.00", width: 20, textAlign: "right" }, + { text: "$950.00", width: 20, textAlign: "right" }, + ], + }, + ], + footers: [], + }, +}; + +export default function InvoiceReceipt(): ReactElement { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/demo/src/templates/index.ts b/packages/demo/src/templates/index.ts index 120c32fc..e39035fe 100644 --- a/packages/demo/src/templates/index.ts +++ b/packages/demo/src/templates/index.ts @@ -12,6 +12,7 @@ import ShippingUpdate from "./ShippingUpdate"; import ProductLaunch from "./ProductLaunch"; import VerificationCode from "./VerificationCode"; import UsageReport from "./UsageReport"; +import InvoiceReceipt from "./InvoiceReceipt"; // Source code via Vite ?raw imports import welcomeSource from "./WelcomeEmail.tsx?raw"; @@ -25,16 +26,18 @@ import shippingSource from "./ShippingUpdate.tsx?raw"; import productLaunchSource from "./ProductLaunch.tsx?raw"; import verificationSource from "./VerificationCode.tsx?raw"; import usageReportSource from "./UsageReport.tsx?raw"; +import invoiceSource from "./InvoiceReceipt.tsx?raw"; export interface TemplateEntry { id: string; name: string; description: string; - category: "onboarding" | "transactional" | "notification" | "security" | "marketing" | "ecommerce" | "newsletter" | "saas"; + category: "onboarding" | "transactional" | "notification" | "security" | "marketing" | "ecommerce" | "newsletter" | "saas" | "document"; inspiration: string; colorAccent: string; component: () => ReactElement; sourceCode: string; + mode?: "email" | "web" | "document"; } export const templates: TemplateEntry[] = [ @@ -148,6 +151,17 @@ export const templates: TemplateEntry[] = [ component: UsageReport, sourceCode: usageReportSource, }, + { + id: "invoice-receipt", + name: "Invoice / Receipt", + description: "Stripe-inspired print-ready invoice for PDF generation", + category: "document", + inspiration: "Stripe Invoicing", + colorAccent: "#635bff", + component: InvoiceReceipt, + sourceCode: invoiceSource, + mode: "document", + }, ]; export const templateMap = Object.fromEntries(