// app-shell.jsx — shared state + header/footer/cart drawer used by every page // ───────────────────────────────────────────────────────────────────────────── // WIRED TO REAL API — FF_API (api-client.js) must load before this file. // localStorage is NO LONGER used for cart/user/orders. // ───────────────────────────────────────────────────────────────────────────── const { useState: useStateS, useEffect: useEffectS, useMemo: useMemoS } = React; // FF_STORE kept ONLY for non-critical UI prefs (theme, cardStyle) const FF_STORE = { get(k, d) { try { const v = localStorage.getItem(k); return v == null ? d : JSON.parse(v); } catch { return d; } }, set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch { } }, del(k) { try { localStorage.removeItem(k); } catch { } }, }; const FF_BASE = (() => { const p = location.pathname; return p.includes('/pages/') ? '../' : './'; })(); const FF_URLS = { home: '/', shop: '/shop', product: (id) => '/product/' + encodeURIComponent(id), cart: '/cart', checkout: '/checkout', order: (id) => '/order/' + encodeURIComponent(id), account: (tab) => '/account' + (tab ? '?tab=' + tab : ''), login: (next) => '/login' + (next ? '?next=' + encodeURIComponent(next) : ''), admin: '/admin', dev: '/pages/dev.html', search: (q) => '/search' + (q ? '?q=' + encodeURIComponent(q) : ''), category: (slug) => '/category/' + encodeURIComponent(slug), notfound: '/pages/404.html', }; const FF_QP = (k, d = '') => { const sp = new URLSearchParams(location.search); return sp.get(k) ?? d; }; // Get ID from clean URL path (/product/FF-123 or /order/FF-123) // Falls back to ?id= query param for backwards compat const FF_PATH_ID = () => { const segs = location.pathname.split('/').filter(Boolean); const last = segs[segs.length - 1]; return (last && last !== 'product' && last !== 'order' && last !== 'category') ? last : FF_QP('id'); }; // ───────────────────────────────────────────────────────────────────────────── // useAppState — THE CAVE KING // All state lives here. One change here = whole site updates. // ───────────────────────────────────────────────────────────────────────────── function useAppState() { const [cart, setCart] = useStateS([]); const [wishIds, setWishIds] = useStateS(new Set()); const [user, setUser] = useStateS(null); const [orders, setOrders] = useStateS([]); const [toast, setToast] = useStateS(''); const [cartOpen, setCartOpen] = useStateS(false); const [authOpen, setAuthOpen] = useStateS(false); const [loading, setLoading] = useStateS(true); // BOOT: load user + cart from API on every page load useEffectS(() => { const boot = async () => { if (typeof FF_API === 'undefined') { console.error('FF_API not defined — api-client.js must load before app-shell.jsx'); setLoading(false); return; } try { if (FF_API.auth.isLoggedIn()) { const me = await FF_API.auth.me(); if (me) setUser(me); } const cartData = await FF_API.cart.get(); if (cartData?.items) setCart(cartData.items); // Load wishlist IDs for heart state on cards if (typeof FF_API !== 'undefined' && FF_API.wishlist) { FF_API.wishlist.ids().then(ids => setWishIds(new Set(ids))).catch(() => { }); } } catch (err) { console.warn('Boot error:', err); } finally { setLoading(false); if (window.__ffHideBoot) window.__ffHideBoot(); } }; boot(); }, []); // ADD TO CART — guest check + stock enforcement const addToCart = async (product, qty = 1) => { // Guest — show login prompt instead of silent fail if (!user) { setToast('Sign in to add to your bag ✦'); setTimeout(() => setOpenLogin(true), 600); return; } try { const updated = await FF_API.cart.add(product.id, qty); if (!updated?.items) throw new Error('No response'); setCart(updated.items); setToast('Added to bag ✦'); setCartOpen(true); if (window.FF_TRACK) window.FF_TRACK('add_to_cart', { product_id: product.id, name: product.name, price: product.price }); } catch (err) { const msg = err?.message || ''; if (msg.includes('stock') || msg.includes('Stock')) setToast(msg); else if (msg.includes('401') || msg.includes('auth')) { setToast('Sign in to add to bag'); setOpenLogin(true); } else setToast('Could not add to cart — try again'); } }; // UPDATE QTY — was: setCart(map) | now: PUT /api/cart/items/:id const updateQty = async (productId, qty) => { try { const updated = await FF_API.cart.update(productId, qty); setCart(updated.items); } catch (err) { console.error('updateQty error:', err); } }; // REMOVE ITEM — was: setCart(filter) | now: DELETE /api/cart/items/:id const removeItem = async (productId) => { try { const updated = await FF_API.cart.remove(productId); setCart(updated.items); } catch (err) { console.error('removeItem error:', err); } }; const clearCart = () => setCart([]); // LOGIN — was: setUser(mock) | now: POST /api/auth/login const onLogin = async (email, password) => { try { const { ok, data } = await FF_API.auth.login(email, password); if (!ok) return { ok: false, error: data.error || 'Login failed' }; setUser(data.user); setAuthOpen(false); setToast('Welcome back ' + data.user.name.split(' ')[0] + ' \u2736'); const cartData = await FF_API.cart.get(); if (cartData?.items) setCart(cartData.items); return { ok: true }; } catch (err) { return { ok: false, error: 'Login failed' }; } }; // REGISTER — was: nothing | now: POST /api/auth/register const onRegister = async (email, password, name, phone) => { try { const { ok, data } = await FF_API.auth.register(email, password, name, phone); if (!ok) return { ok: false, error: data.errors?.[0]?.msg || data.error || 'Registration failed' }; setUser(data.user); setAuthOpen(false); setToast('Welcome to Fussy Finds, ' + data.user.name.split(' ')[0] + ' \u2736'); const cartData = await FF_API.cart.get(); if (cartData?.items) setCart(cartData.items); return { ok: true }; } catch (err) { return { ok: false, error: 'Registration failed' }; } }; // LOGOUT — was: setUser(null) | now: POST /api/auth/logout + clear state const onLogout = async () => { await FF_API.auth.logout().catch(() => { }); setUser(null); setCart([]); setOrders([]); setToast('Logged out. See you soon \u2736'); location.href = FF_URLS.home; }; // PLACE ORDER — was: push fake order to localStorage array // now: order already in DB, just clear cart + return id const placeOrder = (orderId) => { setCart([]); return orderId; }; // LOAD ORDERS — for account page const loadOrders = async () => { if (!user) return; try { const data = await FF_API.orders.list(); setOrders(data); } catch (err) { console.error('loadOrders error:', err); } }; // Expose toast globally for pages that don't have it in props React.useEffect(() => { window.FF_TOAST = setToast; }, [setToast]); const toggleWishlist = async (productId) => { if (!user) { setToast('Sign in to save to wishlist ♡'); setTimeout(() => setAuthOpen(true), 500); return; } const was = wishIds.has(productId); setWishIds(prev => { const n = new Set(prev); was ? n.delete(productId) : n.add(productId); return n; }); setToast(was ? 'Removed from wishlist' : 'Saved to wishlist ♥'); try { await FF_API.wishlist.toggle(productId); } catch { setWishIds(prev => { const n = new Set(prev); was ? n.add(productId) : n.delete(productId); return n; }); } }; return { cart, setCart, user, setUser, orders, setOrders, wishIds, toggleWishlist, refreshUser: async () => { const me = await FF_API.auth.me(); if (me?.id) setUser(me); }, toast, setToast, cartOpen, setCartOpen, authOpen, setAuthOpen, loading, addToCart, updateQty, removeItem, clearCart, onLogin, onRegister, onLogout, login: onLogin, logout: onLogout, placeOrder, loadOrders, }; } // --- goto shim ----------------------------------------------------------- function makeGoto() { return (r = {}) => { if (r.openLogin) { location.href = FF_URLS.login(location.pathname + location.search); return; } switch (r.page) { case 'home': location.href = FF_URLS.home; break; case 'shop': if (r.filter === 'viral') location.href = FF_URLS.shop + '?filter=viral'; else if (r.filter === 'new') location.href = FF_URLS.shop + '?filter=new'; else if (r.cat) location.href = FF_URLS.shop + '?cat=' + r.cat; else if (r.q) location.href = FF_URLS.search(r.q); else location.href = FF_URLS.shop; break; case 'pdp': location.href = FF_URLS.product(r.id); break; case 'cart': location.href = FF_URLS.cart; break; case 'checkout': location.href = FF_URLS.checkout; break; case 'order-confirm': location.href = FF_URLS.order(r.id); break; case 'account': location.href = FF_URLS.account(r.tab); break; case 'admin': location.href = FF_URLS.admin; break; case 'dev': location.href = FF_URLS.dev; break; default: location.href = FF_URLS.home; } }; } // --- AppShell ----------------------------------------------------------- // ── Page loading bar ────────────────────────────────────────────────────────── function PageLoader() { const [visible, setVisible] = React.useState(false); const [width, setWidth] = React.useState(0); React.useEffect(() => { // Show bar when navigating (click on tags) const handleClick = (e) => { const a = e.target.closest('a[href]'); if (!a) return; const href = a.getAttribute('href'); // Only internal page navigations if (!href || href.startsWith('#') || href.startsWith('mailto') || href.startsWith('http')) return; if (a.target === '_blank') return; setVisible(true); setWidth(20); setTimeout(() => setWidth(70), 100); setTimeout(() => setWidth(90), 800); }; document.addEventListener('click', handleClick); // Complete bar when page loads setVisible(true); setWidth(20); setTimeout(() => setWidth(60), 200); setTimeout(() => { setWidth(100); setTimeout(() => setVisible(false), 300); }, 600); return () => document.removeEventListener('click', handleClick); }, []); if (!visible) return null; return (
); } // ── NotFoundPage — fallback for unknown routes (also defined in pages-extra) ── if (typeof NotFoundPage === 'undefined') { window.NotFoundPage = function NotFoundPage() { return React.createElement('main', { style: { textAlign: 'center', padding: '80px 24px' } }, React.createElement('div', { style: { fontSize: 48, marginBottom: 16 } }, '404'), React.createElement('h2', { style: { fontFamily: 'var(--ff-font-display)', fontSize: 32, marginBottom: 8 } }, 'Page not found'), React.createElement('p', { style: { color: 'var(--ff-muted)', marginBottom: 24 } }, "This page doesn't exist."), React.createElement('a', { href: '/', style: { color: 'var(--ff-accent)', fontWeight: 600 } }, '← Back home') ); }; } function AppShell({ children, accent = '#E4439C', cardStyle = 'sticker', hideChrome = false, requireAuth = false }) { const s = useAppState(); const goto = makeGoto(); const [search, setSearch] = useStateS(''); useEffectS(() => { document.documentElement.style.setProperty('--ff-accent', accent); }, [accent]); useEffectS(() => { if (!s.loading && requireAuth && !s.user) { location.href = FF_URLS.login(location.pathname + location.search); } }, [requireAuth, s.user, s.loading]); if (requireAuth && !s.loading && !s.user) { return React.createElement('div', { style: { padding: 60, textAlign: 'center', color: 'var(--ff-muted)' } }, 'Redirecting to login\u2026'); } const ctx = { ...s, goto, search, setSearch, cardStyle }; if (hideChrome) { return React.createElement(React.Fragment, null, typeof children === 'function' ? children(ctx) : children, React.createElement(PageLoader), React.createElement(Toast, { toast: s.toast, onDismiss: () => s.setToast('') }) ); } return React.createElement(React.Fragment, null, React.createElement(Header, { goto, cart: s.cart, openCart: () => s.setCartOpen(true), user: s.user, onLogin: () => s.setAuthOpen(true), search, setSearch }), typeof children === 'function' ? children(ctx) : children, React.createElement(Footer, null), (typeof CartDrawer !== 'undefined') && React.createElement(CartDrawer, { open: s.cartOpen, onClose: () => s.setCartOpen(false), cart: s.cart, updateQty: s.updateQty, removeItem: s.removeItem, goto }), (typeof AuthModal !== 'undefined') && React.createElement(AuthModal, { open: s.authOpen, onClose: () => s.setAuthOpen(false), onLogin: s.onLogin, onRegister: s.onRegister, refreshUser: async () => { const me = await FF_API.auth.me(); if (me?.id) s.setUser(me); } }), typeof ThemeSwitcher !== 'undefined' ? React.createElement(ThemeSwitcher) : null, React.createElement(Toast, { toast: s.toast, onDismiss: () => s.setToast('') }) ); } Object.assign(window, { FF_STORE, FF_URLS, FF_BASE, FF_QP, useAppState, makeGoto, AppShell });