// 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 (
<>
{route === 'home' && (
)}
{route === 'listings' && (
)}
{route === 'sold' && }
{route === 'services' && }
{route === 'about' && }
{route === 'insights' && }
{route === 'contact' && }
{openListingData && (
setOpenListingData(null)}
setRoute={setRoute}
/>
)}
setTweaks('palette', paletteIdFromArray(arr))}
/>
setTweaks('headline', v)}
/>
setTweaks('heroVariant', v)}
/>
{SITE_DATA.nav.map(item => (
setRoute(item.id)} />
))}
>
);
};
ReactDOM.createRoot(document.getElementById('root')).render();