feat(nav): the live YouTube search and Home's budget are named in the address

A YouTube search is `?yt=<term>` (+ `ytn` for a non-default count), so a reload, a link
or Back/Forward reopens it. It runs by itself only where it costs no quota: the free page
scrape, a search asked for in this document, or results still cached. An API search
reopened any other way shows its term and waits for "Search YouTube again".

Home's time budget is `hb`; the question's draw stays on the history entry.

An e2e spec opens every page's bookmark in a fresh browser context; CLAUDE.md and
docs/development.md name the module URL codec as the place for view state.
This commit is contained in:
2026-10-27 11:51:29 +01:00
parent 3d26ca0efa
commit 7b4e007680
20 changed files with 847 additions and 64 deletions
+15 -2
View File
@@ -79,6 +79,8 @@ import {
} from "../../components/ui/mediaCardStyles";
import RemoteImage from "../../components/ui/RemoteImage";
import { useOpenLinkedPlayer } from "../../features/player/useOpenLinkedPlayer";
import { budgetFrom, budgetParams, DEFAULT_BUDGET } from "../../lib/homeUrlState";
import { useUrlMirror } from "../../lib/useUrlMirror";
// The Home surface — "Mit nézzek most?".
//
@@ -701,6 +703,15 @@ function readSavedQuestion(): SavedQuestion | null {
};
}
/** The question the page opens with: the budget its address names (a link wins, and an address
* naming none means the default), with the rest of what the entry was left with only when that was
* asked under the same budget. */
function openingQuestion(): { budget: DiscoverBudget; saved: SavedQuestion | null } {
const budget = budgetFrom(new URLSearchParams(window.location.search)) ?? DEFAULT_BUDGET;
const saved = readSavedQuestion();
return { budget, saved: saved?.budget === budget ? saved : null };
}
export default function Home() {
const { t, i18n } = useTranslation();
const qc = useQueryClient();
@@ -710,8 +721,10 @@ export default function Home() {
// The question on screen lives on this page's history ENTRY as well: opening a card leaves Home
// (it unmounts), and Back used to come back to "Doesn't matter" and a different hero — whose card
// then left the Continue rail, so the keyboard landed on its neighbour (prod UAT 2026-09-19).
const [saved] = useState(readSavedQuestion);
const [budget, setBudget] = useState<DiscoverBudget>(saved?.budget ?? "any");
// The budget alone is also in the address (lib/homeUrlState): a setting, unlike the draw under it.
const [{ budget: openedBudget, saved }] = useState(openingQuestion);
const [budget, setBudget] = useState<DiscoverBudget>(openedBudget);
useUrlMirror(budgetParams(budget));
// 0 = "the hero the page loaded with". Any other value is a reroll, answered by its own query so
// pressing the button does not refetch the rails under the user's cursor.
const [seed, setSeed] = useState(saved?.seed ?? 0);