Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 2m19s
Build & Deploy / 🏗️ Build (push) Successful in 6m16s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 1m23s
Build & Deploy / ⚡ Performance & Accessibility (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
255 lines
9.4 KiB
TypeScript
255 lines
9.4 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,
|
|
// Let the default converters handle text nodes to preserve valid formatting
|
|
// If the text node contains raw HTML (from messy migrations), render it as HTML instead of escaping it
|
|
text: ({ node }: any) => {
|
|
const text = node.text;
|
|
if (text && (text.includes('<') || text.includes('data-start'))) {
|
|
return <span dangerouslySetInnerHTML={{ __html: text }} />;
|
|
}
|
|
|
|
// Handle markdown-style lists embedded in text nodes from MDX migration
|
|
if (text && text.includes('\n- ')) {
|
|
const parts = text.split('\n- ').filter(Boolean);
|
|
// If first part doesn't start with "- ", it's a prefix paragraph
|
|
const startsWithDash = text.trimStart().startsWith('- ');
|
|
const prefix = startsWithDash ? null : parts.shift();
|
|
return (
|
|
<>
|
|
{prefix && <span>{prefix}</span>}
|
|
<ul className="list-disc pl-6 my-4 space-y-2">
|
|
{parts.map((item: string, i: number) => (
|
|
<li key={i}>{item.trim()}</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (node.format === 1) return <strong>{text}</strong>;
|
|
if (node.format === 2) return <em>{text}</em>;
|
|
return <span>{text}</span>;
|
|
},
|
|
// Scale headings to prevent multiple H1s (H1 -> H2, etc)
|
|
h1: ({ children }: any) => <h2 className="text-3xl md:text-4xl font-bold my-6">{children}</h2>,
|
|
h2: ({ children }: any) => <h3 className="text-2xl md:text-3xl font-bold my-5">{children}</h3>,
|
|
h3: ({ children }: any) => <h4 className="text-xl md:text-2xl font-bold my-4">{children}</h4>,
|
|
|
|
blocks: {
|
|
// ... preserved existing 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>
|
|
);
|
|
}
|