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'; // OPENBUREAU-Palette (Hex aus assets/css/custom.css) — für Dropdown + Punkte. const COLORS = [ ['', 'keine', 'transparent'], ['ajisai', 'Ajisai · Hortensie', '#A39EC4'], ['sakura', 'Sakura · Kirschblüte', '#C49EC4'], ['suna', 'Suna · Sand', '#C4C19E'], ['ichigo', 'Ichigo · Erdbeere', '#C49EA0'], ['yuyake', 'Yuyake · Sonnenuntergang', '#CEB188'], ['sora', 'Sora · Himmel', '#9EC3C4'], ['kusa', 'Kusa · Gras', '#9EC49F'], ['kori', 'Kori · Eis', '#A5B4CB'], ['amagumo', 'Amagumo · Regenwolke', '#4C4C4C'], ['yuki', 'Yuki · Schnee', '#F0F0F0'], ]; const hexOf = (name) => (COLORS.find((c) => c[0] === name) || [])[2] || 'transparent'; const LAYOUTS = ['', 'text', 'image', 'icon']; const SECTIONS = ['buerofuehrung', 'software', 'theorie']; const KIND_LABEL = { beitrag: 'Beiträge', biblio: 'Library', seite: 'Seiten', rubrik: 'Rubriken' }; const EMPTY = { isNew: true, path: '', type: 'beitrag', section: 'software', slug: '', title: '', date: new Date().toISOString().slice(0, 10), weight: '', color: '', layout: 'text', tags: '', summary: '', description: '', cover_image: '', external: '', authors: '', group: '', toc: false, draft: true, body: '', }; 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 [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(() => {}); }, []); useEffect(() => { if (!msg) return; const t = setTimeout(() => setMsg(null), 4000); return () => clearTimeout(t); }, [msg]); async function open(entry) { try { setCurrent(fromRead(await api.read(entry.path))); } catch (err) { setMsg({ type: 'err', text: err.message }); } } const q = query.trim().toLowerCase(); const filtered = q ? entries.filter((e) => e.title.toLowerCase().includes(q) || (e.section || '').includes(q)) : entries; const groups = { beitrag: [], biblio: [], seite: [], rubrik: [] }; for (const e of filtered) (groups[e.kind] || groups.seite).push(e); return (
OPENBUREAU Redaktion {email}
{view === 'overview' ? ( ) : view === 'profile' ? ( ) : view === 'users' ? ( ) : view === 'forums' ? ( ) : view === 'moderation' ? ( ) : ( <>
{current ? { setCurrent(loaded); refresh(); }} onMsg={setMsg} /> :

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

}
)}
{msg &&
setMsg(null)}>{msg.text}
}
); } function Editor({ initial, 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 coverIn = useRef(null); const set = (k) => (e) => setF({ ...f, [k]: e.target.type === 'checkbox' ? e.target.checked : e.target.value }); const isWiki = f.type === 'biblio' || (f.path || '').startsWith('library/'); async function pickCover(ev) { const file = ev.target.files?.[0]; ev.target.value = ''; if (!file) return; setBusy(true); try { const { url } = await api.upload(file); setF((p) => ({ ...p, cover_image: url })); } catch (e) { onMsg({ type: 'err', text: e.message }); } finally { setBusy(false); } } // 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(); } function currentPath(data = f) { if (!data.isNew) return data.path; const slug = (data.slug || '').trim(); if (!slug) return ''; if (data.type === 'beitrag') return `archiv/${data.section}/${slug}.md`; if (data.type === 'biblio') return `library/${slug}.md`; return `${slug}.md`; } // overrides erlauben z.B. { draft: false } beim Publizieren. async function save(overrides = {}) { const data = { ...f, ...overrides }; const path = currentPath(data); if (!path) { onMsg({ type: 'err', text: 'Bitte einen Slug angeben.' }); return null; } if (!data.title.trim()) { onMsg({ type: 'err', text: 'Titel fehlt.' }); return null; } setBusy(true); try { await api.save(path, buildFrontmatter(data), data.body); const loaded = fromRead(await api.read(path)); 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? Der Beitrag wird aus „Entwurf“ genommen.')) return; const path = await save({ draft: false }); // Publizieren = nicht mehr Entwurf 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}
{f.draft ? Entwurf : Veröffentlicht}
{f.isNew && (
{f.type === 'beitrag' && ( )}
)}
{isWiki && }
setF((p) => ({ ...p, body }))} onUpload={async (file) => (await api.upload(file)).url} />
{showPreview &&
} {showPreview && (
{previewUrl ?