/* ============================================================
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