Wire up Products.layout (PDP page builder) frontend
New ProductBlocks.tsx renders a product's layout — delegates to PageBlocks.tsx's renderPageBlockSync for the 9 block types shared with Pages.layout, handles productHero/productPricingPanel itself (need live product/shipping/tax context a generic content page doesn't have). Converts /einfach-anfangen to render through this instead of its own Hero/HowItWorks/Focus/Pricing components (now deleted) — the first PDP built as CMS blocks end to end. der-eine/todo-cards/tasse-die-pause are untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
This commit is contained in:
@@ -269,6 +269,12 @@ export type Product = {
|
||||
// related product's own relatedProduct is never resolved/shown) — see
|
||||
// mapPayloadProduct's own comment.
|
||||
relatedProduct: Product | null;
|
||||
// Optional PDP page-builder content — see Products.ts's own `layout`
|
||||
// field comment. Empty for every existing hand-coded PDP (der-eine,
|
||||
// todo-cards, tasse-die-pause); a product's own page.tsx decides whether
|
||||
// to render this or its bespoke components, this type doesn't enforce
|
||||
// either.
|
||||
layout: ProductBlock[];
|
||||
};
|
||||
|
||||
type PayloadProduct = {
|
||||
@@ -276,6 +282,7 @@ type PayloadProduct = {
|
||||
name: string;
|
||||
subline: string | null;
|
||||
slug: string;
|
||||
layout: PayloadProductBlock[] | null;
|
||||
description: unknown | null;
|
||||
sku: string | null;
|
||||
price: number;
|
||||
@@ -399,6 +406,7 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
|
||||
// this nested object's OWN relatedProduct is never populated (stays a
|
||||
// raw id or absent), so the recursion bottoms out after exactly one level.
|
||||
relatedProduct: typeof product.relatedProduct === "object" && product.relatedProduct ? mapPayloadProduct(product.relatedProduct) : null,
|
||||
layout: (product.layout ?? []).map(mapPayloadProductBlock),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1318,6 +1326,64 @@ export async function getTrackingCodes(): Promise<TrackingCode[]> {
|
||||
return Array.isArray(data.docs) ? data.docs : [];
|
||||
}
|
||||
|
||||
// ── Products.layout (PDP page builder) ─────────────────────────────────────
|
||||
// Shares 9 of PageBlock's own block types verbatim (richTextSection/
|
||||
// stepRow/quote/image/icon/pillList/checklistImage/table/ctaCard — same
|
||||
// Payload Block configs, reused a second time on Products.layout, see that
|
||||
// field's own comment) plus two product-specific ones that don't exist on
|
||||
// Pages: productHero (the one PDP hero variant for now — just a body
|
||||
// richText, everything else about the hero comes from the product itself)
|
||||
// and productPricingPanel (the closing buy panel, no fields of its own).
|
||||
|
||||
export type ProductBlock =
|
||||
| { blockType: "productHero"; id: string; body: unknown | null }
|
||||
| { blockType: "productPricingPanel"; id: string }
|
||||
| { blockType: "richTextSection"; id: string; content: unknown }
|
||||
| { blockType: "stepRow"; id: string; items: { id: string; icon: string; title: string; subtitle: string | null; description: string }[] }
|
||||
| { blockType: "quote"; id: string; text: string; label: string | null }
|
||||
| { blockType: "image"; id: string; image: string | null; caption: string | null }
|
||||
| { blockType: "icon"; id: string; icon: string }
|
||||
| { blockType: "pillList"; id: string; items: { id: string; label: string }[] }
|
||||
| { blockType: "checklistImage"; id: string; image: string | null; items: { id: string; text: string }[] }
|
||||
| { blockType: "table"; id: string; labelHeader: string; valueHeader: string; rows: { id: string; label: string; value: string }[] }
|
||||
| { blockType: "ctaCard"; id: string; eyebrow: string; title: string; description: string | null; href: string };
|
||||
|
||||
type PayloadProductBlock =
|
||||
| { blockType: "productHero"; id: string; body?: unknown | null }
|
||||
| { blockType: "productPricingPanel"; id: string }
|
||||
| { blockType: "richTextSection"; id: string; content: unknown }
|
||||
| { blockType: "stepRow"; id: string; items: { id: string; icon: string; title: string; subtitle?: string | null; description: string }[] }
|
||||
| { blockType: "quote"; id: string; text: string; label?: string | null }
|
||||
| { blockType: "image"; id: string; image: PayloadPageImageField; caption?: string | null }
|
||||
| { blockType: "icon"; id: string; icon: string }
|
||||
| { blockType: "pillList"; id: string; items: { id: string; label: string }[] }
|
||||
| { blockType: "checklistImage"; id: string; image: PayloadPageImageField; items: { id: string; text: string }[] }
|
||||
| { blockType: "table"; id: string; labelHeader: string; valueHeader: string; rows: { id: string; label: string; value: string }[] }
|
||||
| { blockType: "ctaCard"; id: string; eyebrow: string; title: string; description?: string | null; href: string };
|
||||
|
||||
function mapPayloadProductBlock(block: PayloadProductBlock): ProductBlock {
|
||||
switch (block.blockType) {
|
||||
case "productHero":
|
||||
return { blockType: "productHero", id: block.id, body: block.body ?? null };
|
||||
case "image":
|
||||
return { blockType: "image", id: block.id, image: mapPayloadPageImage(block.image), caption: block.caption ?? null };
|
||||
case "checklistImage":
|
||||
return { blockType: "checklistImage", id: block.id, image: mapPayloadPageImage(block.image), items: block.items };
|
||||
case "quote":
|
||||
return { blockType: "quote", id: block.id, text: block.text, label: block.label ?? null };
|
||||
case "ctaCard":
|
||||
return { blockType: "ctaCard", id: block.id, eyebrow: block.eyebrow, title: block.title, description: block.description ?? null, href: block.href };
|
||||
case "stepRow":
|
||||
return {
|
||||
blockType: "stepRow",
|
||||
id: block.id,
|
||||
items: block.items.map((item) => ({ ...item, subtitle: item.subtitle ?? null })),
|
||||
};
|
||||
default:
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pages (generic slug-routed content pages / page builder) ──────────────
|
||||
// Powers app/[slug]/page.tsx — the first generic catch-all route in this
|
||||
// repo (every other content page today is its own static directory).
|
||||
|
||||
Reference in New Issue
Block a user