'use client'; import { useState, useCallback } from 'react'; import { parseEssayMarkdown, serializeEssayMarkdown, collectAssetIds, isValidCoordinate, type ParsedEssay, type EssayBlock, type MapItem, } from '@/lib/essay'; import { arrayMove } from '@dnd-kit/sortable'; import { SortableBlockList, SortableBlockCard } from './SortableBlocks'; import { IconCamera, IconChevronUp, IconChevronDown, IconTrash, IconQuote, IconFileText, IconSparkles, IconArrowLeftRight, IconGrid, IconColumns, IconMap, IconFolder, IconPlus, IconX, } 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: collectAssetIds(newBlocks), }; 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 handleReorderBlock = (from: number, to: number) => { updateBlocks(arrayMove(essay.blocks, from, to)); }; 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; case 'photo-grid': newBlock = { type: 'photo-grid', assetIds: ['', '', ''], caption: '' }; break; case 'facts': newBlock = { type: 'facts', items: [ { label: '', value: '' }, { label: '', value: '' }, ], }; break; case 'map': newBlock = { type: 'map', caption: '', line: true, items: [] }; break; case 'album': newBlock = { type: 'album', albumId: '', layout: 'grid' }; 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) => ( } actions={
} > {/* 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 The textarea shows `html` raw, as the journal editor does. It used to strip the tags for display — `` to `**`, everything else to nothing — while `onChange` wrote the stripped text straight back into `html`. Italics, links, anything the markdown had produced was destroyed by the act of editing the paragraph, and the two editors disagreed about the same file. serializeJournalMarkdown() turns the HTML back into markdown on save and handles `` too, so there was nothing to convert here in the first place. */} {block.type === 'paragraph' && (