Files
klz-cables.com/components/PayloadRichText.tsx
2026-02-24 15:52:16 +01:00

219 lines
7.8 KiB
TypeScript

import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
import Image from 'next/image';
// Import all custom React components that were previously mapped via MDX
import StickyNarrative from '@/components/blog/StickyNarrative';
import ComparisonGrid from '@/components/blog/ComparisonGrid';
import VisualLinkPreview from '@/components/blog/VisualLinkPreview';
import TechnicalGrid from '@/components/blog/TechnicalGrid';
import HighlightBox from '@/components/blog/HighlightBox';
import AnimatedImage from '@/components/blog/AnimatedImage';
import ChatBubble from '@/components/blog/ChatBubble';
import PowerCTA from '@/components/blog/PowerCTA';
import { Callout } from '@/components/ui/Callout';
import Stats from '@/components/blog/Stats';
import SplitHeading from '@/components/blog/SplitHeading';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
const jsxConverters: JSXConverters = {
...defaultJSXConverters,
blocks: {
// Map the custom Payload Blocks created in src/payload/blocks to their React components
// Payload Lexical exposes blocks using the 'block-[slug]' pattern
stickyNarrative: ({ node }: any) => (
<StickyNarrative title={node.fields.title} items={node.fields.items} />
),
'block-stickyNarrative': ({ node }: any) => (
<StickyNarrative title={node.fields.title} items={node.fields.items} />
),
comparisonGrid: ({ node }: any) => (
<ComparisonGrid
title={node.fields.title}
leftLabel={node.fields.leftLabel}
rightLabel={node.fields.rightLabel}
items={node.fields.items}
/>
),
'block-comparisonGrid': ({ node }: any) => (
<ComparisonGrid
title={node.fields.title}
leftLabel={node.fields.leftLabel}
rightLabel={node.fields.rightLabel}
items={node.fields.items}
/>
),
visualLinkPreview: ({ node }: any) => (
<VisualLinkPreview
url={node.fields.url}
title={node.fields.title}
summary={node.fields.summary}
image={node.fields.image?.sizes?.card?.url || node.fields.image?.url}
/>
),
'block-visualLinkPreview': ({ node }: any) => (
<VisualLinkPreview
url={node.fields.url}
title={node.fields.title}
summary={node.fields.summary}
image={node.fields.image?.sizes?.card?.url || node.fields.image?.url}
/>
),
technicalGrid: ({ node }: any) => (
<TechnicalGrid title={node.fields.title} items={node.fields.items} />
),
'block-technicalGrid': ({ node }: any) => {
console.log('[PayloadRichText] Rendering block-technicalGrid:', node.fields.title);
return <TechnicalGrid title={node.fields.title} items={node.fields.items} />;
},
highlightBox: ({ node }: any) => (
<HighlightBox title={node.fields.title} color={node.fields.color}>
<RichText data={node.fields.content} converters={jsxConverters} />
</HighlightBox>
),
'block-highlightBox': ({ node }: any) => (
<HighlightBox title={node.fields.title} color={node.fields.color}>
<RichText data={node.fields.content} converters={jsxConverters} />
</HighlightBox>
),
animatedImage: ({ node }: any) => (
<AnimatedImage
src={node.fields.src}
alt={node.fields.alt}
width={node.fields.width}
height={node.fields.height}
/>
),
'block-animatedImage': ({ node }: any) => (
<AnimatedImage
src={node.fields.src}
alt={node.fields.alt}
width={node.fields.width}
height={node.fields.height}
/>
),
chatBubble: ({ node }: any) => (
<ChatBubble
author={node.fields.author}
avatar={node.fields.avatar}
role={node.fields.role}
align={node.fields.align}
>
<RichText data={node.fields.content} converters={jsxConverters} />
</ChatBubble>
),
'block-chatBubble': ({ node }: any) => (
<ChatBubble
author={node.fields.author}
avatar={node.fields.avatar}
role={node.fields.role}
align={node.fields.align}
>
<RichText data={node.fields.content} converters={jsxConverters} />
</ChatBubble>
),
powerCTA: ({ node }: any) => <PowerCTA locale={node.fields.locale} />,
'block-powerCTA': ({ node }: any) => <PowerCTA locale={node.fields.locale} />,
callout: ({ node }: any) => (
<Callout type={node.fields.type} title={node.fields.title}>
<RichText data={node.fields.content} converters={jsxConverters} />
</Callout>
),
'block-callout': ({ node }: any) => (
<Callout type={node.fields.type} title={node.fields.title}>
<RichText data={node.fields.content} converters={jsxConverters} />
</Callout>
),
stats: ({ node }: any) => <Stats stats={node.fields.stats} />,
'block-stats': ({ node }: any) => <Stats stats={node.fields.stats} />,
splitHeading: ({ node }: any) => (
<SplitHeading id={node.fields.id} level={node.fields.level}>
{node.fields.title}
</SplitHeading>
),
'block-splitHeading': ({ node }: any) => (
<SplitHeading id={node.fields.id} level={node.fields.level}>
{node.fields.title}
</SplitHeading>
),
productTabs: ({ node }: any) => (
<ProductTabs
technicalData={
<ProductTechnicalData
data={{
technicalItems: node.fields.technicalItems,
voltageTables: node.fields.voltageTables,
}}
/>
}
>
<></>
</ProductTabs>
),
'block-productTabs': ({ node }: any) => (
<ProductTabs
technicalData={
<ProductTechnicalData
data={{
technicalItems: node.fields.technicalItems,
voltageTables: node.fields.voltageTables,
}}
/>
}
>
{node.fields.content && <RichText data={node.fields.content} converters={jsxConverters} />}
</ProductTabs>
),
},
// Custom converter for the Payload "upload" Lexical node (Media collection)
// This natively reconstructs Next.js <Image /> tags pointing to the focal-point cropped sizes
upload: ({ node }: any) => {
// Attempt to extract the highly optimized 'card' generated size from Payload, fallback to raw url
let src = node?.value?.sizes?.card?.url || node?.value?.url;
const alt = node?.value?.alt || 'Blog Post Media';
if (!src) return null;
// Strip legacy imgproxy query parameters (e.g. ?ar=16:9) that crash Next.js 14+ localPatterns
if (src.includes('?')) {
src = src.split('?')[0];
}
// Fallback dimensions if unmapped or loading from raw
const width = node?.value?.sizes?.card?.width || 800;
const height = node?.value?.sizes?.card?.height || 600;
return (
<figure className="my-8 md:my-12 relative w-full rounded-2xl md:rounded-[32px] overflow-hidden shadow-xl md:shadow-2xl">
<Image
src={src}
alt={alt}
width={width}
height={height}
className="w-full object-cover transition-transform duration-700 hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
style={{
objectPosition: `${node?.value?.focalX ?? 50}% ${node?.value?.focalY ?? 50}%`,
}}
/>
{node?.value?.caption && (
<figcaption className="p-4 bg-neutral-dark text-white/80 text-sm text-center italic border-t border-white/10">
{node.value.caption}
</figcaption>
)}
</figure>
);
},
};
export default function PayloadRichText({ data }: { data: any }) {
if (!data) return null;
return (
<div className="article-content max-w-none">
<RichText data={data} converters={jsxConverters} />
</div>
);
}