Wire up real Live Preview for block-driven PDPs
Products.ts's admin.livePreview (added earlier this session) only made the admin UI show a preview iframe — the actual page had no mechanism to receive postMessage updates and re-render with unsaved draft data, unlike Pages/Posts which already have this (LivePageContent.tsx/ LivePostContent.tsx). Edits in the backend were invisible in the preview until an actual save (reported 2026-08-30). New LiveProductContent.tsx mirrors LivePageContent.tsx's pattern exactly: useLivePreview<PayloadProduct> + mapPayloadProduct, rendering via ProductBlocks.tsx's now-exported renderProductBlock (sync) instead of the full async ProductBlocks wrapper. testimonialsRef/crossSell skipped in the live subset (need an async fetch a client component can't perform inline) — same "editable subset only" scope Pages/Posts already accept. shipping/tax/Kleinunternehmer context is fetched once via the newly-exported getProductBlockContext() and passed in rather than re-fetched per keystroke. /der-eine and /tasse-die-pause now check draftMode() and swap in LiveProductContent instead of the normal async ProductBlocks when Payload's Live Preview iframe has it enabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useLivePreview } from "@payloadcms/live-preview-react";
|
||||
import { renderProductBlock, type ProductBlockContext } from "./ProductBlocks";
|
||||
import { mapPayloadProduct, type PayloadProduct, type Product } from "../lib/payload";
|
||||
|
||||
const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://payload.mk360.de";
|
||||
|
||||
// Live-previewable subset of a Products document's `layout` — same
|
||||
// "editable subset only" scope as LivePageContent.tsx/LivePostContent.tsx:
|
||||
// `testimonialsRef` and `crossSell` are skipped here (both need an async
|
||||
// fetch a client component can't perform inline — testimonialsRef isn't
|
||||
// even registered on Products.layout anymore, kept out of the switch for
|
||||
// the same reason; crossSell's related-product card just doesn't update
|
||||
// live, shows correctly once saved and reloaded outside the iframe).
|
||||
// shipping/tax/Kleinunternehmer are passed in once from the server page
|
||||
// component instead of re-fetched on every keystroke — see
|
||||
// getProductBlockContext's own comment.
|
||||
export function LiveProductContent({
|
||||
initialProduct,
|
||||
shipping,
|
||||
taxRate,
|
||||
kleinunternehmer,
|
||||
}: {
|
||||
initialProduct: Product;
|
||||
} & Omit<ProductBlockContext, "product">) {
|
||||
const { data } = useLivePreview<PayloadProduct>({
|
||||
initialData: initialProduct as unknown as PayloadProduct,
|
||||
serverURL: PAYLOAD_URL,
|
||||
depth: 2,
|
||||
});
|
||||
const product = data?.slug ? mapPayloadProduct(data) : initialProduct;
|
||||
const ctx: ProductBlockContext = { product, shipping, taxRate, kleinunternehmer };
|
||||
|
||||
return <>{product.layout.map((block) => <div key={block.id}>{renderProductBlock(block, ctx)}</div>)}</>;
|
||||
}
|
||||
@@ -30,12 +30,7 @@ import { TestimonialsGrid } from "./TestimonialsGrid";
|
||||
// need the live product/shipping/tax context that a generic content page
|
||||
// doesn't have.
|
||||
export async function ProductBlocks({ product }: { product: Product }) {
|
||||
const [shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
|
||||
getShippingSettings(),
|
||||
getDefaultTaxRatePercent(),
|
||||
getKleinunternehmer(),
|
||||
]);
|
||||
const ctx: ProductBlockContext = { product, shipping, taxRate: effectiveTaxRate(product, defaultTaxRate), kleinunternehmer };
|
||||
const ctx = await getProductBlockContext(product);
|
||||
|
||||
const rendered = await Promise.all(
|
||||
product.layout.map(async (block) => {
|
||||
@@ -114,14 +109,28 @@ export async function ProductBlocks({ product }: { product: Product }) {
|
||||
return <>{rendered}</>;
|
||||
}
|
||||
|
||||
type ProductBlockContext = {
|
||||
export type ProductBlockContext = {
|
||||
product: Product;
|
||||
shipping: Awaited<ReturnType<typeof getShippingSettings>>;
|
||||
taxRate: number;
|
||||
kleinunternehmer: boolean;
|
||||
};
|
||||
|
||||
function renderProductBlock(block: ProductBlock, ctx: ProductBlockContext): React.ReactNode {
|
||||
// Split out so LiveProductContent.tsx (the "use client" live-preview
|
||||
// counterpart, same pattern as LivePageContent.tsx/renderPageBlockSync)
|
||||
// can fetch this once itself instead of duplicating the three calls —
|
||||
// shipping/tax/Kleinunternehmer status essentially never change between
|
||||
// keystrokes in a preview session, unlike the product doc itself.
|
||||
export async function getProductBlockContext(product: Product): Promise<ProductBlockContext> {
|
||||
const [shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
|
||||
getShippingSettings(),
|
||||
getDefaultTaxRatePercent(),
|
||||
getKleinunternehmer(),
|
||||
]);
|
||||
return { product, shipping, taxRate: effectiveTaxRate(product, defaultTaxRate), kleinunternehmer };
|
||||
}
|
||||
|
||||
export function renderProductBlock(block: ProductBlock, ctx: ProductBlockContext): React.ReactNode {
|
||||
switch (block.blockType) {
|
||||
case "productHero":
|
||||
return <ProductHero {...ctx} body={block.body} />;
|
||||
|
||||
Reference in New Issue
Block a user