feat: add watchlist toggle to search results

This commit is contained in:
2026-06-18 20:59:29 +02:00
committed by Milas Holsting
parent bda3c58a98
commit e1ab6e714e
4 changed files with 83 additions and 15 deletions

View File

@@ -17,6 +17,21 @@ import {
isSafeImageUrl,
} from "./state";
const watchlistOverrides = new Map<number, boolean>();
window.addEventListener("watchlist:change", (event) => {
if (!(event instanceof CustomEvent)) {
return;
}
const detail = event.detail as { id?: unknown; inWatchlist?: unknown } | null;
if (!detail || typeof detail.id !== "number" || typeof detail.inWatchlist !== "boolean") {
return;
}
watchlistOverrides.set(detail.id, detail.inWatchlist);
});
export const setClearButtonState = (hasQuery: boolean): void => {
searchClearButtons.forEach((button) => {
button.classList.toggle("opacity-0", !hasQuery);
@@ -158,6 +173,40 @@ const cleanLabel = (item: CommandPaletteItem): string => {
return item.label;
};
const animeMalID = (item: CommandPaletteItem): number | null => {
if (item.type !== "anime") {
return null;
}
const match = item.id.match(/^anime:(\d+)$/);
if (!match) {
return null;
}
const id = Number.parseInt(match[1], 10);
return Number.isFinite(id) ? id : null;
};
const buildWatchlistButton = (item: CommandPaletteItem): HTMLButtonElement | null => {
const malID = animeMalID(item);
if (malID === null) {
return null;
}
const button = document.createElement("button");
const inWatchlist = watchlistOverrides.get(malID) ?? item.inWatchlist === true;
button.type = "button";
button.className =
"absolute bottom-5 left-5 z-20 text-accent opacity-0 transition hover:text-accent/80 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:opacity-50";
button.dataset.watchlistToggle = "";
button.dataset.malId = String(malID);
button.dataset.watchlistTitle = cleanLabel(item);
button.dataset.watchlistState = inWatchlist ? "in" : "out";
button.setAttribute("aria-label", inWatchlist ? "Remove from Watchlist" : "Add to Watchlist");
button.appendChild(buildSvgIcon(iconPaths.bookmark, "size-6 watchlist-icon"));
return button;
};
const buildCard = (item: CommandPaletteItem, index: number): HTMLAnchorElement => {
const card = document.createElement("a");
card.href = item.href;
@@ -174,6 +223,11 @@ const buildCard = (item: CommandPaletteItem, index: number): HTMLAnchorElement =
media.className = "relative mb-3 overflow-hidden bg-background-button";
media.appendChild(buildPosterImage(item));
const watchlistButton = buildWatchlistButton(item);
if (watchlistButton) {
media.appendChild(watchlistButton);
}
if (item.type === "continue") {
const removeButton = document.createElement("button");
removeButton.type = "button";

View File

@@ -6,6 +6,7 @@ export interface CommandPaletteItem {
href: string;
image?: string;
icon?: string;
inWatchlist?: boolean;
}
export interface CommandPaletteResponse {

View File

@@ -149,6 +149,10 @@ const closeClosestDropdown = (from: HTMLElement): void => {
});
};
const dispatchWatchlistChange = (id: number, inWatchlist: boolean): void => {
window.dispatchEvent(new CustomEvent("watchlist:change", { detail: { id, inWatchlist } }));
};
const toggleWatchlist = async (
id: number,
title: string,
@@ -178,6 +182,7 @@ const toggleWatchlist = async (
}
syncIconsForId(id);
syncWatchlistDropdown(id, optimisticNext);
dispatchWatchlistChange(id, optimisticNext);
const url = isInWatchlist ? `/api/watchlist/${id}` : "/api/watchlist";
const method: "DELETE" | "POST" = isInWatchlist ? "DELETE" : "POST";
@@ -197,6 +202,7 @@ const toggleWatchlist = async (
}
} catch (error) {
rollback();
dispatchWatchlistChange(id, isInWatchlist);
toast("Failed to update watchlist");
console.error("failed to update watchlist:", error);
throw error;
@@ -398,6 +404,7 @@ const updateWatchlist = async (
watchlistStore.add(id);
syncIconsForId(id);
syncRemoveButtonVisibility(id);
dispatchWatchlistChange(id, true);
try {
const response = await requestJson("/api/watchlist", {
@@ -419,6 +426,7 @@ const updateWatchlist = async (
toast(`Marked ${title} as ${display}`);
} catch (error) {
rollback();
dispatchWatchlistChange(id, watchlistStore.has(id));
toast("Failed to update watchlist");
console.error("failed to update watchlist:", error);
throw error;
@@ -440,6 +448,7 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement):
watchlistStore.remove(id);
syncIconsForId(id);
syncWatchlistDropdown(id, false);
dispatchWatchlistChange(id, false);
try {
const response = await requestJson(`/api/watchlist/${id}`, { method: "DELETE" });
@@ -460,6 +469,7 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement):
}
} catch (error) {
rollback();
dispatchWatchlistChange(id, watchlistStore.has(id));
toast("Failed to update watchlist");
console.error("failed to update watchlist:", error);
throw error;