// 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}
setTweak('dark', v)} />
setTweak('accent', v)} />
setTweak('fontPair', v)} />
setTweak('heroLayout', v)} />
setTweak('density', v)} />
setTweak('heroHeadline', v)} />
>
);
}
ReactDOM.createRoot(document.getElementById('root')).render();