// Editorial Luxury — router + boot + tweaks const { useEffect: uE, useState: uS } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": "cream-wood", "fontPair": "cormorant-inter", "hero": "fullbleed", "density": "comfy", "planTone": "cream", "showProgress": true }/*EDITMODE-END*/; // Apply tweaks → CSS variables on :root const applyTweaks = (t) => { const root = document.documentElement; // Palettes const palettes = { // основной — Nordic-blue (по референсу заказчика nordic.rent) "cream-wood": { bg: "#f7f7f9", bg2: "#ececef", paper: "#ffffff", ink: "#14181c", inkSoft: "#2c333d", mute: "#6e7682", gold: "#066aab", goldDeep: "#044270" }, "ivory-slate": { bg: "#f1f3f6", bg2: "#e2e6eb", paper: "#ffffff", ink: "#181d28", inkSoft: "#3a4250", mute: "#727a86", gold: "#3a4853", goldDeep: "#1f2530" }, "bronze-noir": { bg: "#e9edf2", bg2: "#d6dde5", paper: "#fbfcfd", ink: "#0b0e12", inkSoft: "#262b35", mute: "#5e6571", gold: "#2c4ee4", goldDeep: "#1a2f8a" } }; const p = palettes[t.palette] || palettes["cream-wood"]; root.style.setProperty('--bg', p.bg); root.style.setProperty('--bg-2', p.bg2); root.style.setProperty('--paper', p.paper); root.style.setProperty('--ink', p.ink); root.style.setProperty('--ink-soft', p.inkSoft); root.style.setProperty('--mute', p.mute); root.style.setProperty('--gold', p.gold); root.style.setProperty('--gold-deep', p.goldDeep); // Fonts const fonts = { // Основная пара — Roboto на весь сайт (референс nordic-arbat.ru) "cormorant-inter": { serif: "'Roboto', system-ui, sans-serif", sans: "'Roboto', system-ui, sans-serif" }, "playfair-dm": { serif: "'Playfair Display', serif", sans: "'DM Sans', system-ui, sans-serif" }, "cormorant-jost": { serif: "'Bodoni Moda', serif", sans: "'Jost', system-ui, sans-serif" } }; const f = fonts[t.fontPair] || fonts["cormorant-inter"]; root.style.setProperty('--serif', f.serif); root.style.setProperty('--sans', f.sans); // Density root.setAttribute('data-density', t.density || 'comfy'); // Hero treatment root.setAttribute('data-hero', t.hero || 'fullbleed'); // Plan tone root.setAttribute('data-plan-tone', t.planTone || 'cream'); }; // Scroll progress bar const ScrollProgress = ({ show }) => { const [p, setP] = uS(0); uE(() => { if (!show) return; const f = () => { const h = document.documentElement; const max = h.scrollHeight - h.clientHeight; setP(max > 0 ? (h.scrollTop / max) * 100 : 0); }; f(); window.addEventListener('scroll', f, { passive: true }); return () => window.removeEventListener('scroll', f); }, [show]); if (!show) return null; return (
); }; const EditorialApp = () => { const [route, setRoute] = uS(window.location.hash || '#/'); const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); uE(() => { applyTweaks(t); }, [t]); const [, setBoot] = uS(0); // Загружаем кабинеты и настройки из БД → подменяем статические значения data.js, // чтобы правки администратора отражались на публичном сайте. uE(() => { (async () => { try { const [r, s] = await Promise.all([window.EH_API.rooms(), window.EH_API.settings()]); if (r && r.rooms && r.rooms.length) window.EH_DATA.rooms = r.rooms; if (s && s.settings) { const st = s.settings; const open = st.open || '08:00', close = st.close || '22:00'; window.EH_DATA.brand = Object.assign({}, window.EH_DATA.brand, { address: st.address || window.EH_DATA.brand.address, phone: st.phone || window.EH_DATA.brand.phone, email: st.email || window.EH_DATA.brand.email, open: open, close: close, weekend: st.weekend || '—', hours: (st.weekend && st.weekend !== '—') ? `Пн–Сб · ${open}–${close}` : `Пн–Вс · ${open}–${close}`, abonementPrice: parseInt(st.abonement_price, 10) || window.EH_DATA.brand.abonementPrice, abonementHours: parseInt(st.abonement_hours, 10) || window.EH_DATA.brand.abonementHours, abonementDayPrice: parseInt(st.abonement_day_price, 10) || window.EH_DATA.brand.abonementDayPrice || 1000, vk: st.vk || window.EH_DATA.brand.vk, instagram: st.instagram || window.EH_DATA.brand.instagram, maxGroup: st.max_group || window.EH_DATA.brand.maxGroup, maxDm: st.max_dm || window.EH_DATA.brand.maxDm, }); // Картинки, загруженные админом (img_<слот> → url), подменяют стандартные const siteImages = {}; Object.keys(st).forEach(k => { if (k.indexOf('img_') === 0 && st[k]) siteImages[k.slice(4)] = st[k]; }); window.EH_DATA.siteImages = siteImages; // Реквизиты для оплаты переводом по QR window.EH_DATA.pay = { name: st.pay_name || '', inn: st.pay_inn || '', account: st.pay_account || '', bank: st.pay_bank || '', bic: st.pay_bic || '', corr: st.pay_corr || '', }; } setBoot(b => b + 1); } catch (e) { /* нет связи — остаются значения из data.js */ } })(); }, []); uE(() => { const f = () => setRoute(window.location.hash || '#/'); window.addEventListener('hashchange', f); return () => window.removeEventListener('hashchange', f); }, []); uE(() => { if (route.startsWith('#/')) window.scrollTo({top: 0, behavior: 'instant'}); }, [route]); // Прокрутка к якорю (#about, #book, #contact и т.д.) — даже если перешли с другой страницы uE(() => { const h = window.location.hash; if (!h || h.startsWith('#/')) return; const id = h.slice(1); // Опрашиваем DOM до 3 сек чтобы дождаться маунта лендинга, и прокручиваем const deadline = Date.now() + 3000; let scrolled = false; const tryScroll = () => { if (scrolled) return; const el = document.getElementById(id); if (el) { scrolled = true; // Сначала мгновенно, потом плавно — чтобы дать React закончить рендер и не дёргать пользователя el.scrollIntoView({ behavior: 'auto', block: 'start' }); setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 30); } else if (Date.now() < deadline) { setTimeout(tryScroll, 80); } }; setTimeout(tryScroll, 60); }, [route]); const r = route.startsWith('#/') ? route : '#/'; let Page, pageKey; if (r.startsWith('#/login')) { Page = ; pageKey = 'login'; } else if (r.startsWith('#/register')) { Page = ; pageKey = 'register'; } else if (r.startsWith('#/cabinet')) { Page = ; pageKey = 'cabinet'; } else if (r.startsWith('#/admin')) { Page = ; pageKey = 'admin'; } else if (r.startsWith('#/reviews')) { Page = ; pageKey = 'reviews'; } else if (r.startsWith('#/privacy')) { Page = ; pageKey = 'privacy'; } else if (r.startsWith('#/offer')) { Page = ; pageKey = 'offer'; } else { Page = ; pageKey = 'landing'; } return ( <>
{Page}
{ const k = o[0] === "#f1f3f6" ? "ivory-slate" : o[0] === "#e9edf2" ? "bronze-noir" : "cream-wood"; setTweak('palette', k); }} /> setTweak('fontPair', v)} /> setTweak('density', v)} /> setTweak('hero', v)} /> setTweak('planTone', v)} /> setTweak('showProgress', v)} /> ); }; ReactDOM.createRoot(document.getElementById('root')).render();