/* ============================================================ KOMPONEN INTI — Interactive CV PhotoSlot, Panel info, 3 konsep interaksi, render konten. ============================================================ */ const { useState, useEffect, useRef, useCallback } = React; /* ---------- Foto interaktif (drag & drop, tersimpan) ---------- */ /* ---------- Foto profil (terkunci — efek hover tetap, tapi tidak bisa diganti) ---------- */ function PhotoSlot({ accent, styleMode, defaultSrc }) { const [hover, setHover] = useState(false); const [intro, setIntro] = useState(true); const shown = defaultSrc || null; useEffect(() => { const id = setTimeout(() => setIntro(false), 1050); return () => clearTimeout(id); }, []); return (
setHover(true)} onMouseLeave={() => setHover(false)} > {shown ? ( Foto Ian Septian
) : (
FOTO PORTRAIT assets/images/profile/me.png
)} {/* bingkai siku-siku — animasi buka dari tengah saat load */}
); } /* ---------- Teks latar bergerak (typewriter) ---------- */ function BigType({ words, motion }) { const [txt, setTxt] = useState((words && words[0]) || ""); useEffect(() => { if (!words || !words.length) return; if (!motion) { setTxt(words[0]); return; } let w = 0, c = 0, deleting = false, timer; const tick = () => { const word = words[w % words.length]; if (!deleting) { c++; setTxt(word.slice(0, c)); if (c >= word.length) { deleting = true; timer = setTimeout(tick, 1500); return; } timer = setTimeout(tick, 95); } else { c--; setTxt(word.slice(0, Math.max(0, c))); if (c <= 0) { deleting = false; w++; timer = setTimeout(tick, 300); return; } timer = setTimeout(tick, 45); } }; timer = setTimeout(tick, 600); return () => clearTimeout(timer); }, [motion, words]); return ( ); } /* ---------- Pager (tombol ‹ halaman › ) ---------- */ function Pager({ page, total, accent, onPrev, onNext }) { if (total <= 1) return null; return (
{page + 1} / {total}
); } /* ---------- Portofolio (paginasi otomatis; lebih banyak saat mode lebar) ---------- */ const SHORTS_PER_PAGE = 6; // mode normal: 3 kolom × 2 baris const SHORTS_PER_PAGE_WIDE = 10; // mode lebar: 5 kolom × 2 baris const FEAT_PER_PAGE = 1; // mode normal const FEAT_PER_PAGE_WIDE = 2; // mode lebar: 2 berdampingan function PortfolioContent({ s, accent, onPlayVideo, expanded }) { const shorts = s.shorts || []; const featured = s.featured || []; const shPer = expanded ? SHORTS_PER_PAGE_WIDE : SHORTS_PER_PAGE; const ftPer = expanded ? FEAT_PER_PAGE_WIDE : FEAT_PER_PAGE; const [shPage, setShPage] = useState(0); const [ftPage, setFtPage] = useState(0); const shTotal = Math.max(1, Math.ceil(shorts.length / shPer)); const ftTotal = Math.max(1, Math.ceil(featured.length / ftPer)); // Jaga halaman tetap valid saat jumlah per-halaman berubah (toggle lebar). const shCur = Math.min(shPage, shTotal - 1); const ftCur = Math.min(ftPage, ftTotal - 1); const shStart = shCur * shPer; const ftStart = ftCur * ftPer; const shView = shorts.slice(shStart, shStart + shPer); const ftView = featured.slice(ftStart, ftStart + ftPer); return (

{s.body}

{shorts.length > 0 && (
Shorts setShPage((p) => (Math.min(p, shTotal - 1) - 1 + shTotal) % shTotal)} onNext={() => setShPage((p) => (Math.min(p, shTotal - 1) + 1) % shTotal)} />
{shView.map((v, i) => { const gi = shStart + i; return ( ); })}
)} {featured.length > 0 && (
Landscape setFtPage((p) => (Math.min(p, ftTotal - 1) - 1 + ftTotal) % ftTotal)} onNext={() => setFtPage((p) => (Math.min(p, ftTotal - 1) + 1) % ftTotal)} />
{ftView.map((v, i) => { const gi = ftStart + i; return ( ); })}
)}
); } /* ---------- Tombol kontak 3D (klik = langsung buka) ---------- Ikon (ContactIcon) & iconKindFromType berasal dari cv-icons.jsx. */ function ContactButtons({ channels, accent }) { const list = channels || []; // Jenis ikon: pakai pilihan eksplisit (c.icon) jika ada, kalau tidak tebak dari teks. const kindOf = (c) => (c.icon ? c.icon : iconKindFromType(c.type)); const urlFor = (c, kind) => { if (c.href) return c.href; const v = String(c.value || "").replace(/^@/, "").trim(); if (kind === "instagram") return "https://instagram.com/" + v; if (kind === "tiktok") return "https://www.tiktok.com/@" + v; if (kind === "youtube") return "https://youtube.com/@" + v; if (kind === "facebook") return "https://facebook.com/" + v; if (kind === "x") return "https://x.com/" + v; if (kind === "linkedin") return "https://www.linkedin.com/in/" + v; if (kind === "telegram") return "https://t.me/" + v; if (kind === "whatsapp") return "https://wa.me/" + v.replace(/[^0-9]/g, ""); if (kind === "email") return "mailto:" + String(c.value || "").trim(); if (kind === "website") { const raw = String(c.value || "").trim(); return /^https?:\/\//i.test(raw) ? raw : "https://" + raw; } return ""; }; const onClick = (c, kind) => { const url = urlFor(c, kind); if (!url) return; if (kind === "email") { window.location.href = url; return; } window.open(url, "_blank", "noopener,noreferrer"); }; return (
{list.map((c, i) => { const kind = kindOf(c); return ( ); })}
); } /* ---------- Konten tiap seksi ---------- */ function SectionContent({ id, data, accent, onPlayVideo, expanded }) { const s = data.sections[id]; if (!s) return null; if (id === "tentang") { return (

{s.body}

fun fact

{s.funfact}

  • Peran{data.role}
  • Lokasi{data.location}
); } if (id === "pengalaman") { return (
    {s.items.map((it, i) => (
  1. {it.period} {it.current && sekarang}

    {it.company}

    {it.role}

    {it.note}

  2. ))}
); } if (id === "pendidikan") { return (
{s.items.map((it, i) => (
{it.period}

{it.school}

{it.degree}

{it.note}

))}
); } if (id === "keahlian") { return (
{s.items.map((it, i) => (
{String(i + 1).padStart(2, "0")}

{it.name}

{it.note}

))}
); } if (id === "portofolio") { return ; } if (id === "kontak") { return (

{s.body}

); } return null; } /* ---------- Panel info (sheet melayang) ---------- */ function InfoPanel({ id, data, accent, onClose, onPlayVideo, expanded, onToggleExpand }) { const open = !!id; const s = id ? data.sections[id] : null; const canExpand = id === "portofolio"; const wide = canExpand && expanded; // Mundur selangkah: kalau sedang full screen → kembali ke mode biasa dulu; // kalau sudah biasa → baru tutup panel. const dismiss = () => { if (wide) onToggleExpand(); else onClose(); }; useEffect(() => { const onKey = (e) => { if (e.key === "Escape") dismiss(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [wide, onToggleExpand, onClose]); return (
); } /* ---------- Unduh CV + preview saat hover ---------- */ function CvDownload({ accent, url, defaultPreview }) { const PKEY = "ian_cv_preview"; const [preview, setPreview] = useState(null); const inputRef = useRef(null); const shownPrev = preview || defaultPreview || null; useEffect(() => { const load = () => { try { setPreview(localStorage.getItem(PKEY) || null); } catch (e) {} }; load(); const onPick = () => inputRef.current && inputRef.current.click(); const onClear = () => { try { localStorage.removeItem(PKEY); } catch (e) {} setPreview(null); }; window.addEventListener("cv-pick-preview", onPick); window.addEventListener("cv-clear-preview", onClear); return () => { window.removeEventListener("cv-pick-preview", onPick); window.removeEventListener("cv-clear-preview", onClear); }; }, []); const handleFile = (file) => { if (!file || !file.type.startsWith("image/")) return; const r = new FileReader(); r.onload = (e) => { const d = e.target.result; setPreview(d); try { localStorage.setItem(PKEY, d); } catch (err) {} }; r.readAsDataURL(file); }; return ( ); } /* ---------- Lightbox player (YouTube Shorts 9:16) ---------- */ function VideoLightbox({ state, videos, land, accent, onClose, onNav }) { useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); else if (e.key === "ArrowRight") onNav(1); else if (e.key === "ArrowLeft") onNav(-1); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose, onNav]); const open = state != null; const v = open ? videos[state] : null; return (
{v && (