import { useState, useRef, useEffect } from "react"; import type { BodyElement } from "../../lib/api"; import { ChartRenderer } from "../charts/ChartRenderer"; interface BodyRendererProps { elements: BodyElement[]; isEditing?: boolean; onUpdate?: (index: number, element: BodyElement) => void; onReorder?: (fromIndex: number, toIndex: number) => void; } export function BodyRenderer({ elements, isEditing = false, onUpdate, onReorder }: BodyRendererProps) { const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); if (elements.length === 0) { return (
No content yet. Use the CLI below to add content.
); } const handleDragStart = (index: number) => (e: React.DragEvent) => { setDraggedIndex(index); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", index.toString()); }; const handleDragOver = (index: number) => (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (draggedIndex !== null && draggedIndex !== index) { setDragOverIndex(index); } }; const handleDragLeave = () => { setDragOverIndex(null); }; const handleDrop = (toIndex: number) => (e: React.DragEvent) => { e.preventDefault(); const fromIndex = draggedIndex; setDraggedIndex(null); setDragOverIndex(null); if (fromIndex !== null && fromIndex !== toIndex && onReorder) { onReorder(fromIndex, toIndex); } }; const handleDragEnd = () => { setDraggedIndex(null); setDragOverIndex(null); }; return (
{elements.map((element, index) => (
{/* Drag handle - only show in edit mode */} {isEditing && onReorder && (
)}
onUpdate(index, el) : undefined} />
))}
); } function BodyElementRenderer({ element, onUpdate, }: { element: BodyElement; onUpdate?: (element: BodyElement) => void; }) { switch (element.type) { case "heading": return ( onUpdate({ ...element, text }) : undefined } /> ); case "paragraph": return ( onUpdate({ ...element, text }) : undefined } /> ); case "chart": return ( ); case "image": return ( ); default: return null; } } function HeadingElement({ level, text, onUpdate, }: { level: number; text: string; onUpdate?: (text: string) => void; }) { const [isEditing, setIsEditing] = useState(false); const [editText, setEditText] = useState(text); const inputRef = useRef(null); useEffect(() => { setEditText(text); }, [text]); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isEditing]); const handleSave = () => { if (onUpdate && editText.trim() !== text) { onUpdate(editText.trim()); } setIsEditing(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { handleSave(); } else if (e.key === "Escape") { setEditText(text); setIsEditing(false); } }; const baseClassName = "font-mono text-[#9bc3ff]"; const sizeClasses: Record = { 1: "text-2xl font-bold", 2: "text-xl font-bold", 3: "text-lg font-semibold", 4: "text-base font-semibold", 5: "text-sm font-semibold", 6: "text-xs font-semibold", }; const sizeClass = sizeClasses[level] || sizeClasses[3]; if (isEditing && onUpdate) { return ( setEditText(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} className={`${baseClassName} ${sizeClass} w-full bg-transparent border-b border-[#3f6fb3] outline-none`} /> ); } const HeadingTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; return ( onUpdate && setIsEditing(true)} > {text} ); } function ParagraphElement({ text, onUpdate, }: { text: string; onUpdate?: (text: string) => void; }) { const [isEditing, setIsEditing] = useState(false); const [editText, setEditText] = useState(text); const textareaRef = useRef(null); useEffect(() => { setEditText(text); }, [text]); useEffect(() => { if (isEditing && textareaRef.current) { textareaRef.current.focus(); // Auto-resize textarea textareaRef.current.style.height = "auto"; textareaRef.current.style.height = textareaRef.current.scrollHeight + "px"; } }, [isEditing]); const handleSave = () => { if (onUpdate && editText.trim() !== text) { onUpdate(editText.trim()); } setIsEditing(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { setEditText(text); setIsEditing(false); } // Ctrl/Cmd + Enter to save if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { handleSave(); } }; const handleInput = (e: React.ChangeEvent) => { setEditText(e.target.value); // Auto-resize e.target.style.height = "auto"; e.target.style.height = e.target.scrollHeight + "px"; }; if (isEditing && onUpdate) { return (