import { useEffect, useRef, useState } from 'react'; import ToastEditor from '@toast-ui/editor'; import '@toast-ui/editor/dist/toastui-editor.css'; import { supabase } from './supabase.js'; import { api } from './api.js'; import { Field, blankForm, formFromFrontmatter, frontmatterFromForm, targetPath, isShort, isBody, hexOf, DEFAULT_PALETTE, } from './fields.jsx'; export default function App() { const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { supabase.auth.getSession().then(({ data }) => { setSession(data.session); setLoading(false); }); const { data: sub } = supabase.auth.onAuthStateChange((_e, s) => setSession(s)); return () => sub.subscription.unsubscribe(); }, []); if (loading) return
; if (!session) return ; return ; } function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [err, setErr] = useState(null); async function submit(e) { e.preventDefault(); setErr(null); const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) setErr(error.message); } return (
OPENBUREAU
Redaktion
setEmail(e.target.value)} autoFocus /> setPassword(e.target.value)} /> {err &&

{err}

}
); } function Dashboard({ email }) { const [entries, setEntries] = useState([]); const [collections, setCollections] = useState(null); const [plugins, setPlugins] = useState([]); const [site, setSite] = useState(''); const [current, setCurrent] = useState(null); const [query, setQuery] = useState(''); const [view, setView] = useState('content'); const [me, setMe] = useState(null); const [msg, setMsg] = useState(null); async function refresh() { try { setEntries(await api.list()); } catch (e) { setMsg({ type: 'err', text: e.message }); } } useEffect(() => { refresh(); api.getMe().then(setMe).catch(() => {}); api.schema().then((sc) => { setCollections((sc.collections || []).slice().sort((a, b) => (a.order ?? 99) - (b.order ?? 99))); setPlugins(sc.plugins || []); setSite(sc.site || ''); }).catch(() => setCollections([])); }, []); useEffect(() => { if (!msg) return; const t = setTimeout(() => setMsg(null), 4000); return () => clearTimeout(t); }, [msg]); const cols = collections || []; const hasDialog = plugins.includes('dialog'); const collectionOf = (kind) => cols.find((c) => c.kind === kind) || cols.find((c) => c.fallback) || cols[0] || null; async function open(entry) { const col = collectionOf(entry.kind); try { const r = await api.read(entry.path); const bodyField = (col?.fields || []).find(isBody); setCurrent({ isNew: false, kind: entry.kind, path: entry.path, ...formFromFrontmatter(r.frontmatter || {}, col || { fields: [] }), ...(bodyField ? { [bodyField.name]: r.body || '' } : {}), }); } catch (err) { setMsg({ type: 'err', text: err.message }); } } function openNew() { const col = cols[0]; if (col) setCurrent({ isNew: true, kind: col.kind, path: '', ...blankForm(col) }); } const q = query.trim().toLowerCase(); const filtered = q ? entries.filter((e) => (e.title || '').toLowerCase().includes(q) || (e.section || '').includes(q)) : entries; const fallbackKind = (cols.find((c) => c.fallback) || cols[0] || {}).kind; const groups = {}; for (const c of cols) groups[c.kind] = []; for (const e of filtered) { const k = groups[e.kind] ? e.kind : fallbackKind; if (groups[k]) groups[k].push(e); } return (
{(site || 'cms').toUpperCase()} Redaktion {email}
{view === 'overview' ? ( ) : view === 'profile' ? ( ) : view === 'users' ? ( ) : view === 'forums' ? ( ) : view === 'moderation' ? ( ) : view === 'system' ? ( ) : ( <>
{current ? { setCurrent(loaded); refresh(); }} onMsg={setMsg} /> :

Wähle links einen Eintrag — oder leg einen neuen an.

}
)}
{msg &&
setMsg(null)}>{msg.text}
}
); } function Editor({ initial, collections, onSaved, onMsg }) { const [f, setF] = useState(initial); const [previewUrl, setPreviewUrl] = useState(null); const [showPreview, setShowPreview] = useState(false); const [pw, setPw] = useState(44); const [busy, setBusy] = useState(false); const editorRef = useRef(null); const dragging = useRef(false); const cols = collections || []; const collection = cols.find((c) => c.kind === f.kind) || cols[0] || { fields: [] }; const fields = collection.fields || []; const titleField = fields.find((x) => x.name === 'title') || fields.find((x) => x.type === 'string' && x.required) || null; const bodyField = fields.find(isBody); const draftField = fields.find((x) => x.type === 'bool' && x.name === 'draft'); const shortFields = fields.filter((x) => x !== titleField && !isBody(x) && isShort(x)); const longFields = fields.filter((x) => x !== titleField && !isBody(x) && !isShort(x)); const isDraft = draftField ? !!f[draftField.name] : false; const hasSlugField = fields.some((x) => x.name === 'slug'); const setField = (name, val) => setF((p) => ({ ...p, [name]: val })); const upload = async (file) => (await api.upload(file)).url; function changeKind(kind) { const col = cols.find((c) => c.kind === kind); if (col) setF({ isNew: true, kind, path: '', ...blankForm(col) }); } // Ziehbarer Trenner Editor ↔ Vorschau. useEffect(() => { function move(e) { if (!dragging.current || !editorRef.current) return; const r = editorRef.current.getBoundingClientRect(); setPw(Math.min(70, Math.max(25, ((r.right - e.clientX) / r.width) * 100))); } function up() { dragging.current = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; } window.addEventListener('mousemove', move); window.addEventListener('mouseup', up); return () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); }; }, []); function startDrag(e) { dragging.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); } async function save(overrides = {}) { const data = { ...f, ...overrides }; const path = data.isNew ? targetPath(data, collection) : data.path; if (!path) { onMsg({ type: 'err', text: 'Bitte einen Slug (und ggf. Pflichtsegmente wie Rubrik) angeben.' }); return null; } if (titleField && !((data[titleField.name] || '').trim())) { onMsg({ type: 'err', text: 'Titel fehlt.' }); return null; } setBusy(true); try { const fm = frontmatterFromForm(data, collection); const body = bodyField ? (data[bodyField.name] || '') : ''; await api.save(path, fm, body); const r = await api.read(path); const loaded = { isNew: false, kind: data.kind, path, ...formFromFrontmatter(r.frontmatter || {}, collection), ...(bodyField ? { [bodyField.name]: r.body || '' } : {}), }; onSaved(loaded); setF(loaded); return path; } catch (e) { onMsg({ type: 'err', text: e.message }); return null; } finally { setBusy(false); } } async function preview() { const path = await save(); if (!path) return; setShowPreview(true); setBusy(true); try { const res = await api.preview(path); setPreviewUrl(`${res.url}?t=${Date.now()}`); } catch (e) { onMsg({ type: 'err', text: e.message }); } finally { setBusy(false); } } async function publish() { if (!confirm('Live publizieren?')) return; const path = await save(draftField ? { [draftField.name]: false } : {}); if (!path) return; setBusy(true); try { const res = await api.publish(path); onMsg({ type: 'ok', text: `Live: ${res.url}` }); } catch (e) { onMsg({ type: 'err', text: e.message }); } finally { setBusy(false); } } return (
{f.isNew ? 'Neuer Eintrag' : f.path}
{draftField && (isDraft ? Entwurf : Veröffentlicht)}
{f.isNew && (
{!hasSlugField && ( )}
)} {titleField && ( )} {shortFields.length > 0 && (
{shortFields.map((fld) => )}
)} {longFields.map((fld) => )} {bodyField && (
setField(bodyField.name, body)} onUpload={upload} />
)}
{showPreview &&
} {showPreview && (
{previewUrl ?