From bf85c3b01879826dd7a7bc8b7f0d81ba93a69dd2 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 13 Jun 2026 16:26:44 +0200 Subject: [PATCH] feat: add poster retry and dedupe to search --- static/search.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/static/search.ts b/static/search.ts index 567a10f..bc5c804 100644 --- a/static/search.ts +++ b/static/search.ts @@ -1,3 +1,5 @@ +import { dedupeByID, dedupeWithin } from "./dedupe"; + interface CommandPaletteItem { id: string; type: string; @@ -53,6 +55,7 @@ const typeLabels: Record = { }; const groupOrder = ["anime"]; +const maxPosterImageRetries = 2; const isMac = (): boolean => /Mac|iPhone|iPad|iPod/.test(navigator.platform); @@ -134,11 +137,24 @@ const buildPosterImage = (item: CommandPaletteItem): HTMLElement => { return buildFallbackIcon(item); } + const source = item.image || ""; + let retries = 0; const img = document.createElement("img"); img.className = "aspect-2/3 w-full bg-background-button object-cover"; - img.src = item.image || ""; img.alt = ""; img.loading = "lazy"; + img.addEventListener("error", () => { + if (retries < maxPosterImageRetries) { + retries += 1; + const retryURL = new URL(source); + retryURL.searchParams.set("_retry", String(retries)); + img.src = retryURL.toString(); + return; + } + + img.replaceWith(buildFallbackIcon(item)); + }); + img.src = source; return img; }; @@ -238,6 +254,7 @@ const buildCard = (item: CommandPaletteItem, index: number): HTMLAnchorElement = card.className = "group block min-w-0 text-foreground no-underline transition focus-visible:outline-none hover:no-underline"; card.dataset.commandPaletteItem = item.id; + card.dataset.id = item.id; card.dataset.resultIndex = String(index); card.setAttribute("role", "option"); card.setAttribute("aria-selected", String(index === selectedIndex)); @@ -285,6 +302,7 @@ const buildCompactItem = (item: CommandPaletteItem, index: number): HTMLAnchorEl row.className = "group flex min-h-16 items-center gap-3 px-3 py-2 text-foreground no-underline transition hover:bg-surface-hover hover:no-underline focus-visible:outline-none"; row.dataset.commandPaletteItem = item.id; + row.dataset.id = item.id; row.dataset.resultIndex = String(index); row.setAttribute("role", "option"); row.setAttribute("aria-selected", String(index === selectedIndex)); @@ -399,7 +417,7 @@ const renderItems = (items: CommandPaletteItem[]): void => { const shell = document.createElement("div"); shell.className = "mx-auto w-full max-w-5xl px-5 py-9 md:px-8 md:py-12"; - const groups = groupedItems(items); + const groups = groupedItems(dedupeByID(items, (item) => item.id)); const keys = orderedGroupKeys(groups); resultItems = keys.flatMap((key) => groups.get(key) || []); let cursor = 0; @@ -418,6 +436,7 @@ const renderItems = (items: CommandPaletteItem[]): void => { }); searchResults.replaceChildren(shell); + shell.querySelectorAll("[role='listbox']").forEach((list) => dedupeWithin(list)); selectItem(0, false); }; @@ -427,7 +446,7 @@ const appendItems = (items: CommandPaletteItem[]): void => { } const existingIDs = new Set(resultItems.map((item) => item.id)); - const nextItems = items.filter((item) => !existingIDs.has(item.id)); + const nextItems = dedupeByID(items, (item) => item.id).filter((item) => !existingIDs.has(item.id)); if (nextItems.length === 0) { return; }