import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import Alert from 'react-bootstrap/Alert'; import Col from 'react-bootstrap/Col'; import Row from 'react-bootstrap/Row'; import Spinner from 'react-bootstrap/Spinner'; import { fetchCv, filesForSection } from '../api/cv'; import { CvHeader } from '../components/cv/CvHeader'; import { CvSection } from '../components/cv/CvSection'; import { CvTimeline } from '../components/cv/CvTimeline'; import './CvPage.css'; export function CvPage() { const { hash } = useLocation(); const cvQ = useQuery({ queryKey: ['cv-gist'], queryFn: fetchCv, staleTime: 5 * 60_000, }); // Scroll to the anchored entry once the gist resolves and the section // body has rendered its ids. Re-runs if the user changes the hash while // already on the page. useEffect(() => { if (!cvQ.data || !hash) return; const target = document.getElementById(hash.slice(1)); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, [cvQ.data, hash]); if (cvQ.isLoading) { return ( <>
loading cv…
); } if (cvQ.isError) { const msg = (cvQ.error as Error).message; const rateHint = /403|rate limit/i.test(msg) ? ' (github limits unauthenticated requests to 60/hour per ip — try again shortly)' : ''; return ( <> cv unavailable

{msg} {rateHint}

); } const data = cvQ.data!; const bodySections = data.config.sections.filter((s) => s.placement === 'body'); const navSections = data.config.sections.filter((s) => s.placement === 'nav'); if (bodySections.length === 0 && navSections.length === 0) { return ( <> cv unavailable: no sections in config ); } return ( <> {bodySections.map((section) => ( ))} {navSections.map((section) => ( ))} ); }