105 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|