// shared.jsx — Logo, Nav, Footer, helpers used across pages const { useState, useEffect, useRef, useLayoutEffect } = React; /* ---------------------------------------------------------- * * Logo — recreated as inline SVG so we can theme the wordmark * * ---------------------------------------------------------- */ function LogoMark({ small }) { // Three overlapping shapes (red square / green triangle / blue circle) // matching the original logo geometry, plus a wordmark we can theme. const w = small ? 42 : 48; return ( VARIETIES ); } /* ---------------------------------------------------------- * * Hash router hook * * ---------------------------------------------------------- */ let __pendingAnchor = null; function __scrollToAnchor(id, behavior) { let tries = 0; const max = 30; const attempt = () => { const el = document.getElementById(id); if (el) { el.scrollIntoView({ behavior: behavior || 'instant', block: 'start' }); } else if (++tries < max) { requestAnimationFrame(attempt); } else { window.scrollTo({ top: 0, behavior: 'instant' }); } }; requestAnimationFrame(attempt); } function useHashRoute() { const [route, setRoute] = useState(() => window.location.hash.replace('#/', '') || 'home'); useEffect(() => { const onHash = () => { const r = window.location.hash.replace('#/', '') || 'home'; setRoute(r); if (__pendingAnchor) { const id = __pendingAnchor; __pendingAnchor = null; __scrollToAnchor(id, 'instant'); } else { window.scrollTo({ top: 0, behavior: 'instant' }); } }; window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); return route; } function navigate(to, anchor) { if (window.location.hash === '#/' + to) { if (anchor) { __scrollToAnchor(anchor, 'smooth'); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); } } else { if (anchor) __pendingAnchor = anchor; window.location.hash = '#/' + to; } } /* ---------------------------------------------------------- * * Reveal-on-scroll wrapper * * ---------------------------------------------------------- */ function Reveal({ children, delay = 0, as: As = 'div', className = '', ...rest }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setTimeout(() => setShown(true), delay); io.disconnect(); } }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); io.observe(el); return () => io.disconnect(); }, [delay]); return ( {children} ); } /* ---------------------------------------------------------- * * Nav * * ---------------------------------------------------------- */ function Nav({ route }) { const [scrolled, setScrolled] = useState(false); const [mobOpen, setMobOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); useEffect(() => { setMobOpen(false); }, [route]); const links = [ { to: 'home', label: 'ホーム' }, { to: 'services', label: 'サービス' }, { to: 'company', label: '会社概要' } ]; return (
{ e.preventDefault(); navigate('home'); }} >
{ e.preventDefault(); navigate('contact'); }} > お問い合わせ
{links.map((l) => ( { e.preventDefault(); navigate(l.to); setMobOpen(false); }} > {l.label} ))}
{ e.preventDefault(); navigate('contact'); setMobOpen(false); }} > お問い合わせ
); } /* ---------------------------------------------------------- * * Footer * * ---------------------------------------------------------- */ function Footer() { const year = new Date().getFullYear(); return ( ); } /* ---------------------------------------------------------- * * Page hero (sub pages) * * ---------------------------------------------------------- */ function PageHero({ crumb, ja, en, sub }) { return (
{crumb}

{ja} {en && {en}}

{sub &&

{sub}

}
); } Object.assign(window, { LogoMark, Nav, Footer, Reveal, PageHero, useHashRoute, navigate });