/* global React, ReactDOM, Nav, Footer, GalleryPage, ProductPage, SampleRequestPage, CompareModal, Icon, useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakSelect */ const { useState, useEffect, useMemo, useCallback } = React; // ===== Tweak defaults ===== const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "default", "gridType": "editorial", "filterPos": "left-rail", "accent": "terracotta" }/*EDITMODE-END*/; // ===== Simple hash router ===== function useRouter() { const [path, setPath] = useState(() => parsePath()); useEffect(() => { const onHash = () => setPath(parsePath()); window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); const navigate = useCallback((to) => { window.location.hash = "#" + to; window.scrollTo({ top: 0, behavior: "instant" }); }, []); return [path, navigate]; } function parsePath() { const raw = (window.location.hash || "").replace(/^#/, "") || "/gallery"; const [pathname, queryStr] = raw.split("?"); const query = {}; if (queryStr) { queryStr.split("&").forEach(pair => { const [k, v] = pair.split("="); if (k) query[decodeURIComponent(k)] = decodeURIComponent(v || ""); }); } return { pathname: pathname || "/gallery", query }; } function App() { const [{ pathname, query }, navigate] = useRouter(); const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); const [catalog, setCatalog] = React.useState(window.PAPERS || []); const [catalogLoading, setCatalogLoading] = React.useState(true); const [catalogError, setCatalogError] = React.useState(null); React.useEffect(function() { if (window.IndieAPI) { window.IndieAPI.getCatalog() .then(function(data) { if (data && data.length > 0) setCatalog(data); setCatalogLoading(false); }) .catch(function(err) { setCatalogError(err.message); setCatalogLoading(false); }); } else { setCatalog(window.PAPERS || []); setCatalogLoading(false); } }, []); // Apply accent variant useEffect(() => { const root = document.documentElement; if (tweaks.accent === "yellow-ink") { root.style.setProperty("--accent", "#1A1A18"); root.style.setProperty("--accent-hover", "#000"); root.style.setProperty("--terracotta", "#BFA024"); root.style.setProperty("--terracotta-deep", "#9A8014"); } else if (tweaks.accent === "ink-only") { root.style.setProperty("--accent", "#1A1A18"); root.style.setProperty("--accent-hover", "#000"); root.style.setProperty("--terracotta", "#1A1A18"); root.style.setProperty("--terracotta-deep", "#000"); } else { // terracotta default — clear overrides root.style.removeProperty("--accent"); root.style.removeProperty("--accent-hover"); root.style.removeProperty("--terracotta"); root.style.removeProperty("--terracotta-deep"); } }, [tweaks.accent]); // ----- Cart / Compare state (in-memory) ----- const [cart, setCart] = useState([]); const [cartOpen, setCartOpen] = useState(false); const removeFromCart = useCallback((index) => { setCart(c => c.filter((_, i) => i !== index)); }, []); const updateCartQty = useCallback((index, qty) => { setCart(c => c.map((item, i) => i === index ? {...item, qty} : item)); }, []); const [lastOrderNumber, setLastOrderNumber] = useState(null); // ── Auth state ────────────────────────────────────────── const [authUser, setAuthUser] = useState(null); const [authProfile, setAuthProfile] = useState(null); const [authModalOpen, setAuthModalOpen] = useState(false); React.useEffect(() => { if (window.IndieAuth) { window.IndieAuth.init(); return window.IndieAuth.onAuthChange(({ session, profile }) => { setAuthUser(session?.user || null); setAuthProfile(profile || null); }); } }, []); const [compare, setCompare] = useState([]); const [compareOpen, setCompareOpen] = useState(false); const addToCart = useCallback((paper, opts = {}) => { setCart(c => [...c, { paper, format: opts.format || (Object.keys(paper.price || {})[0]) || "A4", qty: opts.qty || (paper.moq || 10), at: Date.now() }]); flashToast(`✓ Ditambahkan: ${paper.name}`); }, []); const toggleCompare = useCallback((paper) => { setCompare(c => { if (c.find(x => x.id === paper.id)) return c.filter(x => x.id !== paper.id); if (c.length >= 3) { flashToast("Maksimum 3 kertas untuk dibandingkan"); return c; } return [...c, paper]; }); }, []); const addSample = useCallback((paper) => { try { const cur = JSON.parse(localStorage.getItem("ip_samples") || "[]"); if (!cur.find(x => x.id === paper.id)) { cur.push({ id: paper.id, brand: paper.brand, name: paper.name, gsm: paper.gsm, finish: paper.finish, tone: paper.swatchTone }); localStorage.setItem("ip_samples", JSON.stringify(cur)); } } catch (e) {} }, []); // ----- Route dispatch ----- let page = null; if (pathname.startsWith("/product/")) { const id = pathname.replace("/product/", ""); page = ; } else if (pathname === "/sample-request") { page = ; } else if (pathname === "/profile") { page = ; } else if (pathname === "/orders") { page = ; } else if (pathname === "/about") { page = ; } else if (pathname === "/contact") { page = ; } else if (pathname === "/checkout") { page = { setLastOrderNumber(num); setCart([]); }} authProfile={authProfile} />; } else if (pathname.startsWith("/order-success")) { page = ; } else if (pathname.startsWith("/order-failed")) { page = ; } else { page = setCompareOpen(true)} query={query} catalog={catalog} catalogLoading={catalogLoading} catalogError={catalogError} />; } return ( <> setCartOpen(true)} authUser={authUser} authProfile={authProfile} onAuthClick={() => setAuthModalOpen(true)} onSignOut={() => window.IndieAuth.signOut()} onProfileClick={(tab) => navigate(tab === 'orders' ? '/orders' : '/profile')} /> {page} {authModalOpen && ( setAuthModalOpen(false)} onSuccess={() => setAuthModalOpen(false)} /> )} {cartOpen && ( setCartOpen(false)} onRemove={removeFromCart} onUpdateQty={updateCartQty} navigate={navigate} /> )} {/* Compare drawer (bottom) */} 0 ? " is-open" : "")}> 「 Compare 」 {compare.map(p => ( {p.name} toggleCompare(p)}> ))} {Array.from({ length: Math.max(0, 2 - compare.length) }).map((_, i) => ( + tambah {compare.length === 0 && i === 0 ? "kertas" : "lagi"} ))} setCompare([])} style={{color:"#fff", borderColor:"rgba(255,255,255,0.2)"}}>Clear setCompareOpen(true)} disabled={compare.length < 2}> Compare side-by-side → {compareOpen && ( setCompareOpen(false)} onRemove={(p) => toggleCompare(p)} addToCart={addToCart} navigate={navigate} /> )} setTweak("accent", v)} /> setTweak("gridType", v)} /> setTweak("density", v)} /> setTweak("filterPos", v)} /> navigate("/gallery")}>→ Gallery navigate("/product/arcoprint-milk-100")}>→ Product detail navigate("/sample-request")}>→ Sample request > ); } // ===== Toast ===== function flashToast(msg) { let el = document.getElementById("ip-toast"); if (!el) { el = document.createElement("div"); el.id = "ip-toast"; el.style.cssText = ` position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%) translateY(20px); background: var(--ink); color: var(--paper-bg); padding: 12px 22px; font-size: 13px; font-weight: 500; z-index: 99; opacity: 0; transition: opacity 200ms, transform 200ms; pointer-events: none; font-family: var(--font-sans); letter-spacing: 0.02em; `; document.body.appendChild(el); } el.textContent = msg; requestAnimationFrame(() => { el.style.opacity = "1"; el.style.transform = "translateX(-50%) translateY(0)"; }); clearTimeout(el._t); el._t = setTimeout(() => { el.style.opacity = "0"; el.style.transform = "translateX(-50%) translateY(20px)"; }, 1800); } ReactDOM.createRoot(document.getElementById("root")).render();