// Where you were last, as data. NO server imports -- LastPage.tsx is a client // component and imports these as VALUES, so the same rule lib/note-types.ts // spells out applies here: one `node:` import anywhere in this file's graph and // the bundler follows it into the browser chunk and 500s every page. // // The interesting part of "come back to where I was" is not the storage, it is // deciding WHERE to land when the remembered URL has gone stale -- a song gets // renamed, a cut gets rebuilt away, and /browse/yoshi/wide stops existing. This // app has no app/not-found.tsx, so a stale restore would open the tool on // Next's bare 404, which has no nav on it and no way out but the back button. // So the walk-up lives here, pure and testable without a browser. /** Same `umtool:` prefix as SHORTLIST_EVENT -- one namespace for the app. */ export const LAST_PAGE_KEY = "umtool:last-page"; /** * The floor of the walk-up. It was /sort when / was a doorway into the song * piles; / is the dashboard now, and the page a stale URL degrades to is the * list of every project rather than one kind's judging pile. */ export const DEFAULT_PAGE = "/browse"; /** Long enough for any real query string, short enough to bound a bad write. */ const URL_LIMIT = 2000; /** * Is this a URL we would both record and restore to? * * Deliberately the SAME guard on both sides. Storage is user-editable and * survives a reinstall of the app, so a value read back is untrusted input: a * stored `//evil.example` handed to router.replace() is an off-site redirect * from a URL the user typed themselves. Relative, same-origin, real page only. */ export function isRecordable(url: string): boolean { if (!url || url.length > URL_LIMIT) return false; if (!url.startsWith("/")) return false; // `//host` is protocol-relative -- it looks local and is not. if (url.startsWith("//")) return false; // A backslash is a slash to some URL parsers and not to others; refuse the // whole class rather than guess which one router.replace() feeds. if (url.includes("\\")) return false; // `/` itself is this very page. Restoring to it would be a loop. if (url === "/") return false; // The API is not a place you can be. if (url === "/api" || url.startsWith("/api/")) return false; return true; } /** * Where to try landing, best first. * * The full URL, then the same path without its query, then each parent, then * DEFAULT_PAGE. `/browse/yoshi/wide` gives * `["/browse/yoshi/wide", "/browse/yoshi", "/browse"]` -- so a cut * that no longer exists degrades to its song, a song that no longer exists * degrades to the list of songs, and nothing ever lands on a 404. */ export function restoreCandidates(url: string): string[] { const out: string[] = []; const add = (u: string) => { if (u && !out.includes(u)) out.push(u); }; if (isRecordable(url)) { add(url); const q = url.search(/[?#]/); let path = q === -1 ? url : url.slice(0, q); add(path); // Stop BEFORE "/": it is this page, and isRecordable rejects it anyway. while (path.length > 1) { path = path.slice(0, path.lastIndexOf("/")); if (path.length > 1) add(path); } } add(DEFAULT_PAGE); return out; }