import { useMemo, type ReactNode } from "react";
import {
documentToReactComponents,
type Options,
} from "@contentful/rich-text-react-renderer";
import {
BLOCKS,
INLINES,
MARKS,
type Document,
} from "@contentful/rich-text-types";
import { Checklist } from "@shared/components/checklist";
import { Link } from "@shared/components/link";
import { MaterialIcon } from "@shared/components/material-icon";
import { Text } from "@shared/components/text";
import { toDocument } from "@shared/utils/contentful/to-document";
const isEmptyParagraph = (node: any): boolean =>
Array.isArray(node?.content) &&
node.content.every(
(child: any) =>
child?.nodeType === "text" && (!child.value || child.value.trim() === "")
);
// A link is "external" only when it belongs to a different app than the one
// currently rendering this shared component (business vs. main/www are separate
// apps). Matching is environment-agnostic so a prod link like
// business.gokinetic.com is still "internal" while running on business-dev.
const hostFromUrl = (value?: string): string => {
if (!value) return "";
try {
const withProtocol = /^https?:\/\//i.test(value)
? value
: `https://${value}`;
return new URL(withProtocol).hostname.toLowerCase();
} catch {
return "";
}
};
// e.g. "business-dev.gokinetic.com" -> "gokinetic.com|business",
// "kinetic-dev.gokinetic.com" / "www.gokinetic.com" -> "gokinetic.com|main"
const appIdentity = (host: string): string => {
const clean = host.toLowerCase().replace(/\.$/, "");
const base = clean.split(".").slice(-2).join(".");
const subdomain = clean
.slice(0, clean.length - base.length)
.replace(/\.$/, "");
const firstLabel = (subdomain.split(".")[0] || "").replace(
/-(dev|development|staging|stage|qa|test|uat|prod|production)$/,
""
);
const app =
firstLabel === "" || firstLabel === "www" || firstLabel === "kinetic"
? "main"
: firstLabel;
return `${base}|${app}`;
};
const getCurrentHost = (): string => {
const envHost =
hostFromUrl(process.env.NEXT_PUBLIC_SITE_URL) ||
hostFromUrl(process.env.NEXT_PUBLIC_MAIN_SITE_URL);
if (envHost) return envHost;
if (typeof window !== "undefined")
return window.location.hostname.toLowerCase();
return "";
};
const isExternalUrl = (url: string): boolean => {
if (!/^https?:\/\//i.test(url)) return false; // relative links are always internal
try {
const linkHost = new URL(url).hostname.toLowerCase();
const currentHost = getCurrentHost();
if (!currentHost) return true; // host unknown (SSR without env) → treat as external
return appIdentity(linkHost) !== appIdentity(currentHost);
} catch {
return false;
}
};
const defaultOptions: Options = {
renderMark: {
[MARKS.BOLD]: text => {text},
[MARKS.ITALIC]: text => {text},
[MARKS.UNDERLINE]: text => {text},
[MARKS.CODE]: text => {text},
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => (
{children}, [BLOCKS.UL_LIST]: (_node, children) => (
and still work!
renderMark: {
...defaultOptions.renderMark,
[MARKS.BOLD]: text => (
{text}
),
},
renderNode: {
...defaultOptions.renderNode,
[BLOCKS.PARAGRAPH]: (_node, children) => <>{children}>,
[BLOCKS.TABLE]: (node: any, children) => (
tag
const rawText = node.content[0]?.content[0]?.value
?.toLowerCase()
.trim();
const renderContent = () => {
if (rawText === "yes") {
return (
2
? "min-w-[100.1%]"
: "min-w-full"
}`}
>
{children}
{children}
),
[BLOCKS.TABLE_HEADER_CELL]: (node: any, children) => {
const isScrollable = node.parent?.content.length > 2;
return (
{children}
);
},
[BLOCKS.TABLE_CELL]: (node: any, children) => {
const isScrollable = node.parent?.content.length > 2;
// Logic to check for "yes" or "no"
// children[0].props.children is usually where the text sits if it's in a
<> {renderContent()}>
);
},
[INLINES.EMBEDDED_ENTRY]: node => {
const entryId = node.data.target.sys.id;
const entry = links?.entries?.inline?.find(
(e: any) => e.sys.id === entryId
);
if (!entry) return null;
if (entry.__typename === "ComponentCheckList") {
// FIX: Don't use a Hook here.
// Map the items manually or use a non-hook helper function.
const items =
entry.list?.items?.map((item: any) =>
renderContentfulRichText(
toDocument(item?.checkListTitle ?? ""),
true,
""
)
) || [];
return (