Saved Cars
Indexa’s favourites are a browser feature, not an account feature. A heart on any card writes to localStorage, and /saved/ renders a dashboard from whatever that browser has stored. There is no server, no session and no sync — which is exactly why it works in a static build, and exactly what a real deployment would replace.
The store
Two keys, and the shape matters because two modules read them:
export const SAVED_KEY = "indexa:saved"; // string[] of refs
export const SAVED_AT_KEY = "indexa:savedAt"; // Record<ref, epoch ms>
The ref array is the membership; the timestamp map is what “newest first” sorts by. They are written together by initHearts() in the browse engine and read together by the dashboard, so a record saved on a car page arrives at the dashboard already stamped.
src/js/favorites.ts owns both, and it owns them as pure functions over an injected storage accessor:
export function readSaved(get: StorageLike): string[]
export function toggleSaved(refs, savedAt, ref, now = Date.now()): { on, refs, savedAt }
No DOM at module scope, relative imports only, .ts extensions — which means favorites.test.ts can load it under plain node --experimental-strip-types and assert the logic without a browser. Both readers swallow a corrupt or absent store and return empty, because a JSON.parse throw in this module would take out the heart on every card.
The two stacks
/saved/ server-renders both states — the empty stack and the full one — and ships them in the same HTML. The empty stack is visible; the full one is hidden.
A tiny inline script runs before paint, reads the ref array, and flips them if the store has anything in it. Then the module fills in everything the server could not know: the counts, the rows, the drift ledger, the position tiles and the watch chips.
<script is:inline>
try {
const savedRefs = JSON.parse(localStorage.getItem("indexa:saved") || "[]");
if (Array.isArray(savedRefs) && savedRefs.length) {
document.getElementById("saved-empty")?.setAttribute("hidden", "");
document.getElementById("saved-full")?.removeAttribute("hidden");
}
} catch { /* corrupt store keeps the empty stack */ }
</script>
Two things about that are worth copying. It is inline and pre-paint, so a returning visitor never sees the empty state flash before their records appear. And the empty stack is the server-rendered default, so a browser with no store, or no JavaScript, gets a correct page rather than a skeleton — the truth is what the server shipped, and the script only ever corrects it upwards.
The manifest
The page cannot know which records a browser saved, but it does know every record that could be saved. So it serializes them once, as an inline application/json block:
const manifest = [...carRecords.map(carRow), ...specialistsCategory.rated.items.map(specialistRow)];
carRow and specialistRow flatten a car or a firm into the same SavedRow shape — ref, kind, title, initials, tags, a meta line, an href, plus the numbers the figures need. The engine builds a Map from it and resolves each saved ref. A ref that no longer exists is silently dropped, which is the right behaviour when a record leaves the index.
What the dashboard computes
All of the arithmetic lives in favorites.ts as pure functions, which is why it is all under test:
sortRows handles the three sort modes — newest, oldest, name — with missing timestamps sinking to the bottom. cityCounts groups saved records by town, largest first, for the header’s city strip. median is used by positionTiles, which builds the four “your shortlist, against the index” comparisons — median rating, median year, trade-body share, median price — and returns null for any tile whose kind is not in the saved set. The dashboard hides a null tile rather than printing a zero, so it only ever shows a figure it could actually compute.
formatSavedDate stamps the ledger’s compact “04 SEP” in UTC deliberately: a local-time format would let the server and the client disagree about the day.
The drift ledger is honest about being a demo. savedData.drift.events is a ref-keyed map of scripted changes, and driftHits shows only the events whose record you actually saved — so the section is hidden entirely for a shortlist with no scripted events, rather than showing an empty ledger. A real deployment replaces that map with the index’s own change feed.
The sign-in demo
/sign-in/ is a static form. It posts nowhere; a small script stores the submitted email under indexa:account and sends you to /saved/, which then reads it to personalize one note. That is the whole of the “account” system, and both pages are noindex for that reason — see Routing.
Where a real system plugs in is clear enough to name. The store becomes a server read, readSaved/writeSaved become an API call behind the same signatures, and the manifest becomes a query. The pure helpers, the two stacks and the pre-paint flip all survive that change; only the storage accessor changes shape.
Two implementation notes
The rows are built with static Tailwind classes written as literals in saved.ts, because Tailwind scans source files: a class assembled at runtime from fragments would not be compiled into the stylesheet. filter.ts does the same thing for the same reason.
And the module inlines copies of the four icons it renders rather than importing the registry. Importing ICONS here shipped about 475 KB of SVG to this one page — the exact ceiling the icon README’s ponytail: note names. Icons has the full story.