import { useMemo } from 'react'; import { FeatureGridBlock, HeroBlock, MarketingTemplate, StatsPanel, type FeatureGridBlockProps, type StatItem, } from '@shared'; import { useSiteDefinitionStore } from '../../state/siteDefinitionStore'; import { useBuilderStore } from '../../state/builderStore'; import type { PageBlock, PageBlueprint, SiteStructure } from '../../types/siteBuilder'; type StructuredContent = Record & { items?: unknown[]; eyebrow?: string; ctaLabel?: string; supportingCopy?: string; columns?: number; }; export function PreviewCanvas() { const { structure, pages, selectedSlug, selectPage } = useSiteDefinitionStore(); const { selectedPageIds, togglePageSelection, selectAllPages, clearPageSelection, activeBlueprint } = useBuilderStore(); const page = useMemo(() => { if (structure?.pages?.length) { return structure.pages.find((p) => p.slug === selectedSlug) ?? structure.pages[0]; } return pages.find((p) => p.slug === selectedSlug) ?? pages[0]; }, [structure, pages, selectedSlug]); if (!structure && !pages.length) { return (

Generate a blueprint to see live previews of every page.

); } const navItems = structure?.site?.primary_navigation ?? pages.map((p) => p.slug); const blocks = getBlocks(page); const heroBlock = blocks.find((block) => normalizeType(block.type) === 'hero'); const contentBlocks = heroBlock ? blocks.filter((block) => block !== heroBlock) : blocks; const heroSection = heroBlock || page ? renderBlock(heroBlock ?? buildFallbackHero(page, structure)) : null; const sectionNodes = contentBlocks.length > 0 ? contentBlocks.map((block, index) =>
{renderBlock(block)}
) : buildFallbackSections(page); const sidebar = (

Page objective

{page?.objective ?? 'Launch a high-converting page'}

); // Only show page selection if we have actual PageBlueprint objects with IDs const hasPageBlueprints = pages.length > 0 && pages.every((p) => p.id > 0); const allSelected = hasPageBlueprints && pages.length > 0 && selectedPageIds.length === pages.length; const someSelected = hasPageBlueprints && selectedPageIds.length > 0 && selectedPageIds.length < pages.length; return (
{hasPageBlueprints && activeBlueprint && (
{ if (input) input.indeterminate = someSelected; }} onChange={(e) => { if (e.target.checked) { selectAllPages(); } else { clearPageSelection(); } }} style={{ cursor: 'pointer' }} />
{pages.map((p) => { const isSelected = selectedPageIds.includes(p.id); return ( ); })}
)} {navItems?.map((slug) => ( ))}
); } function getBlocks( page: (SiteStructure['pages'][number] & { blocks_json?: PageBlock[] }) | PageBlueprint | undefined, ) { if (!page) return []; const fromStructure = (page as { blocks?: PageBlock[] }).blocks; if (Array.isArray(fromStructure)) return fromStructure; const fromBlueprint = (page as PageBlueprint).blocks_json; return Array.isArray(fromBlueprint) ? fromBlueprint : []; } function renderBlock(block?: PageBlock) { if (!block) return null; const type = normalizeType(block.type); const structuredContent = extractStructuredContent(block); const listContent = extractListContent(block, structuredContent); if (type === 'hero') { return ( 0 ? ( ) : undefined } /> ); } if (type === 'feature-grid' || type === 'features' || type === 'value-props') { const features = toFeatureList(listContent, structuredContent.items); const columns = normalizeColumns(structuredContent.columns, features.length); return ; } if (type === 'stats' || type === 'metrics') { const stats = toStatItems(listContent, structuredContent.items, block); if (!stats.length) return defaultBlock(block); return ; } return defaultBlock(block); } function defaultBlock(block: PageBlock) { return (
{block.heading &&

{block.heading}

} {block.subheading &&

{block.subheading}

} {Array.isArray(block.content) && (
    {block.content.map((item) => (
  • {item}
  • ))}
)}
); } function normalizeType(type?: string) { return (type ?? '').toLowerCase(); } function extractStructuredContent(block: PageBlock): StructuredContent { if (Array.isArray(block.content)) { return {}; } return (block.content ?? {}) as StructuredContent; } function extractListContent(block: PageBlock, structuredContent: StructuredContent): unknown[] { if (Array.isArray(block.content)) { return block.content; } if (Array.isArray(structuredContent.items)) { return structuredContent.items; } return []; } function toFeatureList(listItems: unknown[], structuredItems?: unknown[]): FeatureGridBlockProps['features'] { const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems; return source.map((item) => { if (typeof item === 'string') { return { title: item }; } if (typeof item === 'object' && item) { const record = item as Record; return { title: String(record.title ?? record.heading ?? 'Feature'), description: record.description ? String(record.description) : undefined, icon: record.icon ? String(record.icon) : undefined, }; } return { title: String(item) }; }); } function toStatItems( listItems: unknown[], structuredItems: unknown[] | undefined, block: PageBlock, ): StatItem[] { const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems; return source .map((item, index) => { if (typeof item === 'string') { return { label: block.heading ?? `Metric ${index + 1}`, value: item, }; } if (typeof item === 'object' && item) { const record = item as Record; const label = record.label ?? record.title ?? `Metric ${index + 1}`; const value = record.value ?? record.metric ?? record.score; if (!value) return null; return { label: String(label), value: String(value), description: record.description ? String(record.description) : undefined, }; } return null; }) .filter((stat): stat is StatItem => Boolean(stat)); } function normalizeColumns( candidate: StructuredContent['columns'], featureCount: number, ): FeatureGridBlockProps['columns'] { const inferred = typeof candidate === 'number' ? candidate : featureCount >= 4 ? 4 : featureCount === 2 ? 2 : 3; if (inferred <= 2) return 2; if (inferred >= 4) return 4; return 3; } function buildFallbackHero( page: SiteStructure['pages'][number] | PageBlueprint | undefined, structure: SiteStructure | undefined, ): PageBlock { return { type: 'hero', heading: page?.title ?? 'Site Builder preview', subheading: structure?.site?.hero_message ?? 'Preview updates as the AI hydrates your blueprint.', content: Array.isArray(structure?.site?.secondary_navigation) ? structure?.site?.secondary_navigation : [], }; } function buildFallbackSections(page: SiteStructure['pages'][number] | PageBlueprint | undefined) { return [ , , ]; } function buildSidebarInsights( page: SiteStructure['pages'][number] | PageBlueprint | undefined, structure: SiteStructure | undefined, ) { return [ { label: 'Primary CTA', value: page?.primary_cta ?? 'Book a demo', }, { label: 'Tone', value: structure?.site?.tone ?? 'Confident & clear', }, { label: 'Status', value: page?.status ?? 'Draft', }, ]; }