feat(web): add client-side routes for views
Deep-link, refresh and back/forward now land on the view the operator
was on instead of the board. Vanilla History API (pushState/popstate),
no router dependency: /library, /queues, /movies/{id}/releases and
/search?q= all restore on load.
Closes #102
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// Vanilla History API routing, no router dependency (see the routes issue). The URL
|
||||
// is the source of truth: every route is derived from `location`, never from
|
||||
// state passed to pushState, so a manually edited or copy-pasted URL works.
|
||||
|
||||
export type Route =
|
||||
| { kind: "board" }
|
||||
| { kind: "library" }
|
||||
| { kind: "queues" }
|
||||
| { kind: "search"; query: string }
|
||||
| { kind: "releases"; movieId: number };
|
||||
|
||||
export function parseRoute(url: URL): Route {
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
if (segments.length === 1 && segments[0] === "library") {
|
||||
return { kind: "library" };
|
||||
}
|
||||
if (segments.length === 1 && segments[0] === "queues") {
|
||||
return { kind: "queues" };
|
||||
}
|
||||
if (segments.length === 1 && segments[0] === "search") {
|
||||
const query = url.searchParams.get("q") ?? "";
|
||||
return query === "" ? { kind: "board" } : { kind: "search", query };
|
||||
}
|
||||
if (segments.length === 3 && segments[0] === "movies" && segments[2] === "releases") {
|
||||
const movieId = Number(segments[1]);
|
||||
if (Number.isInteger(movieId) && movieId > 0) {
|
||||
return { kind: "releases", movieId };
|
||||
}
|
||||
}
|
||||
return { kind: "board" };
|
||||
}
|
||||
|
||||
export function routePath(route: Route): string {
|
||||
switch (route.kind) {
|
||||
case "board":
|
||||
return "/";
|
||||
case "library":
|
||||
return "/library";
|
||||
case "queues":
|
||||
return "/queues";
|
||||
case "search":
|
||||
return `/search?q=${encodeURIComponent(route.query)}`;
|
||||
case "releases":
|
||||
return `/movies/${route.movieId}/releases`;
|
||||
}
|
||||
}
|
||||
|
||||
export function currentRoute(): Route {
|
||||
return parseRoute(new URL(location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the URL to `route`. `replace: true` rewrites the current entry
|
||||
* instead of pushing one — used for search-as-you-type, so every keystroke
|
||||
* doesn't add a back-stack entry.
|
||||
*/
|
||||
export function navigate(route: Route, options: { replace?: boolean } = {}) {
|
||||
const path = routePath(route);
|
||||
if (location.pathname + location.search === path) {
|
||||
if (options.replace) {
|
||||
history.replaceState(null, "", path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (options.replace) {
|
||||
history.replaceState(null, "", path);
|
||||
} else {
|
||||
history.pushState(null, "", path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user