import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
import Image from 'next/image';
import { Suspense, Fragment } from 'react';
// Import all custom React components that were previously mapped via Markdown
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';
import ContactForm from '@/components/ContactForm';
import ContactMap from '@/components/ContactMap';
import Gallery from '@/components/team/Gallery';
import Reveal from '@/components/Reveal';
import { Badge, Container, Heading, Section, Card } from '@/components/ui';
import TrackedLink from '@/components/analytics/TrackedLink';
import { useLocale } from 'next-intl';
import ObfuscatedEmail from '@/components/ObfuscatedEmail';
import ObfuscatedPhone from '@/components/ObfuscatedPhone';
import HomeHero from '@/components/home/Hero';
import ProductCategories from '@/components/home/ProductCategories';
import WhatWeDo from '@/components/home/WhatWeDo';
import RecentPosts from '@/components/home/RecentPosts';
import Experience from '@/components/home/Experience';
import WhyChooseUs from '@/components/home/WhyChooseUs';
import MeetTheTeam from '@/components/home/MeetTheTeam';
import GallerySection from '@/components/home/GallerySection';
import VideoSection from '@/components/home/VideoSection';
import CTA from '@/components/home/CTA';
/**
* Splits a text string on \n and intersperses
elements.
* This is needed because Lexical stores newlines as literal \n characters inside
* text nodes (e.g. dash-lists typed in the editor), but HTML collapses whitespace.
*/
function textWithLineBreaks(text: string, key: string) {
const parts = text.split('\n');
if (parts.length === 1) return text;
return parts.map((part, i) => (
{part}
{i < parts.length - 1 &&
}
));
}
const jsxConverters: JSXConverters = {
...defaultJSXConverters,
// Handle Lexical linebreak nodes (explicit shift+enter)
linebreak: () =>
,
// Custom text converter: preserve \n inside text nodes as
and obfuscate emails
text: ({ node }: any) => {
let content: React.ReactNode = node.text || '';
// Split newlines first
if (typeof content === 'string' && content.includes('\n')) {
content = textWithLineBreaks(content, `t-${(node.text || '').slice(0, 8)}`);
}
// Obfuscate emails in text content
if (typeof content === 'string' && content.includes('@')) {
const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
const parts = content.split(emailRegex);
content = parts.map((part, i) => {
if (part.match(emailRegex)) {
return ;
}
return part;
});
}
// Obfuscate phone numbers in text content (simple pattern for +XX XXX ...)
if (typeof content === 'string' && content.match(/\+\d+/)) {
const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g;
const parts = content.split(phoneRegex);
content = parts.map((part, i) => {
if (part.match(phoneRegex)) {
return ;
}
return part;
});
}
// Handle array content (from previous mappings)
if (Array.isArray(content)) {
content = content.map((item, idx) => {
if (typeof item === 'string') {
// Re-apply phone regex to strings in array
const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g;
if (item.match(phoneRegex)) {
const parts = item.split(phoneRegex);
return parts.map((part, i) => {
if (part.match(phoneRegex)) {
return ;
}
return part;
});
}
}
return item;
});
}
// Apply Lexical formatting flags
if (node.format) {
if (node.format & 1) content = {content};
if (node.format & 2) content = {content};
if (node.format & 8) content = {content};
if (node.format & 4) content = {content};
if (node.format & 16)
content = (
{content}
);
if (node.format & 32) content = {content};
if (node.format & 64) content = {content};
}
return <>{content}>;
},
// Use div instead of p for paragraphs to allow nested block elements (like the lists above)
paragraph: ({ node, nodesToJSX }: any) => {
return (
{nodesToJSX({ nodes: node.children })}
);
},
// Scale headings to prevent multiple H1s (H1 -> H2, etc) and style natively
heading: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
const tag = node?.tag;
// Extract text to generate an ID for the TOC
// Lexical children might contain various nodes; we need a plain text representation
const textContent = node.children ? node.children.map((c: any) => c.text || '').join('') : '';
const id = textContent
? textContent
.toLowerCase()
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/[*_`]/g, '')
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '')
: undefined;
if (tag === 'h1')
return (
{children}
);
if (tag === 'h2')
return (
{children}
);
if (tag === 'h3')
return (
{children}
);
if (tag === 'h4')
return (
{children}
);
if (tag === 'h5')
return (
{children}
);
return (
{children}
);
},
list: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
if (node?.listType === 'number') {
return (
{children}
);
}
if (node?.listType === 'check') {
return ;
}
return (
);
},
listitem: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
if (node?.checked != null) {
return (
{children}
);
}
return {children};
},
quote: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
return (
{children}
);
},
link: ({ node, nodesToJSX }: any) => {
const children = nodesToJSX({ nodes: node.children });
// Handling Payload CMS link nodes
const href = node?.fields?.url || node?.url || '#';
const newTab = node?.fields?.newTab || node?.newTab;
if (href.startsWith('mailto:')) {
const email = href.replace('mailto:', '');
return (
);
}
return (
{children}
);
},
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) => (
),
'block-stickyNarrative': ({ node }: any) => (
),
comparisonGrid: ({ node }: any) => (
),
'block-comparisonGrid': ({ node }: any) => (
),
visualLinkPreview: ({ node }: any) => (
),
'block-visualLinkPreview': ({ node }: any) => (
),
technicalGrid: ({ node }: any) => (
),
'block-technicalGrid': ({ node }: any) => {
if (!node?.fields) return null;
return ;
},
highlightBox: ({ node }: any) => (
),
'block-highlightBox': ({ node }: any) => (
),
animatedImage: ({ node }: any) => (
),
'block-animatedImage': ({ node }: any) => (
),
chatBubble: ({ node }: any) => (
),
'block-chatBubble': ({ node }: any) => (
),
powerCTA: ({ node }: any) => ,
'block-powerCTA': ({ node }: any) => ,
callout: ({ node }: any) => (
),
'block-callout': ({ node }: any) => (
),
stats: ({ node }: any) => ,
'block-stats': ({ node }: any) => ,
splitHeading: ({ node }: any) => (
{node.fields.title}
),
'block-splitHeading': ({ node }: any) => (
{node.fields.title}
),
productTabs: ({ node }: any) => {
if (!node?.fields) return null;
return (
}
>
<>>
);
},
'block-productTabs': ({ node }: any) => (
}
>
{node.fields.content && }
),
// ─── New Page Blocks ───────────────────────────────────────────
heroSection: ({ node }: any) => {
const f = node.fields;
const bgSrc = f.backgroundImage?.sizes?.card?.url || f.backgroundImage?.url;
return (
{bgSrc && (
<>
>
)}
{f.badge && (
{f.badge}
)}
{f.title}
{f.subtitle && (
{f.subtitle}
)}
{f.ctaLabel && f.ctaHref && (
)}
);
},
'block-heroSection': ({ node }: any) => {
const f = node.fields;
const bgSrc = f.backgroundImage?.sizes?.card?.url || f.backgroundImage?.url;
return (
{bgSrc && (
<>
>
)}
{f.badge && (
{f.badge}
)}
{f.title}
{f.subtitle && (
{f.subtitle}
)}
{f.ctaLabel && f.ctaHref && (
)}
);
},
teamProfile: ({ node }: any) => {
const f = node.fields;
const imgSrc = f.image?.sizes?.card?.url || f.image?.url;
const isDark = f.colorScheme === 'dark';
const isImageRight = f.layout === 'imageRight';
return (
{f.role}
{f.name}
{f.quote && (
)}
{f.description && (
{f.description}
)}
{f.linkedinUrl && (
{f.linkedinLabel || 'LinkedIn'}
→
)}
{imgSrc && (
<>
>
)}
);
},
'block-teamProfile': ({ node }: any) => {
const f = node.fields;
const imgSrc = f.image?.sizes?.card?.url || f.image?.url;
const isDark = f.colorScheme === 'dark';
const isImageRight = f.layout === 'imageRight';
return (
{f.role}
{f.name}
{f.quote && (
)}
{f.description && (
{f.description}
)}
{f.linkedinUrl && (
{f.linkedinLabel || 'LinkedIn'}
→
)}
{imgSrc && (
<>
>
)}
);
},
contactSection: ({ node }: any) => {
const f = node.fields;
return (
);
},
'block-contactSection': ({ node }: any) => {
const f = node.fields;
return (
);
},
imageGallery: ({ node }: any) => ,
'block-imageGallery': ({ node }: any) => ,
categoryGrid: ({ node }: any) => {
const cats = node.fields.categories || [];
return (
);
},
'block-categoryGrid': ({ node }: any) => {
const cats = node.fields.categories || [];
return (
);
},
manifestoGrid: ({ node }: any) => {
const f = node.fields;
return (
{f.title && (
{f.title}
)}
{f.tagline && (
{f.tagline}
)}
{(f.items || []).map((item: any, idx: number) => (
-
0{idx + 1}
{item.title}
{item.description}
))}
);
},
'block-manifestoGrid': ({ node }: any) => {
const f = node.fields;
return (
{f.title && (
{f.title}
)}
{f.tagline && (
{f.tagline}
)}
{(f.items || []).map((item: any, idx: number) => (
-
0{idx + 1}
{item.title}
{item.description}
))}
);
},
homeHero: ({ node }: any) => {
console.log('[PayloadRichText] Rendering homeHero block');
return ;
},
'block-homeHero': ({ node }: any) => {
console.log('[PayloadRichText] Rendering block-homeHero block');
return ;
},
homeProductCategories: ({ node }: any) => (
),
'block-homeProductCategories': ({ node }: any) => (
),
homeWhatWeDo: ({ node }: any) => (
),
'block-homeWhatWeDo': ({ node }: any) => (
),
homeExperience: ({ node }: any) => (
),
'block-homeExperience': ({ node }: any) => (
),
homeWhyChooseUs: ({ node }: any) => (
),
'block-homeWhyChooseUs': ({ node }: any) => (
),
homeMeetTheTeam: ({ node }: any) => (
),
'block-homeMeetTheTeam': ({ node }: any) => (
),
homeGallery: ({ node }: any) => (
),
'block-homeGallery': ({ node }: any) => (
),
homeVideo: ({ node }: any) => (
),
'block-homeVideo': ({ node }: any) => (
),
homeCTA: ({ node }: any) => (
),
'block-homeCTA': ({ node }: any) => (
),
},
// Custom converter for the Payload "upload" Lexical node (Media collection)
// This natively reconstructs Next.js 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 (
{node?.value?.caption && (
{node.value.caption}
)}
);
},
};
export default function PayloadRichText({
data,
className = 'article-content max-w-none',
}: {
data: any;
className?: string;
}) {
const locale = useLocale();
if (!data) return null;
if (data.root?.children?.length > 0) {
console.log('[PayloadRichText DEBUG] received children', data.root.children.length);
}
const dynamicConverters: JSXConverters = {
...jsxConverters,
blocks: {
...jsxConverters.blocks,
homeRecentPosts: () => (
),
'block-homeRecentPosts': () => (
),
},
};
return (
);
}