// App entry — picks the page based on window.PAGE_NAME const { Nav, Footer } = window.Shared; const { HomePage, AboutPage, ServicesPage, ExperiencePage, ContactPage } = window.Pages; const { TweaksPanel, useTweaks, TweakSection, TweakColor, TweakToggle, TweakRadio } = window; const PAGES = { home: { Comp: HomePage, key: 'home' }, about: { Comp: AboutPage, key: 'about' }, services: { Comp: ServicesPage, key: 'services' }, experience: { Comp: ExperiencePage, key: 'experience' }, contact: { Comp: ContactPage, key: 'contact' }, }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": ["#6B53FF","#FF6FA8"] }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const route = PAGES[window.PAGE_NAME] || PAGES.home; const PageComp = route.Comp; const progressRef = React.useRef(null); React.useEffect(()=>{ document.documentElement.style.setProperty('--indigo', t.palette[0]); document.documentElement.style.setProperty('--pink', t.palette[1]); }, [t.palette]); // Dismiss splash once React has mounted + first paint completed React.useEffect(()=>{ requestAnimationFrame(()=> document.body.classList.add('app-ready')); }, []); // Scroll-progress bar — updates on scroll across every page React.useEffect(()=>{ let rafId = null; const update = () => { rafId = null; const el = progressRef.current; if (!el) return; const h = document.documentElement; const max = (h.scrollHeight - h.clientHeight); const pct = max > 0 ? Math.min(100, Math.max(0, (h.scrollTop / max) * 100)) : 0; el.style.width = pct + '%'; }; const onScroll = () => { if (rafId != null) return; rafId = requestAnimationFrame(update); }; update(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return ()=> { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); if (rafId != null) cancelAnimationFrame(rafId); }; }, []); // Smooth same-site navigation: replay splash on the way out React.useEffect(()=>{ const sameSitePages = ['/', '/about', '/services', '/experience', '/contact']; const onClick = (e) => { const a = e.target.closest('a'); if (!a) return; const href = a.getAttribute('href') || ''; // hash links, mailto, tel, external, new-tab clicks: ignore if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('http')) return; if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; // Match against known internal pages (with optional #fragment) const [path] = href.split('#'); if (!sameSitePages.includes(path)) return; e.preventDefault(); document.body.classList.remove('app-ready'); // If served from a sub-path (preview, local file://), clean URLs like "/about" // won't resolve. Detect that and rewrite to the .html file relative to here. let dest = href; const onApacheRoot = window.location.pathname === '/' || /\/(about|services|experience|contact)$/.test(window.location.pathname); if (!onApacheRoot && href.startsWith('/')) { const [p, frag] = href.split('#'); const file = p === '/' ? 'index.html' : p.slice(1) + '.html'; dest = file + (frag ? '#' + frag : ''); } setTimeout(()=>{ window.location.href = dest; }, 260); }; document.addEventListener('click', onClick); return ()=> document.removeEventListener('click', onClick); }, []); return (
); } ReactDOM.createRoot(document.getElementById('root')).render();