Files
e-tib.com/components/PayloadRichText.tsx
2026-04-28 23:23:21 +02:00

95 lines
3.0 KiB
TypeScript

import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
import Image from 'next/image';
import { Fragment } from 'react';
import { allBlocks } from '@/src/payload/blocks/allBlocks';
import ObfuscatedEmail from '@/components/ObfuscatedEmail';
import ObfuscatedPhone from '@/components/ObfuscatedPhone';
/**
* Splits a text string on \n and intersperses <br /> elements.
*/
function textWithLineBreaks(text: string, key: string) {
const parts = text.split('\n');
if (parts.length === 1) return text;
return parts.map((part, i) => (
<Fragment key={`${key}-${i}`}>
{part}
{i < parts.length - 1 && <br />}
</Fragment>
));
}
const jsxConverters: JSXConverters = {
...defaultJSXConverters,
linebreak: () => <br />,
text: ({ node }: any) => {
let content: React.ReactNode = node.text || '';
if (typeof content === 'string' && content.includes('\n')) {
content = textWithLineBreaks(content, `t-${(node.text || '').slice(0, 8)}`);
}
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 <ObfuscatedEmail key={`e-${i}`} email={part} />;
}
return part;
});
}
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 <ObfuscatedPhone key={`p-${i}`} phone={part} />;
}
return part;
});
}
return content;
},
upload: ({ node }: any) => {
return (
<div className="relative w-full aspect-video my-8 rounded-3xl overflow-hidden shadow-2xl group">
<Image
src={node.value.url}
alt={node.value.alt || ''}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
</div>
);
},
blocks: allBlocks.reduce((acc, block) => {
const Component = block.render;
if (!Component) return acc;
const renderFn = ({ node }: any) => {
// Pass all fields as props to the component, and also as 'data' prop
return <Component {...node.fields} data={node.fields} />;
};
// Map both the direct slug and the block- prefixed slug
return {
...acc,
[block.slug]: renderFn,
[`block-${block.slug}`]: renderFn,
};
}, {}),
};
export default function PayloadRichText({ data, className, references }: { data: any; className?: string; references?: any[] }) {
if (!data) return null;
return (
<div className={className}>
<RichText data={data} converters={jsxConverters} />
</div>
);
}