// 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 &&
}
setTweak('nodesLocked', v)} />
setTweak('navPosition', v)} />
setTweak('bubbleOpacity', v)} />
setTweak('bubbleNavGraphic', v)} />
setTweak('locationStyle', v)} />
setTweak('indexCardMode', v)} />
setTweak('imageLimit', v)} />
);
}
ReactDOM.createRoot(document.getElementById('root')).render();