"use client"; import React, { useEffect, useRef, useState } from "react"; export type EditorJsBlock = { id: string type: string data: { text?: string level?: number style?: "ordered" | "unordered" items?: string[] } } // Helper function to decode HTML entities function decodeHtmlEntities(text: string): string { const textarea = typeof document !== 'undefined' ? document.createElement('textarea') : null; if (textarea) { textarea.innerHTML = text; return textarea.value; } // Fallback for server-side rendering return text .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, ' '); } // Helper function to check if text contains HTML table elements function isTableFragment(text: string): boolean { const tablePatterns = [ //i, //i, //i, //i, //i, /pattern.test(text)); } // Helper function to merge table fragments function mergeTableBlocks(blocks: EditorJsBlock[]): EditorJsBlock[] { const merged: EditorJsBlock[] = []; let tableBuffer: string[] = []; let tableStartId: string | null = null; let isInTable = false; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; const text = block.data?.text || ""; const decodedText = decodeHtmlEntities(text); // Check if this starts a table if (/
/i, //i, /
tags and preserve whitespace const cleanedText = decodedText .replace(//gi, '') .replace(/^\s+|\s+$/g, ''); // Trim leading/trailing whitespace only if (cleanedText) { tableBuffer.push(cleanedText); } // Check if this closes the table if (/<\/table>/i.test(decodedText)) { isInTable = false; // Create merged table block const mergedHTML = tableBuffer.join(''); merged.push({ id: tableStartId || block.id, type: "table", data: { text: mergedHTML } }); tableBuffer = []; tableStartId = null; } } else { // If we have accumulated table content, create a merged block if (tableBuffer.length > 0 && tableStartId) { // Join without extra newlines to preserve HTML structure const mergedHTML = tableBuffer.join(''); merged.push({ id: tableStartId, type: "table", data: { text: mergedHTML } }); tableBuffer = []; tableStartId = null; isInTable = false; } // Add the non-table block merged.push(block); } } // Handle any remaining table content if (tableBuffer.length > 0 && tableStartId) { const mergedHTML = tableBuffer.join(''); merged.push({ id: tableStartId, type: "table", data: { text: mergedHTML } }); } return merged; } function TableWithScrollCheck({ blockId, html, }: { blockId: string; html: string; }) { const wrapperRef = useRef(null); const [hasOverflow, setHasOverflow] = useState(false); useEffect(() => { const checkOverflow = () => { if (wrapperRef.current) { const hasScroll = wrapperRef.current.scrollWidth > wrapperRef.current.clientWidth; setHasOverflow(hasScroll); } }; checkOverflow(); window.addEventListener("resize", checkOverflow); const observer = new MutationObserver(checkOverflow); if (wrapperRef.current) { observer.observe(wrapperRef.current, { childList: true, subtree: true }); } return () => { window.removeEventListener("resize", checkOverflow); observer.disconnect(); }; }, [html]); return (
{hasOverflow && (

Swipe left to view additional table content

)}
); } export default function EditorRenderer({ content, }: { content: string | null | undefined; }) { let blocks: EditorJsBlock[] = []; if (content) { try { const parsed = JSON.parse(content) if (Array.isArray(parsed?.blocks)) { blocks = parsed.blocks as EditorJsBlock[] } } catch (_) {} } if (!blocks.length) { return (

Data not found

) } // Merge table fragments before rendering const mergedBlocks = mergeTableBlocks(blocks); const renderBlock = (block: EditorJsBlock) => { const { type, data } = block const html = decodeHtmlEntities(data?.text || "") switch (type) { case "table": return ( ); case "header": { // Never render `h1` from CMS content. Each page template should own the // single visible H1 for SEO/accessibility. const level = Math.min(Math.max(Number(data?.level) || 3, 2), 6) return React.createElement( `h${level}`, { key: block.id, className: "text-2xl font-semibold leading-8 tracking-[-0.06px] my-6", dangerouslySetInnerHTML: { __html: html }, } ) } case "list": { const style = data?.style || "unordered" const items = Array.isArray(data?.items) ? data.items : [] const ListTag = style === "ordered" ? "ol" : "ul" const listClassName = style === "ordered" ? "list-decimal list-inside my-4 space-y-2 text-lg leading-7 tracking-[-0.045px]" : "list-disc list-inside my-4 space-y-2 text-lg leading-7 tracking-[-0.045px]" return ( {items.map((item, idx) => (
  • ))} ) } case "paragraph": default: // Check if this paragraph contains table HTML that wasn't merged if (isTableFragment(html)) { return (
    ); } return (

    ) } } return ( <>