/* ════════════════════════════════════════════════
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" />
setForm(f => ({ ...f, body: v }))} />
{saving ? 'Menyimpan…' : 'Simpan Artikel'}
setModal(null)}>Batal
>
)}
);
}
/* ── Pendaftar Santri ── */
function RegistrationsModule({ toast }) {
const [regs, setRegs] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('semua');
const statusMap = { baru: ['Baru', 'orange'], diproses: ['Diproses', 'blue'], selesai: ['Selesai', 'green'], dibatalkan: ['Batal', 'red'] };
const load = () => { setLoading(true); api.get('registrations.php?action=list').then(r => { if (r.ok && r.registrations) setRegs(r.registrations); }).finally(() => setLoading(false)); };
useEffect(load, []);
const setStatus = async (reg, status) => {
const prev = regs;
setRegs(p => p.map(x => x.id === reg.id ? { ...x, status } : x));
const r = await api.post('registrations.php?action=update_status', { id: reg.id, status });
if (r.ok) toast('✅ Status diperbarui'); else { setRegs(prev); toast('⚠️ Gagal'); }
};
const del = async (reg) => {
if (!window.confirm(`Hapus pendaftaran "${reg.name}"?`)) return;
const r = await api.post('registrations.php?action=delete', { id: reg.id });
if (r.ok) { toast('Dihapus'); load(); }
};
const wa = (reg) => window.open(`https://wa.me/62${(reg.phone || '').replace(/^0/, '')}?text=` + encodeURIComponent(`Assalamu'alaikum Bpk/Ibu ${reg.parent_name || ''}, terima kasih telah mendaftarkan ${reg.name} di Markaz Qurrota A'yun.`), '_blank');
const filtered = filter === 'semua' ? regs : regs.filter(r => r.status === filter);
const counts = regs.reduce((a, r) => { a[r.status] = (a[r.status] || 0) + 1; return a; }, {});
return (
{['semua', 'baru', 'diproses', 'selesai'].map(f => (
))}
{loading ? Memuat…
: filtered.length === 0 ? Belum ada pendaftar{filter !== 'semua' ? ' dengan status ini' : ''}.
:
{filtered.map(r => (
{r.name}
{r.usia} · {r.gender || '-'}
{r.program}
{r.level && {r.level}
}
{r.parent_name}
{r.phone}
{(statusMap[r.status] || ['Baru'])[0]}
wa(r)}>💬
{r.status === 'baru' && setStatus(r, 'diproses')}>Proses}
{r.status === 'diproses' && setStatus(r, 'selesai')}>Selesai}
del(r)}>Hapus
))}
}
);
}
Object.assign(window, { ArticlesModule, RegistrationsModule, ART_CATS });