forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
1import {
2 buildUrl,
3 type Route,
4 type RouteParams,
5 routes,
6 type RoutesWithParams,
7} from "./types/routes.ts";
8
9const APP_BASE = "/app";
10
11type Brand<T, B extends string> = T & { readonly __brand: B };
12type AppPath = Brand<string, "AppPath">;
13
14function asAppPath(path: string): AppPath {
15 const normalized = path.startsWith("/") ? path : "/" + path;
16 return normalized as AppPath;
17}
18
19function getAppPath(): AppPath {
20 const pathname = globalThis.location.pathname;
21 if (pathname.startsWith(APP_BASE)) {
22 const path = pathname.slice(APP_BASE.length) || "/";
23 return asAppPath(path);
24 }
25 return asAppPath("/");
26}
27
28function getSearchParams(): URLSearchParams {
29 return new URLSearchParams(globalThis.location.search);
30}
31
32interface RouterState {
33 readonly path: AppPath;
34 readonly searchParams: URLSearchParams;
35}
36
37const state = $state<{ current: RouterState }>({
38 current: {
39 path: getAppPath(),
40 searchParams: getSearchParams(),
41 },
42});
43
44function updateState(): void {
45 state.current = {
46 path: getAppPath(),
47 searchParams: getSearchParams(),
48 };
49}
50
51globalThis.addEventListener("popstate", updateState);
52
53export function navigate<R extends Route>(
54 route: R,
55 options?: {
56 params?: R extends RoutesWithParams ? RouteParams[R] : never;
57 replace?: boolean;
58 },
59): void {
60 const url = options?.params ? buildUrl(route, options.params) : route;
61 const fullPath = APP_BASE + (url.startsWith("/") ? url : "/" + url);
62
63 if (options?.replace) {
64 globalThis.history.replaceState(null, "", fullPath);
65 } else {
66 globalThis.history.pushState(null, "", fullPath);
67 }
68
69 updateState();
70}
71
72export function getCurrentPath(): AppPath {
73 return state.current.path;
74}
75
76export function getCurrentSearchParams(): URLSearchParams {
77 return state.current.searchParams;
78}
79
80export function getSearchParam(key: string): string | null {
81 return state.current.searchParams.get(key);
82}
83
84export function getFullUrl(path: string): string {
85 return APP_BASE + (path.startsWith("/") ? path : "/" + path);
86}
87
88export function isCurrentRoute(route: Route): boolean {
89 const pathWithoutQuery = state.current.path.split("?")[0];
90 return pathWithoutQuery === route;
91}
92
93export { type Route, type RouteParams, routes };