// app.jsx — Jaxon Stickler portfolio shell. // Views: about (landing + diagram) · index (horizontal grid) · project const { useState, useEffect, useRef } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "locationStyle": "both", "indexCardMode": "title", "imageLimit": 6, "navPosition": "bottom-left", "bubbleOpacity": 1, "bubbleNavGraphic": false, "nodesLocked": true } /*EDITMODE-END*/; // hover readout for the diagram — positioned dynamically below the bio panel function KeepAlive({ active, children }) { // Keeps a subtree mounted (images/iframes stay loaded) but visually removed // when inactive — so switching pages never re-downloads or re-renders it. return
{children}
; } function DiagramReadout({ p, top }) { return (
{p &&
{p.title}
{p.year}
}
); } // hover readout for the index grid (when cards are off) function IndexReadout({ p }) { return (
{p &&
{p.title}
{p.year}
{p.keywords &&
{p.keywords.join(', ')}
}
}
); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const isMobile = useMobile(); const [view, setView] = useState(() => location.hash.slice(1) || (window.innerWidth < 768 ? 'index' : 'about')); const [hovered, setHovered] = useState(null); const bioRef = useRef(null); const [readoutTop, setReadoutTop] = useState(500); useEffect(() => { const onHash = () => {setView(location.hash.slice(1) || 'about');setHovered(null);}; window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); useEffect(() => {window.scrollTo(0, 0);}, [view]); // Mobile has no About page — always show Index useEffect(() => { if (isMobile && view === 'about') go('index'); }, [isMobile, view]); // Track bio panel bottom for DiagramReadout positioning useEffect(() => { if (!bioRef.current) return; const update = () => { if (bioRef.current) { const r = bioRef.current.getBoundingClientRect(); setReadoutTop(Math.round(r.bottom) + 20); } }; update(); const ro = new ResizeObserver(update); ro.observe(bioRef.current); return () => ro.disconnect(); }, [view]); const go = (v) => {location.hash = v === 'about' ? '' : v;}; const openProject = (p) => {location.hash = p.id;}; const project = view !== 'about' && view !== 'index' ? window.PROJECTS.find((p) => p.id === view) : null; const isIndex = view === 'index'; const isAbout = !project && !isIndex; let footerLeft = []; // bubble appearance globals (read by BubbleNav + bubble backgrounds) window.__navGraphic = t.bubbleNavGraphic; if (typeof document !== 'undefined') { document.documentElement.style.setProperty('--bubble-op', t.bubbleOpacity); } return (
{isAbout && !isMobile && } {/* Entity 2 stays mounted across pages so it never pops back in */} {!isMobile && } {/* Index stays mounted so its images never re-load when returning */} {!isMobile && } {isIndex && isMobile && } {isIndex && !isMobile && {t.indexCardMode === 'off' && } } {project && }
); } ReactDOM.createRoot(document.getElementById('root')).render();