Files
my-protfolio/src/components/PortfolioRouteShell.tsx

105 lines
3.9 KiB
TypeScript

'use client';
import {
useCallback,
useEffect,
useRef,
useState,
useTransition,
type ReactNode,
} from 'react';
import { usePathname, useRouter } from 'next/navigation';
import type { PublicPortfolio } from '@/server/content';
import { PortfolioNavigationContext } from './PortfolioNavigation';
import { CaseStudyDrawer } from './CaseStudyDrawer';
import { CaseStudyLoading } from './CaseStudyLoading';
import { LoadingIndicator } from './LoadingIndicator';
export function PortfolioRouteShell({
children,
homePath,
background,
portfolio,
}: {
children: ReactNode;
homePath: string;
background: ReactNode;
portfolio: PublicPortfolio;
}) {
const pathname = usePathname();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [target, setTarget] = useState<string | null>(null);
const [kind, setKind] = useState<'project' | 'language' | 'dismiss'>('project');
const pending = isPending && target !== null;
const languagePending = pending && kind === 'language';
const showDrawer = pathname !== homePath || (pending && kind === 'project');
const previousPath = useRef(pathname);
const canReturnWithBack = useRef(false);
const returnScrollY = useRef<number | null>(null);
const previousScrollRestoration = useRef<ScrollRestoration>('auto');
useEffect(() => {
const onPageShow = (event: PageTransitionEvent) => { if (event.persisted) setTarget(null); };
window.addEventListener('pageshow', onPageShow);
return () => window.removeEventListener('pageshow', onPageShow);
}, []);
useEffect(() => {
if (pathname === homePath) {
canReturnWithBack.current = false;
if (previousPath.current !== homePath && returnScrollY.current !== null) {
const scrollY = returnScrollY.current;
requestAnimationFrame(() => window.scrollTo({ top: scrollY, behavior: 'instant' }));
window.setTimeout(() => window.scrollTo({ top: scrollY, behavior: 'instant' }), 120);
window.setTimeout(() => {
window.scrollTo({ top: scrollY, behavior: 'instant' });
history.scrollRestoration = previousScrollRestoration.current;
}, 600);
returnScrollY.current = null;
delete document.documentElement.dataset.caseStudyScrollY;
}
} else if (previousPath.current === homePath) {
canReturnWithBack.current = true;
const recordedScrollY = Number(document.documentElement.dataset.caseStudyScrollY);
returnScrollY.current = Number.isFinite(recordedScrollY) ? recordedScrollY : window.scrollY;
previousScrollRestoration.current = history.scrollRestoration;
history.scrollRestoration = 'manual';
}
previousPath.current = pathname;
}, [homePath, pathname]);
const dismissCaseStudy = useCallback(() => {
setKind('dismiss');
setTarget(homePath);
startTransition(() => {
if (canReturnWithBack.current) router.back();
else router.replace(homePath, { scroll: false });
});
}, [homePath, router]);
return (
<PortfolioNavigationContext.Provider value={{
dismiss: dismissCaseStudy,
loadingLabel: portfolio.messages.loading.project,
navigate: (href, options) => {
if (href === pathname && !pending) return;
setTarget(href);
setKind(options.kind ?? 'project');
startTransition(() => {
if (options.replace) router.replace(href, { scroll: options.scroll });
else router.push(href, { scroll: options.scroll });
});
},
}}>
<div inert={languagePending} aria-busy={languagePending || undefined}>
{background}
{showDrawer ? <CaseStudyDrawer portfolio={portfolio}>
{pending && kind === 'project' ? <CaseStudyLoading /> : children}
</CaseStudyDrawer> : children}
</div>
{languagePending && <LoadingIndicator label={portfolio.messages.loading.language} fullPage />}
</PortfolioNavigationContext.Provider>
);
}