All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Successful in 21m19s
Build & Deploy / 🚀 Deploy (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m59s
Build & Deploy / 🔔 Notify (push) Successful in 9s
Nightly QA / 🔗 Links & Deps (push) Successful in 3m40s
Nightly QA / 🎭 Lighthouse (push) Successful in 4m18s
Nightly QA / 🔍 Static Analysis (push) Successful in 4m34s
Nightly QA / 📝 E2E (push) Successful in 4m45s
Nightly QA / 🔔 Notify (push) Has been skipped
390 lines
12 KiB
TypeScript
390 lines
12 KiB
TypeScript
import {
|
|
RichText,
|
|
defaultJSXConverters,
|
|
} from "@payloadcms/richtext-lexical/react";
|
|
import type { JSXConverters } from "@payloadcms/richtext-lexical/react";
|
|
import { MemeCard } from "@/src/components/MemeCard";
|
|
import { Mermaid } from "@/src/components/Mermaid";
|
|
import { LeadMagnet } from "@/src/components/LeadMagnet";
|
|
import { ComparisonRow } from "@/src/components/Landing/ComparisonRow";
|
|
import { mdxComponents } from "../content-engine/components";
|
|
import React from "react";
|
|
|
|
/**
|
|
* Renders markdown-style inline links [text](/url) as <a> tags.
|
|
* Used by mintelP blocks which store body text with links.
|
|
*/
|
|
function renderInlineMarkdown(text: string): React.ReactNode {
|
|
if (!text) return null;
|
|
const parts = text.split(/(\[[^\]]+\]\([^)]+\)|<Marker>[^<]*<\/Marker>)/);
|
|
return parts.map((part, i) => {
|
|
const linkMatch = part.match(/\[([^\]]+)\]\(([^)]+)\)/);
|
|
if (linkMatch) {
|
|
return (
|
|
<a
|
|
key={i}
|
|
href={linkMatch[2]}
|
|
className="text-slate-900 underline underline-offset-4 hover:text-slate-600 transition-colors"
|
|
>
|
|
{linkMatch[1]}
|
|
</a>
|
|
);
|
|
}
|
|
const markerMatch = part.match(/<Marker>([^<]*)<\/Marker>/);
|
|
if (markerMatch) {
|
|
return (
|
|
<mark key={i} className="bg-yellow-100/60 px-1 rounded">
|
|
{markerMatch[1]}
|
|
</mark>
|
|
);
|
|
}
|
|
return <React.Fragment key={i}>{part}</React.Fragment>;
|
|
});
|
|
}
|
|
|
|
const jsxConverters: JSXConverters = {
|
|
...defaultJSXConverters,
|
|
// Override paragraph to filter out leftover <TableOfContents /> raw text
|
|
paragraph: ({ node, nodesToJSX }: any) => {
|
|
const children = node?.children;
|
|
if (
|
|
children?.length === 1 &&
|
|
children[0]?.type === "text" &&
|
|
children[0]?.text?.trim()?.startsWith("<") &&
|
|
children[0]?.text?.trim()?.endsWith("/>")
|
|
) {
|
|
return null; // suppress raw JSX component text like <TableOfContents />
|
|
}
|
|
return <p>{nodesToJSX({ nodes: children })}</p>;
|
|
},
|
|
blocks: {
|
|
memeCard: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<MemeCard
|
|
template={node.fields.template}
|
|
captions={node.fields.captions}
|
|
/>
|
|
</div>
|
|
),
|
|
mermaid: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid
|
|
id={node.fields.id}
|
|
title={node.fields.title}
|
|
showShare={node.fields.showShare}
|
|
>
|
|
{node.fields.chartDefinition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
leadMagnet: ({ node }: any) => (
|
|
<div className="my-12">
|
|
<LeadMagnet
|
|
title={node.fields.title}
|
|
description={node.fields.description}
|
|
buttonText={node.fields.buttonText}
|
|
href={node.fields.href}
|
|
variant={node.fields.variant}
|
|
/>
|
|
</div>
|
|
),
|
|
comparisonRow: ({ node }: any) => (
|
|
<ComparisonRow
|
|
description={node.fields.description}
|
|
negativeLabel={node.fields.negativeLabel}
|
|
negativeText={node.fields.negativeText}
|
|
positiveLabel={node.fields.positiveLabel}
|
|
positiveText={node.fields.positiveText}
|
|
reverse={node.fields.reverse}
|
|
showShare={true}
|
|
/>
|
|
),
|
|
// --- Core text blocks ---
|
|
mintelP: ({ node }: any) => (
|
|
<p className="text-base md:text-lg text-slate-600 leading-relaxed mb-6">
|
|
{renderInlineMarkdown(node.fields.text)}
|
|
</p>
|
|
),
|
|
mintelTldr: ({ node }: any) => (
|
|
<mdxComponents.TLDR>{node.fields.content}</mdxComponents.TLDR>
|
|
),
|
|
// --- MDX Registry Injections ---
|
|
leadParagraph: ({ node }: any) => (
|
|
<mdxComponents.LeadParagraph>
|
|
{node.fields.text}
|
|
</mdxComponents.LeadParagraph>
|
|
),
|
|
articleBlockquote: ({ node }: any) => (
|
|
<mdxComponents.ArticleBlockquote>
|
|
{node.fields.quote}
|
|
{node.fields.author && ` - ${node.fields.author}`}
|
|
</mdxComponents.ArticleBlockquote>
|
|
),
|
|
mintelH2: ({ node }: any) => (
|
|
<mdxComponents.H2>{node.fields.text}</mdxComponents.H2>
|
|
),
|
|
mintelH3: ({ node }: any) => (
|
|
<mdxComponents.H3>{node.fields.text}</mdxComponents.H3>
|
|
),
|
|
mintelHeading: ({ node }: any) => {
|
|
const displayLevel = node.fields.displayLevel || "h2";
|
|
if (displayLevel === "h3")
|
|
return <mdxComponents.H3>{node.fields.text}</mdxComponents.H3>;
|
|
return <mdxComponents.H2>{node.fields.text}</mdxComponents.H2>;
|
|
},
|
|
statsDisplay: ({ node }: any) => (
|
|
<mdxComponents.StatsDisplay
|
|
label={node.fields.label}
|
|
value={node.fields.value}
|
|
subtext={node.fields.subtext}
|
|
/>
|
|
),
|
|
diagramState: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid id={`diagram-state-${node.fields.id || Date.now()}`}>
|
|
{node.fields.definition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
diagramTimeline: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid id={`diagram-timeline-${node.fields.id || Date.now()}`}>
|
|
{node.fields.definition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
diagramGantt: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid id={`diagram-gantt-${node.fields.id || Date.now()}`}>
|
|
{node.fields.definition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
diagramPie: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid id={`diagram-pie-${node.fields.id || Date.now()}`}>
|
|
{node.fields.definition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
diagramSequence: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid id={`diagram-seq-${node.fields.id || Date.now()}`}>
|
|
{node.fields.definition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
diagramFlow: ({ node }: any) => (
|
|
<div className="my-8">
|
|
<Mermaid id={`diagram-flow-${node.fields.id || Date.now()}`}>
|
|
{node.fields.definition}
|
|
</Mermaid>
|
|
</div>
|
|
),
|
|
|
|
waterfallChart: ({ node }: any) => (
|
|
<mdxComponents.WaterfallChart
|
|
title={node.fields.title}
|
|
events={node.fields.metrics || []}
|
|
/>
|
|
),
|
|
premiumComparisonChart: ({ node }: any) => (
|
|
<mdxComponents.PremiumComparisonChart
|
|
title={node.fields.title}
|
|
items={node.fields.datasets || []}
|
|
/>
|
|
),
|
|
iconList: ({ node }: any) => (
|
|
<mdxComponents.IconList>
|
|
{node.fields.items?.map((item: any, i: number) => {
|
|
const isCheck = item.icon === "check" || !item.icon;
|
|
const isCross = item.icon === "x" || item.icon === "cross";
|
|
const isBullet = item.icon === "circle" || item.icon === "bullet";
|
|
return (
|
|
// @ts-ignore
|
|
<mdxComponents.IconListItem
|
|
key={i}
|
|
check={isCheck}
|
|
cross={isCross}
|
|
bullet={isBullet}
|
|
>
|
|
{item.title || item.description}
|
|
</mdxComponents.IconListItem>
|
|
);
|
|
})}
|
|
</mdxComponents.IconList>
|
|
),
|
|
statsGrid: ({ node }: any) => {
|
|
const rawStats = node.fields.stats || [];
|
|
let statsStr = "";
|
|
if (Array.isArray(rawStats)) {
|
|
statsStr = rawStats
|
|
.map((s: any) => `${s.value || ""}|${s.label || ""}`)
|
|
.join("~");
|
|
} else if (typeof rawStats === "string") {
|
|
statsStr = rawStats;
|
|
}
|
|
return <mdxComponents.StatsGrid stats={statsStr} />;
|
|
},
|
|
metricBar: ({ node }: any) => (
|
|
<mdxComponents.MetricBar
|
|
label={node.fields.label}
|
|
value={node.fields.value}
|
|
color={node.fields.color as any}
|
|
/>
|
|
),
|
|
carousel: ({ node }: any) => (
|
|
<mdxComponents.Carousel
|
|
items={
|
|
node.fields.slides?.map((s: any) => ({
|
|
title: s.title || s.caption || "Slide",
|
|
content: s.content || s.caption || "",
|
|
icon: undefined,
|
|
})) || []
|
|
}
|
|
/>
|
|
),
|
|
imageText: ({ node }: any) => (
|
|
<mdxComponents.ImageText
|
|
image={node.fields.image?.url || ""}
|
|
title="ImageText Component"
|
|
>
|
|
{node.fields.text}
|
|
</mdxComponents.ImageText>
|
|
),
|
|
revenueLossCalculator: ({ node }: any) => (
|
|
<mdxComponents.RevenueLossCalculator />
|
|
),
|
|
performanceChart: ({ node }: any) => <mdxComponents.PerformanceChart />,
|
|
performanceROICalculator: ({ node }: any) => (
|
|
<div className="not-prose my-12">
|
|
<mdxComponents.PerformanceROICalculator />
|
|
</div>
|
|
),
|
|
loadTimeSimulator: ({ node }: any) => (
|
|
<div className="not-prose my-12">
|
|
<mdxComponents.LoadTimeSimulator />
|
|
</div>
|
|
),
|
|
architectureBuilder: ({ node }: any) => (
|
|
<div className="not-prose my-12">
|
|
<mdxComponents.ArchitectureBuilder />
|
|
</div>
|
|
),
|
|
digitalAssetVisualizer: ({ node }: any) => (
|
|
<div className="not-prose my-12">
|
|
<mdxComponents.DigitalAssetVisualizer />
|
|
</div>
|
|
),
|
|
|
|
twitterEmbed: ({ node }: any) => (
|
|
<mdxComponents.TwitterEmbed
|
|
tweetId={node.fields.url?.split("/").pop() || ""}
|
|
/>
|
|
),
|
|
youTubeEmbed: ({ node }: any) => (
|
|
<mdxComponents.YouTubeEmbed
|
|
videoId={node.fields.videoId}
|
|
title={node.fields.title}
|
|
/>
|
|
),
|
|
linkedInEmbed: ({ node }: any) => (
|
|
<mdxComponents.LinkedInEmbed url={node.fields.url} />
|
|
),
|
|
externalLink: ({ node }: any) => (
|
|
<mdxComponents.ExternalLink href={node.fields.href}>
|
|
{node.fields.label}
|
|
</mdxComponents.ExternalLink>
|
|
),
|
|
trackedLink: ({ node }: any) => (
|
|
<mdxComponents.TrackedLink
|
|
href={node.fields.href}
|
|
eventName={node.fields.eventName}
|
|
>
|
|
{node.fields.label}
|
|
</mdxComponents.TrackedLink>
|
|
),
|
|
articleMeme: ({ node }: any) => (
|
|
<mdxComponents.ArticleMeme
|
|
template="drake"
|
|
captions={node.fields.caption || "Top|Bottom"}
|
|
image={node.fields.image?.url || undefined}
|
|
/>
|
|
),
|
|
marker: ({ node }: any) => (
|
|
<mdxComponents.Marker color={node.fields.color} delay={node.fields.delay}>
|
|
{node.fields.text}
|
|
</mdxComponents.Marker>
|
|
),
|
|
boldNumber: ({ node }: any) => (
|
|
<mdxComponents.BoldNumber
|
|
value={node.fields.value}
|
|
label={node.fields.label}
|
|
source={node.fields.source}
|
|
sourceUrl={node.fields.sourceUrl}
|
|
/>
|
|
),
|
|
webVitalsScore: ({ node }: any) => (
|
|
<mdxComponents.WebVitalsScore
|
|
values={{
|
|
lcp: node.fields.lcp,
|
|
inp: node.fields.inp,
|
|
cls: node.fields.cls,
|
|
}}
|
|
description={node.fields.description}
|
|
/>
|
|
),
|
|
buttonBlock: ({ node }: any) => (
|
|
<mdxComponents.Button
|
|
href={node.fields.href}
|
|
variant={node.fields.variant}
|
|
size={node.fields.size}
|
|
showArrow={node.fields.showArrow}
|
|
>
|
|
{node.fields.label}
|
|
</mdxComponents.Button>
|
|
),
|
|
articleQuote: ({ node }: any) => (
|
|
<mdxComponents.ArticleQuote
|
|
quote={node.fields.quote}
|
|
author={node.fields.author}
|
|
role={node.fields.role}
|
|
source={node.fields.source}
|
|
sourceUrl={node.fields.sourceUrl}
|
|
translated={node.fields.translated}
|
|
isCompany={node.fields.isCompany}
|
|
/>
|
|
),
|
|
reveal: ({ node }: any) => (
|
|
<mdxComponents.Reveal
|
|
direction={node.fields.direction}
|
|
delay={node.fields.delay}
|
|
>
|
|
{/* Reveal component takes children, which in MDX is nested content */}
|
|
<PayloadRichText data={node.fields.content} />
|
|
</mdxComponents.Reveal>
|
|
),
|
|
section: ({ node }: any) => (
|
|
<mdxComponents.Section title={node.fields.title}>
|
|
<PayloadRichText data={node.fields.content} />
|
|
</mdxComponents.Section>
|
|
),
|
|
tableOfContents: () => <mdxComponents.TableOfContents />,
|
|
faqSection: ({ node }: any) => (
|
|
<mdxComponents.FAQSection>
|
|
<PayloadRichText data={node.fields.content} />
|
|
</mdxComponents.FAQSection>
|
|
),
|
|
},
|
|
};
|
|
|
|
export function PayloadRichText({ data }: { data: any }) {
|
|
if (!data) return null;
|
|
|
|
return (
|
|
<div className="article-content max-w-none">
|
|
<RichText data={data} converters={jsxConverters} />
|
|
</div>
|
|
);
|
|
}
|