diff --git a/app/api/account/orders/[orderNumber]/invoice/route.ts b/app/api/account/orders/[orderNumber]/invoice/route.ts new file mode 100644 index 0000000..b170539 --- /dev/null +++ b/app/api/account/orders/[orderNumber]/invoice/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { getSessionCustomer, getCustomerOrderDetail } from "../../../../../lib/customerAuth"; +import { generateInvoicePdf } from "../../../../../lib/invoiceData"; + +// On-demand download for "Rechnung herunterladen" on +// /konto/bestellungen/[orderNumber] — reuses the exact same render call as +// the checkout-time attachment (app/lib/orderEmail.ts), so a re-download +// always matches what was emailed; invoiceNumber itself never changes +// (assigned once, server-side, at order creation — see Orders.ts). +export async function GET(request: Request, { params }: { params: Promise<{ orderNumber: string }> }) { + const session = await getSessionCustomer(); + if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 }); + + const { orderNumber } = await params; + const order = await getCustomerOrderDetail(session.token, session.customer.id, decodeURIComponent(orderNumber)); + if (!order) return NextResponse.json({ ok: false, reason: "Bestellung nicht gefunden." }, { status: 404 }); + if (!order.invoiceNumber || !order.invoiceIssuedAt) { + return NextResponse.json({ ok: false, reason: "Für diese Bestellung liegt noch keine Rechnung vor." }, { status: 404 }); + } + + const pdf = await generateInvoicePdf({ + orderNumber: order.orderNumber, + invoiceNumber: order.invoiceNumber, + invoiceIssuedAt: order.invoiceIssuedAt, + customerFirstName: order.customerFirstName, + customerLastName: order.customerLastName, + deliveryMethod: order.deliveryMethod, + street: order.street, + packstationNumber: order.packstationNumber, + postNumber: order.postNumber, + zip: order.zip, + city: order.city, + country: order.country, + items: order.items, + subtotal: order.subtotal, + shippingCost: order.shippingCost, + discountAmount: order.discountAmount, + discountCode: order.discountCode, + total: order.total, + }); + if (!pdf) return NextResponse.json({ ok: false, reason: "Rechnung konnte nicht erzeugt werden." }, { status: 500 }); + + return new NextResponse(new Uint8Array(pdf), { + status: 200, + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="Rechnung-${order.invoiceNumber}.pdf"`, + }, + }); +} diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts index 15dbe5a..9b16a0f 100644 --- a/app/api/checkout/route.ts +++ b/app/api/checkout/route.ts @@ -88,11 +88,12 @@ export async function POST(request: Request) { // Re-price everything server-side — never trust client-submitted prices. const productsBySlug = await fetchProductsBySlug(); - const items: { productId: number; productName: string; quantity: number; unitPrice: number }[] = []; + const items: { productId: number; productName: string; quantity: number; unitPrice: number; imageUrl: string | null }[] = []; for (const line of body.cart) { const product = productsBySlug.get(line.id); if (!product) return NextResponse.json({ ok: false, reason: "Ein Artikel im Warenkorb ist nicht mehr verfügbar." }, { status: 400 }); - items.push({ productId: product.id, productName: product.name, quantity: line.qty, unitPrice: product.price }); + const imageUrl = typeof product.image === "object" && product.image ? product.image.url : null; + items.push({ productId: product.id, productName: product.name, quantity: line.qty, unitPrice: product.price, imageUrl }); } const subtotal = items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0); @@ -164,7 +165,18 @@ export async function POST(request: Request) { { orderNumber: order.orderNumber, createdAt: order.createdAt, - items: items.map((i) => ({ productName: i.productName, quantity: i.quantity, unitPrice: i.unitPrice })), + invoiceNumber: order.invoiceNumber, + invoiceIssuedAt: order.invoiceIssuedAt, + customerFirstName: body.firstName, + customerLastName: body.lastName, + deliveryMethod: body.deliveryMethod, + street: body.street, + packstationNumber: body.packstationNumber, + postNumber: body.postNumber, + zip: body.zip, + city: body.city, + country: body.country, + items: items.map((i) => ({ productName: i.productName, quantity: i.quantity, unitPrice: i.unitPrice, imageUrl: i.imageUrl })), subtotal, shippingCost, discountAmount, diff --git a/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx b/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx index b05834a..8e97982 100644 --- a/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx +++ b/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx @@ -4,6 +4,8 @@ import { useLivePreview } from "@payloadcms/live-preview-react"; import { renderOrderConfirmationHtml, renderPasswordResetHtml, + renderOrderStatusHtml, + ORDER_STATUS_EMAIL_ICON, SAMPLE_ORDER, type EmailTemplateContent, } from "../../../lib/emailTemplates"; @@ -35,7 +37,14 @@ export function LiveEmailPreviewClient({ const html = type === "order-confirmation" ? renderOrderConfirmationHtml(data, SAMPLE_ORDER) - : renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token"); + : type === "password-reset" + ? renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token") + : renderOrderStatusHtml( + data, + ORDER_STATUS_EMAIL_ICON[type] ?? "✓", + SAMPLE_ORDER.orderNumber, + `https://einfach-produktiv.mk360.de/konto/bestellungen/${SAMPLE_ORDER.orderNumber}`, + ); return (
diff --git a/app/email-preview/[type]/page.tsx b/app/email-preview/[type]/page.tsx index 200ee89..adcc425 100644 --- a/app/email-preview/[type]/page.tsx +++ b/app/email-preview/[type]/page.tsx @@ -9,7 +9,21 @@ export const metadata: Metadata = { robots: { index: false, follow: false }, }; -const VALID_TYPES: EmailTemplateType[] = ["order-confirmation", "password-reset"]; +const VALID_TYPES: EmailTemplateType[] = [ + "order-confirmation", + "password-reset", + "order-shipped", + "order-cancelled", + "order-return-requested", + "order-returned", +]; + +const STATUS_TYPE_FALLBACK_HEADING: Record = { + "order-shipped": "Deine Bestellung ist unterwegs", + "order-cancelled": "Deine Bestellung wurde storniert", + "order-return-requested": "Deine Rücksendung wurde angefragt", + "order-returned": "Deine Retoure wurde bearbeitet", +}; // Entered exclusively via EmailTemplates.ts's admin.livePreview.url (a // Payload-admin-only iframe target, see buildPreviewUrl()/api/preview) — @@ -23,10 +37,16 @@ export default async function EmailPreviewPage({ params }: { params: Promise<{ t const emailType = type as EmailTemplateType; await draftMode(); + const fallbackHeading = + emailType === "order-confirmation" + ? "Vielen Dank für deine Bestellung!" + : emailType === "password-reset" + ? "Passwort zurücksetzen" + : STATUS_TYPE_FALLBACK_HEADING[emailType]; const template = (await getEmailTemplate(emailType, { draft: true })) ?? { type: emailType, subject: "", - heading: emailType === "order-confirmation" ? "Vielen Dank für deine Bestellung!" : "Passwort zurücksetzen", + heading: fallbackHeading, bodyText: "Noch kein Inhalt gespeichert — im Payload-Admin unter E-Mail-Vorlagen anlegen.", footerText: null, }; diff --git a/app/konto/bestellungen/[orderNumber]/page.tsx b/app/konto/bestellungen/[orderNumber]/page.tsx index 1f8ada4..a61a136 100644 --- a/app/konto/bestellungen/[orderNumber]/page.tsx +++ b/app/konto/bestellungen/[orderNumber]/page.tsx @@ -108,6 +108,15 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
+ {order.invoiceNumber && ( + + Rechnung herunterladen ({order.invoiceNumber}) + + )} + {action && } diff --git a/app/lib/customerAuth.ts b/app/lib/customerAuth.ts index e23dc96..afdc3cb 100644 --- a/app/lib/customerAuth.ts +++ b/app/lib/customerAuth.ts @@ -424,6 +424,8 @@ export async function getCustomerOrders(token: string, customerId: number): Prom export type CustomerOrderDetail = CustomerOrder & { id: number; + invoiceNumber: string | null; + invoiceIssuedAt: string | null; customerFirstName: string; customerLastName: string; customerEmail: string; diff --git a/app/lib/emailTemplates.ts b/app/lib/emailTemplates.ts index 431fe37..f29f730 100644 --- a/app/lib/emailTemplates.ts +++ b/app/lib/emailTemplates.ts @@ -116,7 +116,12 @@ function emailShell(icon: string, headingHtml: string, bodyHtml: string, footerT `; } -export type OrderConfirmationItem = { productName: string; quantity: number; unitPrice: number }; +export type OrderConfirmationItem = { + productName: string; + quantity: number; + unitPrice: number; + imageUrl?: string | null; +}; export type OrderConfirmationData = { orderNumber: string; createdAt: string; @@ -132,8 +137,13 @@ export const SAMPLE_ORDER: OrderConfirmationData = { orderNumber: "#EP-0001-A7K2", createdAt: new Date().toISOString(), items: [ - { productName: "ToDo-Karten – Set", quantity: 1, unitPrice: 12.9 }, - { productName: "Wochenplaner – Überblick", quantity: 2, unitPrice: 14.9 }, + { + productName: "ToDo-Karten – Set", + quantity: 1, + unitPrice: 12.9, + imageUrl: "https://payload.mk360.de/api/media/file/product-todo-karten.png", + }, + { productName: "Wochenplaner – Überblick", quantity: 2, unitPrice: 14.9, imageUrl: null }, ], subtotal: 42.7, shippingCost: 0, @@ -146,7 +156,14 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde const rows = order.items .map( (item) => ` - ${escapeHtml(item.productName)} × ${item.quantity} + + ${ + item.imageUrl + ? `` + : `
` + } + + ${escapeHtml(item.productName)} × ${item.quantity} ${formatPrice(item.quantity * item.unitPrice)} `, ) @@ -158,7 +175,7 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde const body = ` ${paragraphs(template.bodyText, "center")} - + ${rows}
Bestellnummer ${escapeHtml(order.orderNumber)} · ${formatDate(order.createdAt)}
Bestellnummer ${escapeHtml(order.orderNumber)} · ${formatDate(order.createdAt)}
@@ -177,6 +194,34 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde return emailShell("✓", escapeHtml(template.heading), body, template.footerText); } +// Icon shown per status — matches the Payload-side send exactly (see +// STATUS_EMAIL in the backend repo's src/collections/Orders.ts), kept here +// only for the Live Preview approximation (the real send happens from +// Payload itself, not this repo — see that file's own comment on why the +// two aren't pixel-identical, same established gap as password-reset). +export const ORDER_STATUS_EMAIL_ICON: Record = { + "order-shipped": "→", + "order-cancelled": "✕", + "order-return-requested": "↩", + "order-returned": "✓", +}; + +export function renderOrderStatusHtml(template: EmailTemplateContent, icon: string, orderNumber: string, orderUrl: string): string { + const body = ` + ${paragraphs(template.bodyText, "center")} +

Bestellnummer ${escapeHtml(orderNumber)}

+
+ + + +
+ Bestellung ansehen +
+ `; + + return emailShell(icon, escapeHtml(template.heading), body, template.footerText); +} + export function renderPasswordResetHtml(template: EmailTemplateContent, resetUrl: string): string { const body = ` ${paragraphs(template.bodyText, "center")} diff --git a/app/lib/invoiceData.ts b/app/lib/invoiceData.ts new file mode 100644 index 0000000..6d287fd --- /dev/null +++ b/app/lib/invoiceData.ts @@ -0,0 +1,15 @@ +import { getInvoiceSettings } from "./payload"; +import { renderInvoicePdf, type InvoiceOrder } from "./invoicePdf"; + +// Shared by app/lib/orderEmail.ts (checkout — attaches to the confirmation +// mail) and app/api/account/orders/[orderNumber]/invoice/route.ts (on-demand +// download) — same seller lookup + render call both times, so what a +// customer downloads later always matches what they were emailed. +export async function generateInvoicePdf(order: InvoiceOrder): Promise { + const seller = await getInvoiceSettings(); + if (!seller) { + console.error("generateInvoicePdf: no invoice-settings row found for tenant"); + return null; + } + return renderInvoicePdf(order, seller); +} diff --git a/app/lib/invoicePdf.tsx b/app/lib/invoicePdf.tsx new file mode 100644 index 0000000..b1aea84 --- /dev/null +++ b/app/lib/invoicePdf.tsx @@ -0,0 +1,218 @@ +import React from "react"; +import { Document, Page, View, Text, StyleSheet, renderToBuffer } from "@react-pdf/renderer"; +import { formatPrice, formatDate } from "./format"; + +// Generated synchronously in the checkout request (see app/lib/orderEmail.ts) +// and attached to the order-confirmation email, plus available on-demand via +// app/api/account/orders/[orderNumber]/invoice/route.ts — same render +// function both times, so a re-download always matches what was emailed. +// +// Built-in Helvetica, not a registered web font — this renders inside the +// checkout request's own fire-and-forget email step; a font-fetch failure +// there is one more way to lose the invoice attachment for no real design +// benefit. Same choice as the Payload-side correction-invoice PDF +// (src/lib/correctionInvoicePdf.tsx in the backend repo) — brand color via +// StyleSheet, not Georgia/Playfair. +const BRAND = "#f6a701"; +const TEXT_MUTED = "#6b6b69"; +const BORDER = "#e5e0d8"; + +const styles = StyleSheet.create({ + page: { padding: 48, fontSize: 10, fontFamily: "Helvetica", color: "#1a1a18" }, + wordmark: { fontFamily: "Helvetica-Bold", fontSize: 13, marginBottom: 32 }, + kindLabel: { fontFamily: "Helvetica-Bold", fontSize: 20, color: BRAND, marginBottom: 24 }, + addressRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 28 }, + addressBlock: { width: "45%" }, + addressLabel: { fontSize: 8, color: TEXT_MUTED, marginBottom: 4, textTransform: "uppercase" }, + addressLine: { fontSize: 10, lineHeight: 1.5 }, + metaRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 }, + metaLabel: { fontSize: 8, color: TEXT_MUTED }, + metaValue: { fontSize: 10, fontFamily: "Helvetica-Bold" }, + table: { marginTop: 8, borderTopWidth: 1, borderTopColor: BORDER }, + tableHeader: { flexDirection: "row", borderBottomWidth: 1, borderBottomColor: BORDER, paddingVertical: 6 }, + tableRow: { flexDirection: "row", borderBottomWidth: 1, borderBottomColor: BORDER, paddingVertical: 6 }, + colName: { flex: 3 }, + colQty: { flex: 1, textAlign: "right" }, + colPrice: { flex: 1, textAlign: "right" }, + colTotal: { flex: 1, textAlign: "right" }, + headerCell: { fontSize: 8, color: TEXT_MUTED, textTransform: "uppercase" }, + summary: { marginTop: 16, alignItems: "flex-end" }, + summaryRow: { flexDirection: "row", width: 220, justifyContent: "space-between", paddingVertical: 2 }, + summaryLabel: { fontSize: 10, color: TEXT_MUTED }, + summaryValue: { fontSize: 10 }, + grandTotalRow: { + flexDirection: "row", + width: 220, + justifyContent: "space-between", + paddingTop: 8, + marginTop: 6, + borderTopWidth: 1, + borderTopColor: BORDER, + }, + grandTotalLabel: { fontSize: 12, fontFamily: "Helvetica-Bold" }, + grandTotalValue: { fontSize: 12, fontFamily: "Helvetica-Bold" }, + footer: { + position: "absolute", + bottom: 40, + left: 48, + right: 48, + fontSize: 8, + color: TEXT_MUTED, + borderTopWidth: 1, + borderTopColor: BORDER, + paddingTop: 12, + }, +}); + +export type InvoiceItem = { productName: string; quantity: number; unitPrice: number }; + +export type InvoiceOrder = { + orderNumber: string; + invoiceNumber: string; + invoiceIssuedAt: string; + customerFirstName: string; + customerLastName: string; + deliveryMethod: "address" | "packstation"; + street?: string | null; + packstationNumber?: string | null; + postNumber?: string | null; + zip: string; + city: string; + country: string; + items: InvoiceItem[]; + subtotal: number; + shippingCost: number; + discountAmount: number; + discountCode: string | null; + total: number; +}; + +export type InvoiceSeller = { + sellerName: string; + sellerStreet: string; + sellerZip: string; + sellerCity: string; + sellerCountry: string; + sellerEmail: string; + vatId: string; + taxRatePercent: number; + bankDetails?: string | null; +}; + +function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller: InvoiceSeller }) { + const rate = seller.taxRatePercent; + const grossTotal = order.total; + const netTotal = grossTotal / (1 + rate / 100); + const taxTotal = grossTotal - netTotal; + const deliveryLine = + order.deliveryMethod === "address" ? order.street : `Packstation ${order.packstationNumber} · Postnummer ${order.postNumber}`; + + return ( + + + einfach produktiv. + Rechnung + + + + Von + {seller.sellerName} + {seller.sellerStreet} + + {seller.sellerZip} {seller.sellerCity} + + {seller.sellerCountry} + + + An + + {order.customerFirstName} {order.customerLastName} + + {deliveryLine} + + {order.zip} {order.city} + + {order.country} + + + + + + Rechnungs-Nr. + {order.invoiceNumber} + + + Datum + {formatDate(order.invoiceIssuedAt)} + + + Bestellnummer + {order.orderNumber} + + + USt-IdNr. + {seller.vatId} + + + + + + Artikel + Menge + Einzelpreis + Betrag + + {order.items.map((item, i) => ( + + {item.productName} + {item.quantity} + {formatPrice(item.unitPrice)} + {formatPrice(item.quantity * item.unitPrice)} + + ))} + + + + + Zwischensumme + {formatPrice(order.subtotal)} + + {order.discountAmount > 0 && ( + + Rabatt{order.discountCode ? ` (${order.discountCode})` : ""} + -{formatPrice(order.discountAmount)} + + )} + + Versand + {order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)} + + + Netto + {formatPrice(netTotal)} + + + zzgl. {rate}% MwSt. + {formatPrice(taxTotal)} + + + Gesamt + {formatPrice(grossTotal)} + + + + + + {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "} + {seller.vatId} + + {seller.bankDetails ? {seller.bankDetails} : null} + + + + ); +} + +export async function renderInvoicePdf(order: InvoiceOrder, seller: InvoiceSeller): Promise { + return renderToBuffer(); +} diff --git a/app/lib/orderEmail.ts b/app/lib/orderEmail.ts index fb0d499..4c7fc6b 100644 --- a/app/lib/orderEmail.ts +++ b/app/lib/orderEmail.ts @@ -1,6 +1,26 @@ import { transport } from "./mailer"; import { getEmailTemplate } from "./payload"; import { renderOrderConfirmationHtml, type OrderConfirmationData } from "./emailTemplates"; +import { generateInvoicePdf } from "./invoiceData"; +import { sendCriticalAlert } from "./alertAdmin"; + +// Superset of OrderConfirmationData (used for the HTML render) plus the +// address/invoice fields the PDF needs but the email body doesn't — +// avoids threading a second parallel "order" argument through this +// function for what's ultimately one order. +export type OrderConfirmationEmailData = OrderConfirmationData & { + invoiceNumber: string; + invoiceIssuedAt: string; + customerFirstName: string; + customerLastName: string; + deliveryMethod: "address" | "packstation"; + street?: string | null; + packstationNumber?: string | null; + postNumber?: string | null; + zip: string; + city: string; + country: string; +}; // Called from app/api/checkout/route.ts right after a successful // createOrder() — fire-and-forget, must never block or fail the checkout @@ -9,7 +29,15 @@ import { renderOrderConfirmationHtml, type OrderConfirmationData } from "./email // nobody's visited the Payload admin yet (mirrors seed-email-templates.ts's // defaults, kept in sync by hand — there are only two places this wording // lives, seeding it is a one-time setup step, not a runtime dependency). -export async function sendOrderConfirmationEmail(order: OrderConfirmationData, customerEmail: string): Promise { +// +// The invoice PDF is generated here too (not a separate fire-and-forget +// step) so it can ride along as an attachment on this same send, per the +// "always emailed, not just downloadable" checkout decision — but its own +// failure must not sink the confirmation email itself, so it's wrapped in +// its own try/catch and just sends without the attachment if generation +// fails (still alerted, same severity as the frontend's own critical-error +// path for this checkout flow). +export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailData, customerEmail: string): Promise { const template = (await getEmailTemplate("order-confirmation")) ?? { subject: "Bestellt! Deine Ruhe kann kommen 🎉", heading: "Geschafft!", @@ -18,11 +46,45 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationData, c }; const html = renderOrderConfirmationHtml(template, order); + + let attachments: { filename: string; content: Buffer }[] | undefined; + try { + const pdf = await generateInvoicePdf({ + orderNumber: order.orderNumber, + invoiceNumber: order.invoiceNumber, + invoiceIssuedAt: order.invoiceIssuedAt, + customerFirstName: order.customerFirstName, + customerLastName: order.customerLastName, + deliveryMethod: order.deliveryMethod, + street: order.street, + packstationNumber: order.packstationNumber, + postNumber: order.postNumber, + zip: order.zip, + city: order.city, + country: order.country, + items: order.items, + subtotal: order.subtotal, + shippingCost: order.shippingCost, + discountAmount: order.discountAmount, + discountCode: order.discountCode, + total: order.total, + }); + if (pdf) attachments = [{ filename: `Rechnung-${order.invoiceNumber}.pdf`, content: pdf }]; + else throw new Error("generateInvoicePdf returned null (missing invoice-settings?)"); + } catch (err) { + sendCriticalAlert("Rechnungs-PDF konnte nicht erzeugt werden", { + orderNumber: order.orderNumber, + invoiceNumber: order.invoiceNumber, + error: String(err), + }); + } + await transport.sendMail({ from: '"einfach produktiv" ', to: customerEmail, subject: template.subject, html, + attachments, }); return true; } diff --git a/app/lib/orderServer.ts b/app/lib/orderServer.ts index 9f7e9fc..89c8e97 100644 --- a/app/lib/orderServer.ts +++ b/app/lib/orderServer.ts @@ -49,7 +49,7 @@ export type CreateOrderInput = { total: number; }; -export type CreatedOrder = { orderNumber: string; createdAt: string }; +export type CreatedOrder = { orderNumber: string; createdAt: string; invoiceNumber: string; invoiceIssuedAt: string }; export async function createOrder(input: CreateOrderInput): Promise { const tenantId = await resolveTenantId(); @@ -99,6 +99,11 @@ export async function createOrder(input: CreateOrderInput): Promise { + const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, limit: "1" }); + const res = await fetch(`${PAYLOAD_URL}/api/invoice-settings?${params}`, { + headers: { "x-order-service-secret": process.env.ORDER_SERVICE_SECRET || "" }, + cache: "no-store", + }); + if (!res.ok) { + console.error(`getInvoiceSettings: Payload returned ${res.status} ${res.statusText}`); + return null; + } + const data: { docs?: InvoiceSettings[] } = await res.json(); + return data.docs?.[0] ?? null; +} diff --git a/app/lib/productsServer.ts b/app/lib/productsServer.ts index e48751c..ae090ad 100644 --- a/app/lib/productsServer.ts +++ b/app/lib/productsServer.ts @@ -7,10 +7,20 @@ const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de"; const TENANT_SLUG = "einfach-produktiv"; -export type RawProduct = { id: number; slug: string; name: string; price: number; active: boolean }; +export type RawProduct = { + id: number; + slug: string; + name: string; + price: number; + active: boolean; + image: { url: string } | number | null; +}; export async function fetchProductsBySlug(): Promise> { - const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, limit: "100" }); + // depth=1 so `image` resolves to { url } instead of just the media id — + // needed for the order-confirmation email's product thumbnails + // (app/lib/orderEmail.ts), not just re-pricing. + const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, depth: "1", limit: "100" }); const res = await fetch(`${PAYLOAD_URL}/api/products?${params}`, { cache: "no-store" }); const map = new Map(); if (!res.ok) return map; diff --git a/package-lock.json b/package-lock.json index b39e0c8..448803a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@payloadcms/live-preview-react": "^3.85.2", + "@react-pdf/renderer": "^4.5.1", "motion": "^12.42.2", "next": "16.2.9", "nodemailer": "^9.0.3", @@ -232,6 +233,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -1262,6 +1272,30 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1329,6 +1363,183 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1" } }, + "node_modules/@react-pdf/fns": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.3.tgz", + "integrity": "sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.8.tgz", + "integrity": "sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^5.1.1", + "@react-pdf/types": "^2.11.1", + "fontkit": "^2.0.2", + "is-url": "^1.2.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.1.0.tgz", + "integrity": "sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g==", + "license": "MIT", + "dependencies": { + "@react-pdf/svg": "^1.1.0", + "jay-peg": "^1.1.1", + "png-js": "^2.0.0" + } + }, + "node_modules/@react-pdf/layout": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.6.1.tgz", + "integrity": "sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/image": "^3.1.0", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/stylesheet": "^6.2.1", + "@react-pdf/textkit": "^6.3.0", + "@react-pdf/types": "^2.11.1", + "emoji-regex-xs": "^1.0.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-5.1.1.tgz", + "integrity": "sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "browserify-zlib": "^0.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^2.0.0", + "vite-compatible-readable-stream": "^3.6.1" + } + }, + "node_modules/@react-pdf/primitives": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.3.0.tgz", + "integrity": "sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==", + "license": "MIT" + }, + "node_modules/@react-pdf/reconciler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-2.0.0.tgz", + "integrity": "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "scheduler": "0.25.0-rc-603e6108-20241029" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/reconciler/node_modules/scheduler": { + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", + "license": "MIT" + }, + "node_modules/@react-pdf/render": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.5.1.tgz", + "integrity": "sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/textkit": "^6.3.0", + "@react-pdf/types": "^2.11.1", + "abs-svg-path": "^0.1.1", + "color-string": "^2.1.4", + "normalize-svg-path": "^1.1.0", + "parse-svg-path": "^0.1.2", + "svg-arc-to-cubic-bezier": "^3.2.0" + } + }, + "node_modules/@react-pdf/renderer": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.5.1.tgz", + "integrity": "sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/font": "^4.0.8", + "@react-pdf/layout": "^4.6.1", + "@react-pdf/pdfkit": "^5.1.1", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/reconciler": "^2.0.0", + "@react-pdf/render": "^4.5.1", + "@react-pdf/types": "^2.11.1", + "events": "^3.3.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "queue": "^6.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/stylesheet": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.2.1.tgz", + "integrity": "sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/types": "^2.11.1", + "color-string": "^2.1.4", + "hsl-to-hex": "^1.0.0", + "media-engine": "^1.0.3", + "postcss-value-parser": "^4.1.0" + } + }, + "node_modules/@react-pdf/svg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/svg/-/svg-1.1.0.tgz", + "integrity": "sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==", + "license": "MIT", + "dependencies": { + "@react-pdf/primitives": "^4.3.0" + } + }, + "node_modules/@react-pdf/textkit": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.3.0.tgz", + "integrity": "sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.11.1.tgz", + "integrity": "sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.8", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/stylesheet": "^6.2.1" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2349,6 +2560,12 @@ "win32" ] }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2642,6 +2859,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.40", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", @@ -2654,6 +2891,15 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -2678,6 +2924,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.28.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", @@ -2815,6 +3079,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2835,6 +3108,27 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3003,6 +3297,12 @@ "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3045,6 +3345,12 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", @@ -3687,11 +3993,19 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3748,6 +4062,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3812,6 +4132,23 @@ "dev": true, "license": "ISC" }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4163,6 +4500,27 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hsl-to-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", + "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", + "license": "MIT", + "dependencies": { + "hsl-to-rgb-for-reals": "^1.1.0" + } + }, + "node_modules/hsl-to-rgb-for-reals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", + "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", + "license": "ISC" + }, + "node_modules/hyphen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", + "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4200,6 +4558,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4600,6 +4964,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -4678,6 +5048,15 @@ "node": ">= 0.4" } }, + "node_modules/jay-peg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", + "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", + "license": "MIT", + "dependencies": { + "restructure": "^3.0.0" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4688,11 +5067,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5098,6 +5482,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5125,7 +5528,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5164,6 +5566,12 @@ "node": ">= 0.4" } }, + "node_modules/media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5419,11 +5827,19 @@ "node": ">=6.0.0" } }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5610,6 +6026,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5623,6 +6045,12 @@ "node": ">=6" } }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5669,6 +6097,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/png-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-2.0.0.tgz", + "integrity": "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==", + "dependencies": { + "fflate": "^0.8.2" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5708,6 +6144,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5722,7 +6164,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -5740,6 +6181,15 @@ "node": ">=6" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5786,7 +6236,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/reflect.getprototypeof": { @@ -5833,6 +6282,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5877,6 +6335,12 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5932,6 +6396,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6219,6 +6703,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6405,6 +6898,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, "node_modules/tailwindcss": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", @@ -6426,6 +6925,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6687,6 +7192,32 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -6766,6 +7297,26 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite-compatible-readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", + "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6901,6 +7452,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index c7f2ac4..16753c2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@payloadcms/live-preview-react": "^3.85.2", + "@react-pdf/renderer": "^4.5.1", "motion": "^12.42.2", "next": "16.2.9", "nodemailer": "^9.0.3",