From a1b0dffff7a04211a2139ef792e5e813ba0bb23b Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 26 Aug 2026 22:40:12 +0000 Subject: [PATCH] Render the 6 new blog-post blocks (StepRow/Icon/PillList/ChecklistImage/Table/CtaCard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows Posts.content's BlocksFeature update (docker/payload commit 5426a02) — mirrors PageBlocks.tsx's own JSX for each block field-for-field since it's the same Block config reused a second time. Works in both the server-rendered page and the client-side Live Preview renderer (shared converters), unlike testimonialsRef which needs an async fetch and stays page-builder only. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt --- app/components/RichText.tsx | 135 ++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/app/components/RichText.tsx b/app/components/RichText.tsx index aa4daf7..970c11d 100644 --- a/app/components/RichText.tsx +++ b/app/components/RichText.tsx @@ -1,7 +1,10 @@ import Image from "next/image"; +import Link from "next/link"; import { RichText as LexicalRichText, type JSXConvertersFunction } from "@payloadcms/richtext-lexical/react"; import type { TOCSection } from "./SectionTOC"; import { QuoteLabel } from "./QuoteLabel"; +import { StepArrow } from "./StepArrow"; +import { STEP_ICONS, SYMBOLIC_ICONS } from "./icons/StepIcons"; // Switched 2026-07-24 from a small hand-rolled Lexical JSON->JSX walker to // Payload's own official React renderer + custom JSXConverters — needed @@ -89,12 +92,29 @@ type ImageBlockFields = { image: MediaRef; caption?: string | null }; type ImageGalleryBlockFields = { images: { image: MediaRef; caption?: string | null }[] }; type VideoEmbedBlockFields = { url: string; caption?: string | null }; type QuoteBlockFields = { text: string; label?: string | null }; +type IconBlockFields = { icon: string }; +type PillListBlockFields = { items: { id?: string | null; label: string }[] }; +type ChecklistImageBlockFields = { image: MediaRef; items: { id?: string | null; text: string }[] }; +type TableBlockFields = { labelHeader: string; valueHeader: string; rows: { id?: string | null; label: string; value: string }[] }; +type StepRowBlockFields = { + items: { id?: string | null; icon: string; title: string; subtitle?: string | null; description: string }[]; +}; +type CtaCardBlockFields = { eyebrow: string; title: string; description?: string | null; href: string }; function BlockCaption({ caption }: { caption?: string | null }) { if (!caption) return null; return

{caption}

; } +// Same checkmark as PageBlocks.tsx's own — see that file's comment. +function CheckIcon() { + return ( + + + + ); +} + // Same visual treatment as the QuoteBlock converter below (and the native // blockquote case it replaces going forward) — see that converter's own // comment for why both still exist. @@ -250,6 +270,121 @@ function buildConverters(quoteLabel: string): JSXConvertersFunction { ); }, + // Same page-builder blocks as PageBlocks.tsx (Pages.layout) — see + // that file's own comment on each block's visual treatment, mirrored + // here field-for-field since it's the exact same Block config + // registered a second time (Posts.content's BlocksFeature) so blog + // posts can use them mid-article too. `testimonialsRef` is the one + // page-builder block deliberately NOT included: it needs an async + // data fetch, and this converter set is shared with LiveRichText's + // client-side live-preview rendering (see blog's LivePostContent.tsx), + // where an async component would break — PageBlocks.tsx only gets + // away with it by awaiting outside the sync per-block switch, in its + // own async server component. + icon: ({ node }: { node: { fields: unknown } }) => { + const fields = node.fields as IconBlockFields; + const SymbolIcon = SYMBOLIC_ICONS[fields.icon]; + return SymbolIcon ?
{SymbolIcon()}
: null; + }, + pillList: ({ node }: { node: { fields: unknown } }) => { + const fields = node.fields as PillListBlockFields; + return ( +
+ {fields.items.map((item, i) => ( + + {item.label} + + ))} +
+ ); + }, + checklistImage: ({ node }: { node: { fields: unknown } }) => { + const fields = node.fields as ChecklistImageBlockFields; + const url = mediaUrl(fields.image); + return ( +
+ {url && ( +
+ +
+ )} +
    + {fields.items.map((item, i) => ( +
  • + +

    {item.text}

    +
  • + ))} +
+
+ ); + }, + table: ({ node }: { node: { fields: unknown } }) => { + const fields = node.fields as TableBlockFields; + return ( +
+ + + + + + + + + {fields.rows.map((row, i) => ( + + + + + ))} + +
{fields.labelHeader}{fields.valueHeader}
{row.label}{row.value}
+
+ ); + }, + stepRow: ({ node }: { node: { fields: unknown } }) => { + const fields = node.fields as StepRowBlockFields; + const icons = fields.items.map((item) => STEP_ICONS[item.icon]); + return ( +
+ {fields.items.flatMap((item, i) => [ +
+
+ {icons[i]?.()} +
+
+

{item.title}

+ {item.subtitle &&

{item.subtitle}

} +

{item.description}

+
+
, + i < fields.items.length - 1 ? ( +
+ +
+ ) : null, + ])} +
+ ); + }, + ctaCard: ({ node }: { node: { fields: unknown } }) => { + const fields = node.fields as CtaCardBlockFields; + return ( + +
+

{fields.eyebrow}

+

{fields.title}

+ {fields.description &&

{fields.description}

} +
+ + + ); + }, }, }); }