'use client'; import { useState, useCallback } from 'react'; import { parseEssayMarkdown, serializeEssayMarkdown, type ParsedEssay, type EssayBlock, } from '@/lib/essay'; import { IconCamera, IconChevronUp, IconChevronDown, IconTrash, IconGripVertical, IconQuote, IconFileText, IconSparkles, IconArrowLeftRight, } from './Icons'; import { BlockBadge } from './BlockBadge'; interface EssayBlockEditorProps { markdown: string; onChange: (newMarkdown: string) => void; onSelectPhoto?: (callback: (assetId: string) => void) => void; } export function EssayBlockEditor({ markdown, onChange, onSelectPhoto }: EssayBlockEditorProps) { const [essay, setEssay] = useState(() => parseEssayMarkdown(markdown || '# Title\n\nWrite your story here...'), ); // Sync internal state when the prop changes from outside (e.g. initial load). // Adjusting during render rather than in an effect: an effect would commit the // stale essay first and re-render, which flashes the previous story's blocks. const [lastMarkdown, setLastMarkdown] = useState(markdown); if (markdown !== lastMarkdown) { setLastMarkdown(markdown); if (markdown && markdown !== serializeEssayMarkdown(essay)) { setEssay(parseEssayMarkdown(markdown)); } } const updateBlocks = useCallback( (newBlocks: EssayBlock[]) => { const updatedEssay: ParsedEssay = { ...essay, blocks: newBlocks, referencedAssetIds: Array.from( new Set( newBlocks.flatMap((b) => { if (b.type === 'photo') return [b.assetId]; if (b.type === 'photo-pair') return b.assetIds; return []; }), ), ), }; setEssay(updatedEssay); onChange(serializeEssayMarkdown(updatedEssay)); }, [essay, onChange], ); // Block manipulation helpers const handleMoveBlock = (index: number, direction: 'up' | 'down') => { const targetIdx = direction === 'up' ? index - 1 : index + 1; if (targetIdx < 0 || targetIdx >= essay.blocks.length) return; const newBlocks = [...essay.blocks]; const [moved] = newBlocks.splice(index, 1); newBlocks.splice(targetIdx, 0, moved); updateBlocks(newBlocks); }; const handleDeleteBlock = (index: number) => { const newBlocks = essay.blocks.filter((_, i) => i !== index); updateBlocks(newBlocks); }; const handleUpdateBlock = (index: number, updated: EssayBlock) => { const newBlocks = [...essay.blocks]; newBlocks[index] = updated; updateBlocks(newBlocks); }; const handleAddBlock = (type: EssayBlock['type']) => { let newBlock: EssayBlock; switch (type) { case 'heading': newBlock = { type: 'heading', level: 2, text: 'New Section Heading' }; break; case 'paragraph': newBlock = { type: 'paragraph', html: 'Enter text paragraph here...' }; break; case 'quote': newBlock = { type: 'quote', text: 'Enter quote text here...', author: '' }; break; case 'photo': newBlock = { type: 'photo', assetId: '', caption: '', layout: 'contained', }; break; case 'photo-pair': newBlock = { type: 'photo-pair', assetIds: ['', ''], caption: '', }; break; } updateBlocks([...essay.blocks, newBlock]); }; return (
{/* Sticky Dark Glass Toolbar */}
Add Story Block:
{essay.blocks.length === 0 && (

No story blocks yet. Use the buttons above to build your photo essay!

)} {/* Render Blocks */} {essay.blocks.map((block, idx) => (
{/* Card Header */}
{/* Block Form Fields */} {/* 1. HEADING BLOCK */} {block.type === 'heading' && (
handleUpdateBlock(idx, { ...block, text: e.target.value })} placeholder="Section heading..." style={{ flex: 1, fontWeight: block.level === 1 ? 700 : block.level === 2 ? 600 : 500, }} />
)} {/* 2. PARAGRAPH BLOCK */} {block.type === 'paragraph' && (