/* Shared hooks + small components (attached to window) */ const { useState, useEffect, useRef } = React; /* Reveal-on-scroll wrapper */ const Reveal = ({ as: Tag = "div", delay = 0, className = "", children, ...p }) => { const ref = useRef(null); const [show, setShow] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver( ([e]) => { if (e.isIntersecting) { setShow(true); io.disconnect(); } }, { threshold: 0.12, rootMargin: "0px 0px -40px 0px" } ); io.observe(el); return () => io.disconnect(); }, []); const d = delay ? ` d${delay}` : ""; return {children}; }; /* Count-up number when visible */ const CountUp = ({ value }) => { const ref = useRef(null); const [txt, setTxt] = useState("0"); useEffect(() => { const el = ref.current; if (!el) return; // parse number, keep separators const target = parseFloat(value.replace(/[^\d,.-]/g, "").replace(/\./g, "").replace(",", ".")) || 0; const io = new IntersectionObserver(([e]) => { if (!e.isIntersecting) return; io.disconnect(); const dur = 1400, t0 = performance.now(); const fmt = (n) => { const r = Math.round(n); return value.includes(".") ? r.toLocaleString("id-ID") : String(r); }; const tick = (t) => { const k = Math.min(1, (t - t0) / dur); const e2 = 1 - Math.pow(1 - k, 3); setTxt(fmt(target * e2)); if (k < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, { threshold: 0.4 }); io.observe(el); return () => io.disconnect(); }, [value]); return {txt}; }; window.Reveal = Reveal; window.CountUp = CountUp; /* Paginated list controller: returns {page, setPage, pageItems, pages, total, range} */ const usePaged = (items, perPage, deps = []) => { const [page, setPage] = useState(1); const pages = Math.max(1, Math.ceil(items.length / perPage)); useEffect(() => { setPage(1); }, deps); // reset when deps (e.g. filter) change const safe = Math.min(page, pages); const start = (safe - 1) * perPage; const pageItems = items.slice(start, start + perPage); return { page: safe, setPage, pageItems, pages, total: items.length, from: items.length ? start + 1 : 0, to: Math.min(start + perPage, items.length) }; }; /* Pager UI with windowed page numbers + ellipses */ const Pager = ({ page, pages, onChange }) => { if (pages <= 1) return null; const nums = []; for (let i = 1; i <= pages; i++) { if (i === 1 || i === pages || Math.abs(i - page) <= 1) nums.push(i); else if (nums[nums.length - 1] !== "…") nums.push("…"); } const goto = (n) => { onChange(n); }; return (
{nums.map((n, i) => n === "…" ? : )}
); }; window.usePaged = usePaged; window.Pager = Pager;