website/src/router.ts
2026-03-18 13:17:33 -07:00

68 lines
1.6 KiB
TypeScript

type PageHandler = (
outlet: HTMLElement,
params: Record<string, string>,
) => void | Promise<void>;
interface Route {
pattern: RegExp;
keys: string[];
handler: PageHandler;
}
const routes: Route[] = [];
let notFoundHandler: PageHandler | null = null;
export function route(path: string, handler: PageHandler) {
if (path === "*") {
notFoundHandler = handler;
return;
}
const keys: string[] = [];
const pattern = path.replace(/:(\w+)/g, (_, key: string) => {
keys.push(key);
return "([^/]+)";
});
routes.push({ pattern: new RegExp(`^${pattern}$`), keys, handler });
}
export function navigate(path: string) {
history.pushState(null, "", path);
void render();
}
async function render() {
const path = location.pathname;
const outlet = document.getElementById("outlet")!;
for (const r of routes) {
const match = path.match(r.pattern);
if (match) {
const params: Record<string, string> = {};
r.keys.forEach((key, i) => {
params[key] = match[i + 1]!;
});
outlet.innerHTML = "";
await r.handler(outlet, params);
return;
}
}
outlet.innerHTML = "";
if (notFoundHandler) {
await notFoundHandler(outlet, {});
}
}
export function initRouter() {
window.addEventListener("popstate", () => void render());
document.addEventListener("click", (e) => {
const anchor = (e.target as HTMLElement).closest("a");
if (anchor?.origin === location.origin && !anchor.hasAttribute("download")) {
e.preventDefault();
navigate(anchor.pathname);
}
});
void render();
}