// app.jsx — main App with routing + theme + tweaks const ACCENTS = [ '#D4A24C', // signature gold (business card) '#F4D35E', // butter / brighter gold '#F39137', // harjog orange (from logo gradient) '#E63E1F', // harjog red (deep end of logo) '#E8FF5C', // electric chartreuse '#41E0B8', // mint ]; const FONT_PAIRS = [ { value: 'butler', label: 'Butler' }, // Cormorant Garamond — luxury high-contrast serif { value: 'brand', label: 'Brand' }, // Anton uppercase + Playfair Display italic — billboard editorial { value: 'editorial', label: 'Editorial' }, // Instrument Serif headlines { value: 'sans', label: 'Sans' }, // Sora all-around { value: 'technical', label: 'Mono' }, // JetBrains Mono headlines ]; const HERO_LAYOUTS = [ { value: 'editorial', label: 'Editorial' }, { value: 'centered', label: 'Centered' }, { value: 'marquee', label: 'Marquee' }, ]; const DENSITIES = ['compact', 'comfortable', 'spacious']; // Decide readable ink color on top of an accent function isLightHex(hex) { const h = String(hex).replace('#', ''); const x = h.length === 3 ? h.replace(/./g, c => c + c) : h.padEnd(6, '0'); const n = parseInt(x.slice(0, 6), 16); if (Number.isNaN(n)) return true; const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; return r * 299 + g * 587 + b * 114 > 148000; } function App() { const [tweaks, setTweak] = useTweaks(window.TWEAK_DEFAULTS); // Read initial route from the URL path so reloading/sharing a URL lands on the right page const getRouteFromPath = () => window.location.pathname.replace(/^\//, '') || 'home'; const [route, setRouteState] = React.useState(getRouteFromPath); // Every section has its own HTML file with a preloader — navigate there directly const FULL_PAGE_HREFS = { services: '/services', work: '/work', about: '/about', contact: '/contact', book: '/book', privacy: '/privacy', disclaimer: '/disclaimer', }; const setRoute = React.useCallback((newRoute) => { if (!newRoute || newRoute === 'home') { window.location.href = '/'; return; } if (FULL_PAGE_HREFS[newRoute]) { window.location.href = FULL_PAGE_HREFS[newRoute]; return; } // service/brand-identity → /service-brand-identity if (newRoute.startsWith('service/')) { window.location.href = '/service-' + newRoute.slice(8); return; } // case/township-launch → /case-township-launch if (newRoute.startsWith('case/')) { window.location.href = '/case-' + newRoute.slice(5); return; } setRouteState(newRoute); window.history.pushState({ route: newRoute }, '', '/' + newRoute); }, []); // Disable browser scroll-restoration and wire up back/forward buttons React.useEffect(() => { window.history.scrollRestoration = 'manual'; // Stamp the current entry so the very first popstate has state window.history.replaceState( { route: getRouteFromPath() }, '', window.location.pathname ); const onPop = (e) => { setRouteState((e.state && e.state.route) || getRouteFromPath() || 'home'); }; window.addEventListener('popstate', onPop); return () => window.removeEventListener('popstate', onPop); }, []); // Dismiss preloader once React has painted React.useEffect(() => { const el = document.getElementById('preloader'); if (!el) return; // Give the fill animation a moment to feel satisfying before fading out const t = setTimeout(() => { el.classList.add('pre-done'); setTimeout(() => el.remove(), 700); }, 1800); return () => clearTimeout(t); }, []); // Apply theme attributes globally React.useEffect(() => { const root = document.documentElement; root.dataset.theme = tweaks.dark ? 'dark' : 'light'; root.dataset.density = tweaks.density; root.dataset.font = tweaks.fontPair; // Remove inline font vars set by fonts.js so [data-font] CSS rules control them ['--f-h', '--f-h-weight', '--f-h-tracking', '--f-h-italic'].forEach(v => root.style.removeProperty(v)); root.style.setProperty('--accent', tweaks.accent); root.style.setProperty('--accent-ink', isLightHex(tweaks.accent) ? '#0a0a0a' : '#fafafa'); }, [tweaks.dark, tweaks.density, tweaks.fontPair, tweaks.accent]); // Scroll to top on every route change React.useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }, [route]); // GSAP animations — re-initialise on every route mount React.useEffect(() => { if (!window.initGSAPAnimations) return; const cleanup = window.initGSAPAnimations(); // Extra refresh after the preloader fully fades (home page only) let refreshId; if (route === 'home') { refreshId = setTimeout(() => { if (window.ScrollTrigger) window.ScrollTrigger.refresh(); }, 2700); } return () => { if (refreshId) clearTimeout(refreshId); if (typeof cleanup === 'function') cleanup(); }; }, [route]); // Render current page let page; if (route.startsWith('case/')) { const slug = route.slice(5); page = ; } else if (route.startsWith('service/')) { const slug = route.slice(8); page = ; } else { switch (route) { case 'services': page = ; break; case 'work': page = ; break; case 'about': page = ; break; case 'contact': page = ; break; default: page = ; } } // Top-level nav route (strip 'case/...' or 'service/...' to its parent) const navRoute = route.startsWith('case/') ? 'work' : route.startsWith('service/') ? 'services' : route; return ( <>
setTweak('dark', v)} /> {route !== 'home' && ( )}
{page}