82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { isNativePlatform } from '@/lib/platform';
|
|
|
|
/**
|
|
* Hover / touch prefetch for the primary navigation destinations.
|
|
*
|
|
* The route chunks stay lazy (so first paint is not blocked), but once the user
|
|
* shows intent — hovers a menu row, tabs into a link, or touches it — the page
|
|
* module starts downloading. On mobile the touchstart head start is often enough
|
|
* to avoid the page skeleton entirely. Only modules are fetched; no page hooks
|
|
* mount, so this never triggers relay work or an Amber prompt.
|
|
*/
|
|
const loaders: Record<string, () => Promise<unknown>> = {
|
|
'/lab': () => import('@/pages/LabPage'),
|
|
'/events': () => import('@/pages/EventsPage'),
|
|
'/crews': () => import('@/pages/CrewsPage'),
|
|
'/community': () => import('@/pages/CommunityPage'),
|
|
'/skaters': () => import('@/pages/SkatersPage'),
|
|
'/messages': () => import('@/pages/DirectMessagesPage'),
|
|
'/settings': () => import('@/pages/SettingsPage'),
|
|
'/wallet': () => import('@/pages/WalletPage'),
|
|
'/stash': () => import('@/pages/StashPage'),
|
|
'/marketplace': () => import('@/pages/MarketplaceHubPage'),
|
|
'/stats': () => import('@/pages/CommunityStatsPage'),
|
|
'/collections': () => import('@/pages/SpotCollectionsPage'),
|
|
'/glossary': () => import('@/pages/GlossaryPage'),
|
|
'/about': () => import('@/pages/AboutPage'),
|
|
'/card': () => import('@/pages/RiderCardPage'),
|
|
'/supporters': () => import('@/pages/SupportersPage'),
|
|
'/newsletter': () => import('@/pages/NewsletterPage'),
|
|
};
|
|
|
|
const preloaded = new Set<string>();
|
|
|
|
function routePath(anchor: HTMLAnchorElement): string | null {
|
|
const href = anchor.getAttribute('href') ?? '';
|
|
if (!href || href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:')) {
|
|
return null;
|
|
}
|
|
// HashRouter renders to="#/lab" as href="#/lab".
|
|
if (href.startsWith('#/')) return href.slice(1).split('?')[0] || '/';
|
|
try {
|
|
const url = new URL(anchor.href, window.location.href);
|
|
if (url.hash.startsWith('#/')) return url.hash.slice(1).split('?')[0] || '/';
|
|
return url.pathname;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function preloadFromTarget(target: EventTarget | null): void {
|
|
if (!(target instanceof Element)) return;
|
|
const anchor = target.closest('a[href]');
|
|
if (!(anchor instanceof HTMLAnchorElement)) return;
|
|
const path = routePath(anchor);
|
|
if (!path || preloaded.has(path)) return;
|
|
const loader = loaders[path];
|
|
if (!loader) return;
|
|
preloaded.add(path);
|
|
void loader().catch(() => {
|
|
// A failed prefetch is harmless; lazyWithRetry will retry on navigation.
|
|
preloaded.delete(path);
|
|
});
|
|
}
|
|
|
|
export function RoutePreloader() {
|
|
useEffect(() => {
|
|
if (isNativePlatform()) return;
|
|
|
|
const onIntent = (event: Event) => preloadFromTarget(event.target);
|
|
document.addEventListener('pointerover', onIntent, { capture: true, passive: true });
|
|
document.addEventListener('focusin', onIntent, { capture: true, passive: true });
|
|
document.addEventListener('touchstart', onIntent, { capture: true, passive: true });
|
|
return () => {
|
|
document.removeEventListener('pointerover', onIntent, true);
|
|
document.removeEventListener('focusin', onIntent, true);
|
|
document.removeEventListener('touchstart', onIntent, true);
|
|
};
|
|
}, []);
|
|
|
|
return null;
|
|
}
|