// Main app shell const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": "sage", "headline": "newsreader", "heroVariant": "skyline" }/*EDITMODE-END*/; const PALETTE_OPTIONS = [ ["#F6F2EA", "#4D6149", "#1F2A24"], // sage ["#F4F1EC", "#1F1F1B", "#15140F"], // noir ["#FAF4EC", "#B4654A", "#2A1F18"], // clay ["#F2F3F0", "#2E4B5A", "#1B2A33"], // coast ]; const PALETTE_IDS = ["sage", "noir", "clay", "coast"]; const paletteIdFromArray = (arr) => { const i = PALETTE_OPTIONS.findIndex(p => p.join() === arr.join()); return i >= 0 ? PALETTE_IDS[i] : "sage"; }; const paletteArrayFromId = (id) => PALETTE_OPTIONS[PALETTE_IDS.indexOf(id)] || PALETTE_OPTIONS[0]; const HEADLINE_FONTS = [ { value: "newsreader", label: "Newsreader" }, { value: "cormorant", label: "Cormorant" }, { value: "playfair", label: "Playfair" }, { value: "dmserif", label: "DM Serif" }, ]; const HERO_VARIANTS = [ { value: "skyline", label: "Skyline" }, { value: "interior", label: "Interior" }, { value: "exterior", label: "Exterior" }, ]; const App = () => { const [route, setRoute] = React.useState('home'); const [openListingData, setOpenListingData] = React.useState(null); const [faves, setFaves] = React.useState(() => { try { return JSON.parse(localStorage.getItem('faves') || '[]'); } catch (e) { return []; } }); const [toast, setToast] = React.useState(null); const [mobileOpen, setMobileOpen] = React.useState(false); const [lang, setLang] = React.useState(() => localStorage.getItem('lang') || 'en'); const [tweaks, setTweaks] = useTweaks(TWEAK_DEFAULTS); const t = React.useMemo(() => window.makeT(lang), [lang]); React.useEffect(() => { localStorage.setItem('lang', lang); document.documentElement.lang = lang; }, [lang]); // Apply palette + headline to root React.useEffect(() => { document.documentElement.dataset.palette = tweaks.palette; document.documentElement.dataset.headline = tweaks.headline; }, [tweaks.palette, tweaks.headline]); // Persist faves React.useEffect(() => { localStorage.setItem('faves', JSON.stringify(faves)); }, [faves]); const toggleFav = (id) => { setFaves(f => f.includes(id) ? f.filter(x => x !== id) : [...f, id]); }; const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 3200); }; // Scroll to top on route change React.useEffect(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }, [route]); // Close the mobile menu if the window is widened to desktop, and lock // body scroll while it's open so it can never leak onto the page. React.useEffect(() => { const onResize = () => { if (window.innerWidth > 940) setMobileOpen(false); }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); React.useEffect(() => { document.body.style.overflow = mobileOpen ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [mobileOpen]); const onDarkHero = route === 'home'; return ( <>