feat: payload cms
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
This commit is contained in:
213
components/PayloadRichText.tsx
Normal file
213
components/PayloadRichText.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { 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 = {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<></>
|
||||
</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"
|
||||
/>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default function ProductSidebar({
|
||||
<div className="relative w-full aspect-[16/10] mb-6 rounded-2xl overflow-hidden bg-white/5 backdrop-blur-md p-4 border border-white/10 z-10 group">
|
||||
<div className="relative w-full h-full transition-transform duration-1000 ease-out group-hover:scale-105">
|
||||
<Image
|
||||
src={productImage}
|
||||
src={productImage.split('?')[0]}
|
||||
alt={productName}
|
||||
fill
|
||||
className="object-contain p-2 drop-shadow-[0_20px_30px_rgba(0,0,0,0.4)]"
|
||||
|
||||
@@ -82,7 +82,7 @@ export default async function RelatedProducts({
|
||||
{product.frontmatter.images?.[0] ? (
|
||||
<>
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
src={product.frontmatter.images[0].split('?')[0]}
|
||||
alt={product.frontmatter.title}
|
||||
fill
|
||||
className="object-contain p-4 transition-transform duration-700 group-hover:scale-110 z-10"
|
||||
|
||||
@@ -29,10 +29,14 @@ export default function TrackedLink({
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
trackEvent(eventName, {
|
||||
href,
|
||||
...eventProperties,
|
||||
});
|
||||
try {
|
||||
trackEvent(eventName, {
|
||||
href,
|
||||
...eventProperties,
|
||||
});
|
||||
} catch (_e) {
|
||||
// Analytics tracking should not block navigation, so we catch and ignore errors.
|
||||
}
|
||||
if (onClick) onClick();
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function PostNavigation({
|
||||
{prev.frontmatter.featuredImage ? (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
||||
style={{ backgroundImage: `url(${prev.frontmatter.featuredImage}?ar=16:9)` }}
|
||||
style={{ backgroundImage: `url(${prev.frontmatter.featuredImage.split('?')[0]})` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-neutral-100" />
|
||||
@@ -82,7 +82,7 @@ export default function PostNavigation({
|
||||
{next.frontmatter.featuredImage ? (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
||||
style={{ backgroundImage: `url(${next.frontmatter.featuredImage}?ar=16:9)` }}
|
||||
style={{ backgroundImage: `url(${next.frontmatter.featuredImage.split('?')[0]})` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-neutral-100" />
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function VisualLinkPreview({ url, title, summary, image }: Visual
|
||||
<div className="relative w-full md:w-64 h-48 md:h-auto flex-shrink-0 bg-neutral-50 overflow-hidden">
|
||||
{image ? (
|
||||
<Image
|
||||
src={image}
|
||||
src={image.split('?')[0]}
|
||||
alt={title}
|
||||
fill
|
||||
unoptimized
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
|
||||
{post.frontmatter.featuredImage && (
|
||||
<div className="relative h-64 overflow-hidden">
|
||||
<Image
|
||||
src={`${post.frontmatter.featuredImage}?ar=16:9`}
|
||||
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||
alt={post.frontmatter.title}
|
||||
fill
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Button({
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const baseStyles =
|
||||
'inline-flex items-center justify-center rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
|
||||
@@ -45,8 +45,8 @@ export function Button({
|
||||
const sizes = {
|
||||
sm: 'h-9 px-4 text-sm md:text-base',
|
||||
md: 'h-11 px-6 text-base md:text-lg',
|
||||
lg: 'h-14 px-8 text-base md:text-lg',
|
||||
xl: 'h-16 px-10 text-lg md:text-xl',
|
||||
lg: 'h-14 px-5 md:px-8 text-base md:text-lg',
|
||||
xl: 'h-16 px-6 md:px-10 text-lg md:text-xl',
|
||||
};
|
||||
|
||||
const styles = cn(baseStyles, variants[variant], sizes[size], className);
|
||||
|
||||
Reference in New Issue
Block a user