SkateSpots/src/components/RoutePreloader.tsx

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;
}