/* ── Komponen bersama: ikon, Header, Footer, UI kecil ── */ const { useState, useEffect } = React; const IC = { menu: , x: , heart: , star: , sun: , moon: , search: , home: , doc: , }; /* ── Toggle Mode Gelap (header) ── */ function ThemeToggle() { const [dark, setDark] = useState(() => document.documentElement.dataset.theme === 'dark'); const toggle = () => { const next = !dark; document.documentElement.dataset.theme = next ? 'dark' : 'light'; try { localStorage.setItem('mqa_theme', next ? 'dark' : 'light'); } catch {} setDark(next); }; return ( ); } const NAV = [ { key: 'home', label: 'Beranda' }, { key: 'articles', label: 'Artikel' }, { key: 'programs', label: 'Program' }, { key: 'teachers', label: 'Pengajar' }, { key: 'kegiatan', label: 'Kegiatan' }, { key: 'gallery', label: 'Fasilitas' }, { key: 'toko', label: 'Toko' }, { key: 'donasi', label: 'Donasi' }, { key: 'about', label: 'Tentang' }, { key: 'contact', label: 'Kontak' }, ]; function Header({ navigate, page, user }) { const [mobileOpen, setMobileOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const go = (k) => { navigate(k); setMobileOpen(false); }; return ( <> {searchOpen && setSearchOpen(false)} />}
go('home')}> Markaz Qurrota A'yunRumah Qur'an Qurrota A'yun
{user ? : }
Markaz Qurrota A'yun
{NAV.map(n => ( ))}
); } function Footer({ navigate }) { const [s, setS] = useState({}); useEffect(() => { api.get('content.php?action=get_section§ion=settings').then(r => { if (r.ok && r.data && typeof r.data === 'object' && !Array.isArray(r.data)) setS(r.data); }).catch(() => {}); }, []); const programs = ["Tahfidz Al-Qur'an", 'Bahasa Arab', 'Fiqih Ibadah', 'Akhlaq & Sirah']; const navLinks = [['home', 'Beranda'], ['articles', 'Artikel'], ['teachers', 'Pengajar'], ['gallery', 'Fasilitas'], ['contact', 'Kontak']]; const support = [['donasi', 'Donasi'], ['donasi', 'Wakaf'], ['donasi', 'Beasiswa'], ['donasi', 'Transparansi']]; const norm = (u) => u ? (/^https?:/.test(u) ? u : 'https://' + u) : null; const waLink = s.wa ? 'https://wa.me/' + String(s.wa).replace(/\D/g, '') : (s.phone ? 'https://wa.me/' + String(s.phone).replace(/\D/g, '').replace(/^0/, '62') : 'https://wa.me/628123456789'); const socials = [ ['WhatsApp', waLink, '', true], ['Instagram', norm(s.ig), '', !!s.ig], ['YouTube', norm(s.yt), '', !!s.yt], ['Facebook', norm(s.fb), '', !!s.fb], ['TikTok', norm(s.tiktok), '', !!s.tiktok], ].filter(x => x[3]); return ( ); } function WAButton() { return ; } function DonasiFAB({ navigate }) { return ( ); } function SectionHeader({ label, title, sub, center }) { return (
{label}
{title}
{sub &&
{sub}
}
); } const fmtRp = (n) => 'Rp ' + (n || 0).toLocaleString('id-ID'); /* Format ringkas untuk kartu statistik sempit (mis. Rp 176,7 jt) */ const fmtRpShort = (n) => { n = n || 0; if (n >= 1e9) return 'Rp ' + (n / 1e9).toLocaleString('id-ID', { maximumFractionDigits: 1 }) + ' M'; if (n >= 1e6) return 'Rp ' + (n / 1e6).toLocaleString('id-ID', { maximumFractionDigits: 1 }) + ' jt'; if (n >= 1e3) return 'Rp ' + Math.round(n / 1e3) + ' rb'; return 'Rp ' + n.toLocaleString('id-ID'); }; /* ── Kategori artikel: warna & label ── */ const CATS = { 'Tahfidz': { color: '#1F6B47', bg: 'rgba(31,107,71,.10)' }, 'Bahasa Arab': { color: '#1B2F6E', bg: 'rgba(27,47,110,.10)' }, 'Fiqih': { color: '#E8912A', bg: 'rgba(232,145,42,.12)' }, 'Akhlaq': { color: '#8B5A2B', bg: 'rgba(139,90,43,.12)' }, }; // Palet untuk kategori BARU (otomatis dapat warna konsisten per nama) const CAT_PALETTE = ['#1F6B47', '#1B2F6E', '#E8912A', '#8B5A2B', '#7A4E8C', '#0E7C86', '#B23A48', '#3A6EA5']; function hexToRgba(hex, a) { const n = parseInt(hex.slice(1), 16); return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`; } const catStyle = (c) => { if (CATS[c]) return CATS[c]; if (!c) return { color: 'var(--muted)', bg: 'var(--bg2)' }; // warna deterministik dari nama kategori let h = 0; for (let i = 0; i < c.length; i++) h = (h * 31 + c.charCodeAt(i)) >>> 0; const color = CAT_PALETTE[h % CAT_PALETTE.length]; const style = { color, bg: hexToRgba(color, 0.11) }; CATS[c] = style; // cache agar konsisten return style; }; window.catStyle = catStyle; /* Ikon garis elegan per kategori (pengganti emoji childish saat cover kosong) */ const _gi = (p) => "" + p + ""; const GLYPH = { book: "", pen: "", scale: "", moon: "", scroll: "", cap: "", calendar:"", hands: "", mosque: "", }; const CAT_GLYPH = { 'Tahfidz': 'book', 'Bahasa Arab': 'pen', 'Fiqih': 'scale', 'Akhlaq': 'moon', 'Hadits': 'scroll' }; function glyphURL(name) { return "url(\"data:image/svg+xml," + _gi(GLYPH[name] || GLYPH.book) + "\")"; } function GlyphMark({ name, size }) { return ; } window.GLYPH = GLYPH; window.glyphURL = glyphURL; window.GlyphMark = GlyphMark; window.CAT_GLYPH = CAT_GLYPH; /* Ikon default per kategori (PHP asli tak punya kolom icon) */ const CAT_ICON = { 'Tahfidz': '📖', 'Bahasa Arab': '✍️', 'Fiqih': '⚖️', 'Akhlaq': '🌙' }; /* Normalisasi artikel: PHP asli pakai author_name/read_time/published_at, mock pakai author/readTime/date. Komponen membaca satu bentuk seragam. */ function normArticle(a) { if (!a) return a; const author = a.author || a.author_name || 'Tim MQA'; let date = a.date; if (!date && a.published_at) { try { date = new Date(a.published_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); } catch { date = ''; } } return { ...a, author, authorInitial: a.authorInitial || (author ? author[0] : '?'), readTime: a.readTime || a.read_time || '5 menit', date: date || '', icon: a.icon || CAT_ICON[a.category] || '📖', slug: a.slug || slugify(a.title || ('artikel-' + (a.id || ''))), }; } function slugify(t) { return String(t).toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); } window.slugify = slugify; window.normArticle = normArticle; /* ── Markdown ringan & AMAN (escape dulu, lalu format subset) ── Dukungan: ## H2, ### H3, **tebal**, *miring*, - poin, 1. nomor, > kutipan, [teks](url) */ function renderMD(src) { if (!src) return ''; // Jika sudah HTML (dari editor WYSIWYG), tampilkan apa adanya if (/<(p|h2|h3|ul|ol|li|blockquote|br|div|strong|em|b|i|a)[\s>]/i.test(src)) return src; const esc = (t) => t.replace(/&/g, '&').replace(//g, '>'); const inline = (t) => esc(t) .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/(^|[^*])\*([^*]+)\*/g, '$1$2') .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '
$1'); const lines = String(src).replace(/\r/g, '').split('\n'); const isAr = (t) => { const ar = (t.match(/[\u0600-\u06ff]/g) || []).length; return ar > 0 && ar / t.replace(/\s/g, '').length > 0.4; }; const pTag = (t) => isAr(t) ? '

' + inline(t) + '

' : '

' + inline(t) + '

'; let html = '', list = null; const closeList = () => { if (list) { html += list === 'ul' ? '' : ''; list = null; } }; for (let raw of lines) { const line = raw.trim(); if (!line) { closeList(); continue; } let m; if ((m = line.match(/^###\s+(.*)/))) { closeList(); html += '

' + inline(m[1]) + '

'; } else if ((m = line.match(/^##\s+(.*)/))) { closeList(); html += '

' + inline(m[1]) + '

'; } else if ((m = line.match(/^>\s+(.*)/))) { closeList(); const t = m[1]; html += isAr(t) ? '
' + inline(t) + '
' : '
' + inline(t) + '
'; } else if ((m = line.match(/^[-*]\s+(.*)/))) { if (list !== 'ul') { closeList(); html += '
    '; list = 'ul'; } html += '
  • ' + inline(m[1]) + '
  • '; } else if ((m = line.match(/^\d+\.\s+(.*)/))) { if (list !== 'ol') { closeList(); html += '
      '; list = 'ol'; } html += '
    1. ' + inline(m[1]) + '
    2. '; } else { closeList(); html += pTag(line); } } closeList(); return html; } window.renderMD = renderMD; function CatBadge({ cat, small }) { const s = catStyle(cat); return ( {cat} ); } /* ── Cover artikel: foto asli bila ada, atau pola geometris Islami yang elegan ── */ const STAR_TILE = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='44'%20height='44'%20viewBox='0%200%2044%2044'%3E%3Cg%20fill='none'%20stroke='%23ffffff'%20stroke-opacity='0.16'%20stroke-width='1.3'%3E%3Crect%20x='10'%20y='10'%20width='24'%20height='24'/%3E%3Crect%20x='10'%20y='10'%20width='24'%20height='24'%20transform='rotate(45%2022%2022)'/%3E%3C/g%3E%3C/svg%3E\")"; function CoverArt({ a, ratio, iconSize, badge }) { const s = catStyle(a.category); return (
      {a.cover_url ? : <>
      } {badge && {a.category}}
      ); } /* ── Kartu artikel (anchor ke URL SEO; klik = SPA instan) ── */ function ArticleCard({ a: raw, onClick, featured }) { const a = normArticle(raw); const s = catStyle(a.category); const href = '/artikel/' + a.slug; const handle = (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return; e.preventDefault(); onClick && onClick(); }; if (featured) { return (

      {a.title}

      {a.excerpt}

      {a.authorInitial || (a.author || '?')[0]} {a.author} ·{a.date}·{a.readTime}
      ); } return (

      {a.title}

      {a.excerpt}

      {a.author}·{a.readTime}
      ); } Object.assign(window, { IC, NAV, Header, Footer, WAButton, DonasiFAB, SectionHeader, fmtRp, fmtRpShort, CATS, catStyle, CatBadge, ArticleCard }); /* ── YouTube: ambil ID dari berbagai format URL → URL embed ── */ function ytId(url) { if (!url) return null; const m = String(url).match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/|live\/))([\w-]{11})/); return m ? m[1] : (/^[\w-]{11}$/.test(url) ? url : null); } function ytThumb(url) { const id = ytId(url); return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null; } /* Blok media: embed video YouTube bila ada, jika tidak gambar, jika tidak fallback */ function MediaBlock({ youtube, image, fallbackIcon, glyph, ratio = '16/9', bg }) { const id = ytId(youtube); const [play, setPlay] = useState(false); if (id) { return (
      {play ?