// portfolio-parts.jsx — shared pieces for the Jaxon Stickler site. // Exposes on window: FONT, GalleryItem, DepthBackground, CustomCursor, // LocationSignifier, BioAbout, BioShort, Footer const { useState: usePState, useEffect: usePEffect, useRef: usePRef } = React; // Mobile detection hook — shared globally (other files use window.useMobile) function useMobile() { const [m, setM] = usePState(() => window.innerWidth < 768); usePEffect(() => { const h = () => setM(window.innerWidth < 768); window.addEventListener('resize', h); return () => window.removeEventListener('resize', h); }, []); return m; } const FONT = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"; const DEPTH_BG = 'images/about/depth-bg.png'; // ---------------------------------------------------------------- gallery item function GalleryItem({ item, rounded }) { const radius = rounded ? 4 : 0; if (item.type === 'video') { return ( ); } if (item.type === 'vimeo') { const vimeoSrc = item.src + (item.src.includes('?') ? '&muted=1' : '?muted=1'); return (
); } if (item.type === 'youtube') { const ytSrc = item.src + (item.src.includes('?') ? '&' : '?') + 'rel=0'; return (
); } return ( ); } // ---------------------------------------------------------- depth cloud bg // Mounts the WebGL2 cursor-lit point cloud (depth-cloud.js) behind everything. function DepthCloud() { const ref = usePRef(null); usePEffect(() => { if (!ref.current || !window.initDepthCloud) return; const cleanup = window.initDepthCloud(ref.current); return cleanup; }, []); return ( ); } // ---------------------------------------------------------------- custom cursor (+) function CustomCursor({ enabled = true }) { const ref = usePRef(null); usePEffect(() => { if (!enabled) return; if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) return; const el = ref.current; if (!el) return; const styleEl = document.createElement('style'); styleEl.textContent = '* { cursor: none !important; }'; document.head.appendChild(styleEl); const onMove = (e) => { const t = e.target; const interactive = t && typeof t.closest === 'function' && t.closest('a, button, [data-clickable]'); el.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%) scale(${interactive ? 0.5 : 1})`; el.style.opacity = interactive ? '1' : '0.65'; }; window.addEventListener('mousemove', onMove); return () => { window.removeEventListener('mousemove', onMove); styleEl.remove(); }; }, [enabled]); if (!enabled) return null; return (
); } // ---------------------------------------------- live "current location" signifier // style: 'pulse' | 'clock' | 'both' function LocationSignifier({ style = 'both', city = 'Berlin', tz = 'Europe/Berlin' }) { const [now, setNow] = usePState(''); usePEffect(() => { if (style === 'pulse') return; const fmt = () => { try { return new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: tz }).format(new Date()); } catch (_) {return '';} }; setNow(fmt()); const id = setInterval(() => setNow(fmt()), 1000); return () => clearInterval(id); }, [style, tz]); const dot = ; return ( {(style === 'pulse' || style === 'both') && dot} Current Location: {city} {(style === 'clock' || style === 'both') && now && {now} } ); } // ---------------------------------------------------------------- bio bubbles const BUBBLE = { background: 'rgba(0,0,0,var(--bubble-op,1))', borderRadius: 15, color: '#fff', fontSize: 12, lineHeight: 1.5, fontFamily: FONT }; // Top-of-bubble nav: About / state-glyph / Index. The middle glyph is a "+" // normally and a "○" while a project is open. Self-contained — navigates via hash // so it can be dropped into any bubble without prop threading. function BubbleNav() { const cur = (location.hash.slice(1) || (window.innerWidth < 768 ? 'index' : 'about')); const isProject = cur && cur !== 'index' && cur !== 'about'; const mobile = window.innerWidth < 768; const graphic = window.__navGraphic; const go = (v) => { location.hash = (v === 'about' ? '' : v); }; const btn = (active) => ({ fontFamily: FONT, color: '#fff', fontSize: 12, letterSpacing: '0.02em', background: 'none', border: 'none', padding: 0, cursor: 'pointer', fontWeight: 700, opacity: active ? 1 : 0.5, }); return (
{!mobile ? : } {graphic ? : +}
); } const BioAbout = React.forwardRef(function BioAbout({ locationStyle, mobile = false }, ref) { const italic = { opacity: 0.92 }; const [wip1, setWip1] = usePState(false); const [wip2, setWip2] = usePState(false); const [mx, setMx] = usePState(0); const [my, setMy] = usePState(0); usePEffect(() => { const onMove = (e) => { setMx(e.clientX); setMy(e.clientY); }; window.addEventListener('mousemove', onMove); return () => window.removeEventListener('mousemove', onMove); }, []); return (
Jaxon Stickler
Designer/Artist
Blessed & Cursed artefacts for a collapsing world. Combining advanced digital fabrication with raw & reclaimed materials, working at the scale of objects, bodies & sites, my work engages the darkness & otherness of ecological & technological systems to visualize new futures.
Open to collaborative projects, commissions and design/art services.
Working on:
setWip1(true)} onMouseLeave={() => setWip1(false)}> embodied machine workflow for drawing forth spirit from the technosphere
setWip2(true)} onMouseLeave={() => setWip2(false)}> custom software for Creative Living Infrastructure
Contact:
Instagram: @jaxonstickler
Download CV {(wip1 || wip2) && ReactDOM.createPortal(
, document.body )}
); }); function BioShort({ locationStyle, top = 19, mobile = false, contact = true }) { return (
Jaxon Stickler
Artist/Designer
{contact &&
Contact:
Instagram: @jaxonstickler
}
); } // ---------------------------------------------------------------- footer / nav // navPosition: 'bottom-left' | 'top-right' | 'top-center' function Footer({ left, navPosition = 'bottom-left' }) { const base = { fontFamily: FONT, color: '#fff', fontSize: 12, cursor: 'pointer', background: 'none', border: 'none', padding: 0, letterSpacing: '0.02em' }; const posStyle = navPosition === 'top-right' ? { position: 'fixed', right: 32, top: 22, zIndex: 40, display: 'flex', alignItems: 'baseline', gap: 32 } : navPosition === 'top-center' ? { position: 'fixed', left: '50%', top: 22, transform: 'translateX(-50%)', zIndex: 40, display: 'flex', alignItems: 'baseline', gap: 32 } : { position: 'fixed', left: 43, bottom: 26, zIndex: 40, display: 'flex', alignItems: 'baseline', gap: 32 }; const copyrightStyle = navPosition !== 'bottom-left' ? { position: 'fixed', left: 43, bottom: 26, zIndex: 40 } : null; return (
{left.map((l, i) => )} {navPosition === 'bottom-left' && Jaxon Stickler 2026© }
{navPosition !== 'bottom-left' &&
Jaxon Stickler 2026©
}
); } // ---------------------------------------------------------------- vector entities // Animated "living edges" entity — persistent overlay across every view. function AnimatedEntity() { const isMob = useMobile(); if (isMob) return null; return (