65 lines
2.3 KiB
JavaScript
Raw Normal View History

2025-10-17 14:46:44 +02:00
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
export default function AppRouter({ routes }) {
const [match, setMatch] = useState(() => resolveRoute(location.pathname, routes));
useEffect(() => {
const handlePopState = () => setMatch(resolveRoute(location.pathname, routes));
const handleLinkClick = (event) => {
2025-10-20 15:42:12 +02:00
if (event.defaultPrevented || event.button !== 0) return; // only left-click
2025-10-17 14:46:44 +02:00
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const anchor = event.target.closest?.('a[href]');
if (!anchor) return;
2025-10-20 15:42:12 +02:00
// Respect hints/targets
2025-10-17 14:46:44 +02:00
if (anchor.target && anchor.target !== '_self') return;
if (anchor.hasAttribute('download')) return;
if (anchor.getAttribute('rel')?.includes('external')) return;
if (anchor.dataset.external === 'true' || anchor.dataset.noRouter === 'true') return;
const href = anchor.getAttribute('href');
if (!href) return;
2025-10-20 15:42:12 +02:00
// Allow in-page, mailto, tel, etc.
2025-10-17 14:46:44 +02:00
if (href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
2025-10-20 15:42:12 +02:00
// Cross-origin goes to browser
2025-10-17 14:46:44 +02:00
if (anchor.origin !== location.origin) return;
2025-10-20 15:42:12 +02:00
// Likely a static asset
2025-10-17 14:46:44 +02:00
if (/\.[a-z0-9]+($|\?)/i.test(href)) return;
event.preventDefault();
history.pushState({}, '', href);
setMatch(resolveRoute(location.pathname, routes));
};
window.addEventListener('popstate', handlePopState);
document.addEventListener('click', handleLinkClick);
return () => {
window.removeEventListener('popstate', handlePopState);
document.removeEventListener('click', handleLinkClick);
};
}, [routes]);
const View = match?.view ?? NotFound;
2025-10-20 15:42:12 +02:00
return h(View, { parameters: match?.parameters ?? [] });
2025-10-17 14:46:44 +02:00
}
function resolveRoute(pathname, routes) {
for (const route of routes) {
2025-10-20 15:42:12 +02:00
const rx = route.pattern || route.re;
if (!rx) continue;
const m = pathname.match(rx);
if (m) return { view: route.view, parameters: m.slice(1) };
2025-10-17 14:46:44 +02:00
}
return null;
}
function NotFound() {
return h('main', { class: 'wrap' }, h('h1', null, 'Not found'));
}