"use client"; /** * T-141 — read-only template PREVIEW dialog. * * Renders the template's CURRENT snapshot (`GET /templates/:id/preview`) * as read-only text. The snapshot's blocks are DETACHED (they belong to no * live page), so the interactive `BlockRenderer` cannot be reused — this * reuses the house text-extractors instead (`blockText` for text-ish * content shapes, `codeBlockText` for code), which also never interpret the * stored HTML (a saved snapshot is untrusted input — same rule as the * version-compare panel). */ import { useQuery } from "@tanstack/react-query"; import { Dialog } from "./dialog"; import { templateApi } from "@/lib/api"; import { blockText } from "@/components/editor/version-text"; import { codeBlockText } from "@/components/editor/block-renderer"; import { sortByPosition } from "@/lib/utils"; import type { TemplateInfo } from "@/lib/types"; /* The snapshot's frozen shape (see `captureSubtree` in page.service). */ interface SnapshotBlock { id: string; type: string; content: unknown; properties: Record; parentId: string | null; position: string; } interface TemplateSnapshot { page?: { title: string; icon: string | null; description: string | null } | null; blocks: SnapshotBlock[]; children: { title: string; blocks: SnapshotBlock[] }[]; } function Placeholder({ label }: { label: string }) { return ( {label} ); } function BlockView({ b }: { b: SnapshotBlock }) { const text = blockText(b.content); const props = b.properties ?? {}; switch (b.type) { case "heading_1": return

{text}

; case "heading_2": return

{text}

; case "heading_3": return

{text}

; case "quote": return

{text}

; case "callout": return (

{(props.icon as string) ?? "ℹ️"} {text}

); case "bulleted_list": return

{text}

; case "numbered_list": return

•{text}

; case "todo": return (

{props.checked ? "✓" : ""} {text}

); case "code": return (
          {codeBlockText(b.content)}
        
); case "table": { const rows = (props.rows as string[][] | undefined) ?? []; return rows.length ? (
{rows.map((row, ri) => ( {row.map((cell, ci) => ( ))} ))}
{cell}
) : ( ); } case "divider": return
; case "spacer": return null; case "image": case "file": case "video": case "audio": return ; case "bookmark": case "embed": return ; case "database": case "linked_database": case "child_database": return ; case "chart": return ; case "form": return ; case "child_page": return ; case "synced_block": return ; case "toc": return ; case "equation": return
{text}
; default: // paragraph, toggle titles, column content, anything new — plain text. return text ?

{text}

: null; } } export function TemplatePreviewDialog({ template, onClose }: { template: TemplateInfo; onClose: () => void }) { const query = useQuery({ queryKey: ["templates", template.id, "preview"], queryFn: () => templateApi.preview(template.id), staleTime: 60_000 }); const snap: TemplateSnapshot | null = (query.data as { snapshot?: TemplateSnapshot } | null)?.snapshot ?? null; return ( {query.isLoading ? (

Loading preview…

) : query.isError ? (

Could not load the template snapshot.

) : (
{snap?.page?.icon && (

{snap.page.icon}

)}

{snap?.page?.title || template.name || "Untitled"}

{sortByPosition(snap?.blocks ?? []).map((b) => (
{}
))} {snap && snap.blocks.length === 0 && (

This template has no blocks.

)} {snap && snap.children.length > 0 && (

Sub-pages

    {snap.children.map((c, i) => (
  • ▸ {c.title}
  • ))}
)}
)}
); }