/* ============================================================ APP — menyatukan foto, konsep interaksi, dan panel. Setelan tampilan dikunci permanen (panel Tweaks sudah dihapus). Ubah nilai di THEME ini kalau mau ganti tema situs. ============================================================ */ const THEME = { dark: true, // mode gelap accent: "#c6f432", // warna aksen (lime) motion: true, // animasi aktif videoBg: false, // video latar (nonaktif) videoAuto: true, videoDark: 0.62, cvUrl: "", // kosong → pakai link CV dari cv-data.js }; /* Gabungkan konten dari Firestore (site/content) di atas cv-data.js. Field yang tidak ada di Firestore otomatis pakai nilai cv-data.js. */ function mergeContent(base, c) { if (!c) return base; const out = { ...base }; ["name", "location", "tagline", "experience", "birth", "brandRoles", "typeWords", "tools"].forEach((k) => { if (c[k] !== undefined && c[k] !== null) out[k] = c[k]; }); out.defaults = { ...(base.defaults || {}), ...(c.defaults || {}) }; out.sections = { ...(base.sections || {}), ...(c.sections || {}) }; return out; } /* Baca dokumen konten secara realtime; null = belum ada / offline. */ function useContent() { const [content, setContent] = useState(null); useEffect(() => { if (!window.FB || !window.FB.db) return; const unsub = window.FB.db.collection("site").doc("content").onSnapshot( (snap) => { setContent(snap.exists ? snap.data() : null); }, (err) => { console.error("content snapshot:", err); } ); return () => unsub(); }, []); return content; } function App() { const staticData = window.CV_DATA; // Konten teks live dari Firestore (site/content); null → pakai cv-data.js. const content = useContent(); // Portofolio realtime dari Firestore; kalau kosong/offline → pakai seed di cv-data.js. const live = usePortfolio(); const staticPf = staticData.sections.portofolio || {}; const useSeed = !live.loaded || live.empty; const liveShorts = useSeed ? (staticPf.shorts || []) : live.shorts; const liveFeatured = useSeed ? (staticPf.featured || []) : live.featured; const data = React.useMemo(() => { const base = mergeContent(staticData, content); const pf = base.sections.portofolio || {}; return { ...base, sections: { ...base.sections, portofolio: { ...pf, shorts: liveShorts, featured: liveFeatured } }, }; }, [staticData, content, liveShorts, liveFeatured]); const defs = data.defaults || {}; const t = THEME; const [active, setActive] = useState(null); const accent = t.accent; const motion = t.motion !== false; const pick = useCallback((id) => { setActive((cur) => (cur === id ? null : id)); setPfWide(false); // buka panel selalu mulai dari mode biasa }, []); // Lightbox portofolio — { kind: "shorts"|"featured", index } | null const pf = data.sections.portofolio || {}; const pfShorts = pf.shorts || []; const pfFeatured = pf.featured || []; const [lightbox, setLightbox] = useState(null); const [pfWide, setPfWide] = useState(false); const lbList = lightbox ? (lightbox.kind === "featured" ? pfFeatured : pfShorts) : []; const navVideo = useCallback((dir) => { setLightbox((cur) => { if (!cur) return cur; const list = cur.kind === "featured" ? pfFeatured : pfShorts; if (!list.length) return cur; return { kind: cur.kind, index: (cur.index + dir + list.length) % list.length }; }); }, [pfShorts.length, pfFeatured.length]); // Koreografi intro: foto muncul di tengah → jeda → geser kiri + menu masuk. const [revealed, setRevealed] = useState(false); useEffect(() => { const id = setTimeout(() => setRevealed(true), 680); return () => clearTimeout(id); }, []); return (