/* ============================================================ ADMIN + PORTOFOLIO LIVE (Firebase) - usePortfolio(): baca koleksi 'portfolio' dari Firestore (realtime). - AdminGate: muncul saat URL diakhiri #admin → login → kelola video. Hooks (useState/useEffect/dst) sudah di-destructure global di cv-components.jsx. ============================================================ */ /* ---------- Ekstrak ID YouTube dari berbagai bentuk link ---------- */ function ytId(input) { if (!input) return ""; const s = String(input).trim(); // Sudah berupa ID polos (11 karakter) if (/^[a-zA-Z0-9_-]{11}$/.test(s)) return s; try { const u = new URL(s.startsWith("http") ? s : "https://" + s); const host = u.hostname.replace(/^www\./, "").replace(/^m\./, ""); if (host === "youtu.be") { const id = u.pathname.slice(1, 12); return /^[a-zA-Z0-9_-]{11}$/.test(id) ? id : ""; } if (host.endsWith("youtube.com")) { const v = u.searchParams.get("v"); if (v && /^[a-zA-Z0-9_-]{11}$/.test(v)) return v; const m = u.pathname.match(/\/(?:shorts|embed|v|live)\/([a-zA-Z0-9_-]{11})/); if (m) return m[1]; } } catch (e) {} // Cadangan: cari pola ID di mana saja const m = s.match(/[a-zA-Z0-9_-]{11}/); return m ? m[0] : ""; } /* ---------- Hook: portofolio realtime ---------- */ function usePortfolio() { const [items, setItems] = useState([]); const [loaded, setLoaded] = useState(false); useEffect(() => { if (!window.FB || !window.FB.db) { setLoaded(true); return; } const unsub = window.FB.db .collection("portfolio") .orderBy("order", "asc") .onSnapshot( (snap) => { const arr = []; snap.forEach((doc) => { const d = doc.data() || {}; arr.push({ _docId: doc.id, id: d.videoId || "", title: d.title || "", kind: d.kind === "featured" ? "featured" : "shorts", order: typeof d.order === "number" ? d.order : 0, }); }); setItems(arr); setLoaded(true); }, (err) => { console.error("portfolio snapshot:", err); setLoaded(true); } ); return () => unsub(); }, []); const shorts = items.filter((x) => x.kind !== "featured"); const featured = items.filter((x) => x.kind === "featured"); return { shorts, featured, all: items, loaded, empty: loaded && items.length === 0 }; } /* ---------- Deteksi #admin di URL ---------- */ function useHashAdmin() { const read = () => typeof location !== "undefined" && location.hash.toLowerCase().replace(/\/$/, "") === "#admin"; const [on, setOn] = useState(read); useEffect(() => { const h = () => setOn(read()); window.addEventListener("hashchange", h); return () => window.removeEventListener("hashchange", h); }, []); const close = () => { if (location.hash) history.replaceState(null, "", location.pathname + location.search); setOn(false); }; return [on, close]; } /* ---------- Status login ---------- */ function useAuthUser() { const [user, setUser] = useState(null); const [ready, setReady] = useState(false); useEffect(() => { if (!window.FB || !window.FB.auth) { setReady(true); return; } const unsub = window.FB.auth.onAuthStateChanged((u) => { setUser(u); setReady(true); }); return () => unsub(); }, []); return { user, ready }; } /* ---------- Gerbang admin ---------- */ function AdminGate({ accent }) { const [open, close] = useHashAdmin(); if (!open) return null; return ; } function AdminOverlay({ accent, onClose }) { const { user, ready } = useAuthUser(); useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const noFB = !window.FB; return (
{noFB ? (
Firebase belum siap. Cek koneksi atau config.
) : !ready ? (
Memuat…
) : !user ? ( ) : ( )}
); } /* ---------- Form login ---------- */ function AdminLogin() { const [email, setEmail] = useState(""); const [pass, setPass] = useState(""); const [err, setErr] = useState(""); const [busy, setBusy] = useState(false); const submit = async () => { if (!email.trim() || !pass) { setErr("Email & password wajib diisi."); return; } setErr(""); setBusy(true); try { await window.FB.auth.signInWithEmailAndPassword(email.trim(), pass); } catch (e) { setErr("Gagal masuk. Email atau password salah."); } setBusy(false); }; return (
Panel Admin

Masuk

{err &&
{err}
}
); } /* ---------- Panel kelola video ---------- */ function AdminPanel({ user, onClose }) { const { shorts, featured, all } = usePortfolio(); const db = window.FB.db; const [title, setTitle] = useState(""); const [link, setLink] = useState(""); const [kind, setKind] = useState("shorts"); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState(null); // { type:"ok"|"err", text } const preview = ytId(link); const add = async () => { const vid = ytId(link); if (!title.trim()) return setMsg({ type: "err", text: "Judul belum diisi." }); if (!vid) return setMsg({ type: "err", text: "Link YouTube tidak terbaca. Coba salin ulang." }); setBusy(true); setMsg(null); try { const maxOrder = all.reduce((m, x) => Math.max(m, x.order || 0), 0); await db.collection("portfolio").add({ title: title.trim(), videoId: vid, kind, order: maxOrder + 1, createdAt: firebase.firestore.FieldValue.serverTimestamp(), }); setTitle(""); setLink(""); setMsg({ type: "ok", text: "Ditambahkan ke portofolio ✓" }); } catch (e) { setMsg({ type: "err", text: "Gagal menyimpan: " + (e.message || e) }); } setBusy(false); }; const remove = async (it) => { if (!window.confirm('Hapus "' + (it.title || "video") + '"?')) return; try { await db.collection("portfolio").doc(it._docId).delete(); } catch (e) { window.alert("Gagal hapus: " + (e.message || e)); } }; const rename = async (it) => { const t = window.prompt("Judul baru:", it.title); if (t == null) return; try { await db.collection("portfolio").doc(it._docId).update({ title: t.trim() }); } catch (e) { window.alert("Gagal ubah: " + (e.message || e)); } }; const move = async (list, idx, dir) => { const a = list[idx], b = list[idx + dir]; if (!a || !b) return; try { const batch = db.batch(); batch.update(db.collection("portfolio").doc(a._docId), { order: b.order }); batch.update(db.collection("portfolio").doc(b._docId), { order: a.order }); await batch.commit(); } catch (e) { window.alert("Gagal mengurutkan: " + (e.message || e)); } }; const logout = async () => { try { await window.FB.auth.signOut(); } catch (e) {} }; const renderList = (list, label) => (
{label}{list.length}
{list.length === 0 ? (
Belum ada.
) : ( list.map((it, i) => (
{it.title || "(tanpa judul)"}
{it.id}
)) )}
); return (
Panel Admin

Kelola Portofolio

{user.email}
{/* ---- Form tambah ---- */}
{preview ? (
Terbaca: {preview}
) : null} {msg &&
{msg.text}
}
{/* ---- Daftar ---- */} {renderList(shorts, "Shorts")} {renderList(featured, "Landscape")}
); }