// AdminArticleEditor.jsx — formulario de creación/edición de artículo function AdminArticleEditor({ article, onSaved, onCancel }) { const isNew = !article; const [title, setTitle] = React.useState(article?.title || ''); const [slug, setSlug] = React.useState(article?.slug || ''); const [slugTouched, setSlugTouched] = React.useState(!!article); const [excerpt, setExcerpt] = React.useState(article?.excerpt || ''); const [category, setCategory] = React.useState(article?.category || 'IA'); const [readTime, setReadTime] = React.useState(article?.readTime || ''); const [accent, setAccent] = React.useState(article?.accent || '#005BC2'); const [client, setClient] = React.useState(article?.client || ''); const [sector, setSector] = React.useState(article?.sector || ''); const [stackText, setStackText] = React.useState((article?.stack || []).join(', ')); const [coverUrl, setCoverUrl] = React.useState(article?.coverUrl || ''); const [status, setStatus] = React.useState(article?.status || 'private'); const [body, setBody] = React.useState(article?.body || null); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(''); const editorRef = React.useRef(null); const editorHostRef = React.useRef(null); React.useEffect(() => { if (!editorHostRef.current || editorRef.current) return; if (!window.EditorJS) { console.warn('EditorJS no disponible'); return; } const ej = new window.EditorJS({ holder: editorHostRef.current, placeholder: 'Escribe el artículo…', data: body || { blocks: [] }, tools: { header: window.EditorJSHeader ? { class: window.EditorJSHeader, config: { levels: [2, 3], defaultLevel: 2 } } : undefined, list: window.EditorJSList ? { class: window.EditorJSList, inlineToolbar: true } : undefined, quote: window.EditorJSQuote || undefined, code: window.EditorJSCode || undefined, delimiter: window.EditorJSDelimiter || undefined, image: window.EditorJSImage ? { class: window.EditorJSImage, config: { uploader: { uploadByFile: async (file) => { const url = await window.supabaseApi.uploadCover(file); return { success: 1, file: { url } }; }, uploadByUrl: async (url) => ({ success: 1, file: { url } }), }, }, } : undefined, embed: window.EditorJSEmbed ? { class: window.EditorJSEmbed, config: { services: { youtube: true, vimeo: true, twitter: true } }, } : undefined, }, onChange: async () => { try { const out = await ej.save(); setBody(out); } catch (_) {} }, }); editorRef.current = ej; return () => { try { ej.destroy && ej.destroy(); } catch (_) {} editorRef.current = null; }; // eslint-disable-next-line }, []); React.useEffect(() => { if (!slugTouched && title) setSlug(window.supabaseApi.slugify(title)); }, [title, slugTouched]); const onPickFile = async (e) => { const file = e.target.files?.[0]; if (!file) return; setBusy(true); setErr(''); try { const url = await window.supabaseApi.uploadCover(file); setCoverUrl(url); } catch (e) { setErr(e.message || 'Error subiendo imagen'); } finally { setBusy(false); } }; const save = async (newStatus) => { setBusy(true); setErr(''); try { let finalBody = body; if (editorRef.current) { try { finalBody = await editorRef.current.save(); } catch (_) {} } const payload = { slug: slug || window.supabaseApi.slugify(title), title, excerpt, category, readTime, accent, client, sector, stack: stackText.split(',').map(s => s.trim()).filter(Boolean), coverUrl, status: newStatus || status, body: finalBody, }; const saved = await window.supabaseApi.upsertArticle(payload, article?._row?.id); window.dispatchEvent(new Event('articles:refresh')); onSaved && onSaved(saved); } catch (e) { setErr(e.message || 'Error guardando'); } finally { setBusy(false); } }; return (

{isNew ? 'Nuevo artículo' : 'Editar artículo'}

{status === 'public' ? 'Público' : 'Privado'}
{err &&
{err}
}