/* ════════════════════════════════════════════════ Dashboard — modul Artikel & Pendaftar ════════════════════════════════════════════════ */ const ART_CATS = ['Tahfidz', 'Bahasa Arab', 'Fiqih', 'Akhlaq']; /* WYSIWYG editor (contentEditable) — format langsung terlihat */ function Wysiwyg({ value, onChange }) { const ref = React.useRef(); const [active, setActive] = React.useState({}); React.useEffect(() => { if (ref.current && ref.current.innerHTML !== (value || '')) ref.current.innerHTML = value || ''; }, []); const sync = () => { if (ref.current) onChange(ref.current.innerHTML); }; const exec = (cmd, val) => { document.execCommand('styleWithCSS', false, false); document.execCommand(cmd, false, val); ref.current && ref.current.focus(); sync(); refreshActive(); }; const block = (tag) => exec('formatBlock', tag); const link = () => { const url = window.prompt('Masukkan URL tautan:', 'https://'); if (url) exec('createLink', url); }; const arabicBlock = () => { exec('formatBlock', 'p'); const sel = document.getSelection(); let n = sel && sel.anchorNode; while (n && n.nodeName !== 'P' && n !== ref.current) n = n.parentNode; if (n && n.nodeName === 'P') { n.setAttribute('dir', 'rtl'); n.className = 'ar'; sync(); } }; const hr = () => { exec('insertHTML', '
'); }; const fileRef = React.useRef(); const insertImage = async (e) => { const file = e.target.files[0]; e.target.value = ''; if (!file) return; const reader = new FileReader(); reader.onload = async () => { let url = reader.result; // dataURL sementara ref.current && ref.current.focus(); document.execCommand('insertHTML', false, `


`); sync(); // unggah ke server, ganti dataURL dgn URL asli try { const fd = new FormData(); fd.append('file', file); fd.append('_preview', url); const res = await api.post('upload.php', fd); const real = res.ok && (res.data?.url || res.url); if (real && ref.current) { ref.current.innerHTML = ref.current.innerHTML.replace(url, real); sync(); } } catch {} }; reader.readAsDataURL(file); }; const refreshActive = () => { try { setActive({ bold: document.queryCommandState('bold'), italic: document.queryCommandState('italic'), underline: document.queryCommandState('underline'), ul: document.queryCommandState('insertUnorderedList'), ol: document.queryCommandState('insertOrderedList') }); } catch {} }; const btn = (label, on, st) => ; const selStyle = { padding: '6px 8px', borderRadius: 7, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--ink-deep)', fontSize: 12.5, fontFamily: 'inherit', cursor: 'pointer' }; const sep = () => ; return (
{btn(B, () => exec('bold'), active.bold)} {btn(I, () => exec('italic'), active.italic)} {btn(U, () => exec('underline'), active.underline)} {sep()} {btn('H2', () => block('h2'))} {btn('H3', () => block('h3'))} {btn('¶', () => block('p'))} {sep()} {/* Jenis font */} {/* Ukuran font */} {/* Warna font */} {sep()} {/* Perataan */} {btn('⬅', () => exec('justifyLeft'))} {btn('⬌', () => exec('justifyCenter'))} {btn('➡', () => exec('justifyRight'))} {btn('☰', () => exec('justifyFull'))} {btn('⇤', () => exec('outdent'))} {btn('⇥', () => exec('indent'))} {sep()} {btn('• Poin', () => exec('insertUnorderedList'), active.ul)} {btn('1. Nomor', () => exec('insertOrderedList'), active.ol)} {btn('Kutipan', () => block('blockquote'))} {btn('Tautan', link)} {btn('Gambar', () => fileRef.current && fileRef.current.click())} {btn('﷽ Teks Arab', arabicBlock)} {btn('Garis', hr)} {btn('Hapus Format', () => exec('removeFormat'))}
Ketik langsung — gunakan tombol di atas untuk memformat. Hasil tampil persis seperti di artikel.
); } /* Toolbar format Markdown untuk textarea isi artikel */ function MdToolbar({ taRef, value, onChange }) { const wrap = (before, after, placeholder) => { const ta = taRef.current; if (!ta) return; const s = ta.selectionStart, e = ta.selectionEnd; const sel = value.slice(s, e) || placeholder; const next = value.slice(0, s) + before + sel + after + value.slice(e); onChange(next); setTimeout(() => { ta.focus(); ta.selectionStart = s + before.length; ta.selectionEnd = s + before.length + sel.length; }, 0); }; const prefixLine = (prefix) => { const ta = taRef.current; if (!ta) return; const s = ta.selectionStart; const lineStart = value.lastIndexOf('\n', s - 1) + 1; const next = value.slice(0, lineStart) + prefix + value.slice(lineStart); onChange(next); setTimeout(() => { ta.focus(); ta.selectionStart = ta.selectionEnd = s + prefix.length; }, 0); }; const btn = { padding: '6px 10px', borderRadius: 7, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--ink-deep)', fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit', minWidth: 32 }; const tools = [ ['B', () => wrap('**', '**', 'teks tebal'), { fontWeight: 900 }], ['I', () => wrap('*', '*', 'teks miring'), { fontStyle: 'italic' }], ['H2', () => prefixLine('## '), {}], ['H3', () => prefixLine('### '), {}], ['• Poin', () => prefixLine('- '), {}], ['1. Nomor', () => prefixLine('1. '), {}], ['❝ Kutipan', () => prefixLine('> '), {}], ['🔗 Tautan', () => wrap('[', '](https://)', 'teks tautan'), {}], ]; return (
{tools.map(([label, fn, st]) => ( ))}
); } function ArticlesModule({ user, toast }) { const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(true); const [modal, setModal] = useState(null); // null | {id?} form const [form, setForm] = useState({}); const [saving, setSaving] = useState(false); const bodyRef = React.useRef(); const [writers, setWriters] = useState([]); useEffect(() => { api.get('auth.php?action=users').then(r => { if (r.ok && r.users) setWriters(r.users); }); }, []); const load = () => { setLoading(true); const act = user.role === 'admin' ? 'all' : 'my'; api.get('articles.php?action=' + act).then(r => { if (r.ok && r.articles) setArticles(r.articles); }).finally(() => setLoading(false)); }; useEffect(load, []); const openAdd = () => { setForm({ title: '', category: 'Tahfidz', excerpt: '', body: '', icon: '📖', status: 'published', cover_url: '', author_name: user.name }); setModal({}); }; const openEdit = (a) => { setForm({ ...a }); setModal({ id: a.id }); }; const save = async () => { if (!form.title) return; setSaving(true); const action = modal.id ? 'update' : 'create'; const payload = modal.id ? { ...form, id: modal.id } : form; try { const r = await api.post('articles.php?action=' + action, payload); if (r.ok) { toast(modal.id ? '✅ Artikel diperbarui' : '✅ Artikel ditambahkan'); setModal(null); load(); } else toast('⚠️ ' + (r.error || 'Gagal menyimpan')); } finally { setSaving(false); } }; const del = async (a) => { if (!window.confirm(`Hapus artikel "${a.title}"?`)) return; const r = await api.post('articles.php?action=delete', { id: a.id }); if (r.ok) { toast('Artikel dihapus'); load(); } else toast('⚠️ Gagal menghapus'); }; return (

Kelola seluruh artikel yang tampil di situs.

+ Tulis Artikel
{loading ?
Memuat…
: articles.length === 0 ?
Belum ada artikel. Klik "Tulis Artikel".
:
{articles.map(a => (
{a.cover_url ? : }
{a.title}
{a.author_name || a.author || user.name}
{a.status === 'published' ? 'Terbit' : 'Draf'}
openEdit(a)}>Edit del(a)}>Hapus
))}
}
setModal(null)} title={modal && modal.id ? 'Edit Artikel' : 'Tulis Artikel Baru'} width="640px"> {modal && ( <> setForm(f => ({ ...f, cover_url: v }))} /> setForm(f => ({ ...f, title: e.target.value }))} style={dInp} placeholder="Judul artikel" />
setForm(f => ({ ...f, category: e.target.value }))} style={dInp} placeholder="mis. Tahfidz" />{ART_CATS.map(c =>