feat: show contextual navigation loading

This commit is contained in:
2026-09-16 15:10:37 +03:30
parent 9119eb6c29
commit 083a4e0b7b
23 changed files with 242 additions and 40 deletions

View File

@@ -1,31 +1,50 @@
'use client';
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
useTransition,
type ReactNode,
} from 'react';
import { usePathname, useRouter } from 'next/navigation';
const PortfolioRouteContext = createContext<(() => void) | null>(null);
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;
@@ -51,22 +70,35 @@ export function PortfolioRouteShell({
}, [homePath, pathname]);
const dismissCaseStudy = useCallback(() => {
if (canReturnWithBack.current) {
router.back();
return;
}
router.replace(homePath, { scroll: false });
setKind('dismiss');
setTarget(homePath);
startTransition(() => {
if (canReturnWithBack.current) router.back();
else router.replace(homePath, { scroll: false });
});
}, [homePath, router]);
return (
<PortfolioRouteContext.Provider value={dismissCaseStudy}>
{children}
</PortfolioRouteContext.Provider>
<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>
);
}
export function useDismissCaseStudy() {
const dismiss = useContext(PortfolioRouteContext);
if (!dismiss) throw new Error('useDismissCaseStudy must be used within PortfolioRouteShell');
return dismiss;
}