style: format static/watchlist.ts

This commit is contained in:
2026-06-21 02:05:10 +02:00
committed by Milas Holsting
parent ebb5e59134
commit c0e2e7f8fb

View File

@@ -1,5 +1,3 @@
export {};
import { onReady } from "./utils"; import { onReady } from "./utils";
type WatchlistStatus = "watching" | "completed" | "plan_to_watch" | "dropped"; type WatchlistStatus = "watching" | "completed" | "plan_to_watch" | "dropped";
@@ -32,7 +30,9 @@ class WatchlistStore {
} }
withOptimistic(id: number, onRollback: () => void): (() => void) | null { withOptimistic(id: number, onRollback: () => void): (() => void) | null {
if (this.isBusy(id)) return null; if (this.isBusy(id)) {
return null;
}
const wasInWatchlist = this.has(id); const wasInWatchlist = this.has(id);
this.inflight.add(id); this.inflight.add(id);
@@ -72,7 +72,9 @@ const toast = (message: string): void => {
}; };
const toInt = (value: string | undefined): number | null => { const toInt = (value: string | undefined): number | null => {
if (!value) return null; if (!value) {
return null;
}
const parsed = Number.parseInt(value, 10); const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : null; return Number.isFinite(parsed) ? parsed : null;
}; };
@@ -80,7 +82,9 @@ const toInt = (value: string | undefined): number | null => {
const withTimeout = async <T>(promise: Promise<T>, ms: number): Promise<T> => { const withTimeout = async <T>(promise: Promise<T>, ms: number): Promise<T> => {
let timeoutId: number | undefined; let timeoutId: number | undefined;
const timeout = new Promise<never>((_, reject) => { const timeout = new Promise<never>((_, reject) => {
timeoutId = window.setTimeout(() => reject(new Error("timeout")), ms); timeoutId = window.setTimeout(() => {
reject(new Error("timeout"));
}, ms);
}); });
try { try {
@@ -97,13 +101,17 @@ const requestJson = async (input: string, init: RequestInit): Promise<Response>
const syncRemoveButtonVisibility = (id: number): void => { const syncRemoveButtonVisibility = (id: number): void => {
const container = document.getElementById(`remove-watchlist-container-${id}`); const container = document.getElementById(`remove-watchlist-container-${id}`);
if (!container) return; if (!container) {
return;
}
container.classList.toggle("hidden", !watchlistStore.has(id)); container.classList.toggle("hidden", !watchlistStore.has(id));
}; };
const syncWatchlistDropdown = (id: number, inWatchlist: boolean): void => { const syncWatchlistDropdown = (id: number, inWatchlist: boolean): void => {
const statusDisplay = document.getElementById(`watchlist-status-display-${id}`); const statusDisplay = document.getElementById(`watchlist-status-display-${id}`);
if (!statusDisplay) return; if (!statusDisplay) {
return;
}
statusDisplay.textContent = inWatchlist ? "Plan to Watch" : "Add to Watchlist"; statusDisplay.textContent = inWatchlist ? "Plan to Watch" : "Add to Watchlist";
syncRemoveButtonVisibility(id); syncRemoveButtonVisibility(id);
}; };
@@ -114,7 +122,9 @@ const syncIconsForId = (id: number): void => {
.querySelectorAll<HTMLElement>("[data-watchlist-toggle][data-mal-id]") .querySelectorAll<HTMLElement>("[data-watchlist-toggle][data-mal-id]")
.forEach((button) => { .forEach((button) => {
const malId = toInt(button.dataset.malId); const malId = toInt(button.dataset.malId);
if (malId !== id) return; if (malId !== id) {
return;
}
button.classList.toggle("in-watchlist", shouldBeInWatchlist); button.classList.toggle("in-watchlist", shouldBeInWatchlist);
button.dataset.watchlistState = shouldBeInWatchlist ? "in" : "out"; button.dataset.watchlistState = shouldBeInWatchlist ? "in" : "out";
button.setAttribute( button.setAttribute(
@@ -136,7 +146,9 @@ const setBusy = (id: number, busy: boolean): void => {
) )
.forEach((button) => { .forEach((button) => {
const malId = toInt(button.dataset.malId); const malId = toInt(button.dataset.malId);
if (malId !== id) return; if (malId !== id) {
return;
}
button.disabled = busy; button.disabled = busy;
button.toggleAttribute("aria-busy", busy); button.toggleAttribute("aria-busy", busy);
}); });
@@ -168,7 +180,9 @@ const toggleWatchlist = async (
syncIconsForId(id); syncIconsForId(id);
syncWatchlistDropdown(id, watchlistStore.has(id)); syncWatchlistDropdown(id, watchlistStore.has(id));
}); });
if (!rollback) return; if (!rollback) {
return;
}
const isInWatchlist = watchlistStore.has(id); const isInWatchlist = watchlistStore.has(id);
@@ -217,18 +231,19 @@ type WatchlistSort = "date" | "title";
const csvEscape = (value: unknown): string => { const csvEscape = (value: unknown): string => {
const str = String(value ?? ""); const str = String(value ?? "");
if (/[",\r\n]/.test(str)) { if (/[",\r\n]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`; return `"${str.replaceAll(/"/g, '""')}"`;
} }
return str; return str;
}; };
const watchlistItems = (): HTMLElement[] => const watchlistItems = (): HTMLElement[] => [
Array.from(document.querySelectorAll<HTMLElement>(".watchlist-item")); ...document.querySelectorAll<HTMLElement>(".watchlist-item"),
];
const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void => { const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void => {
const grids: HTMLElement[] = []; const grids: HTMLElement[] = [];
const singleGrid = document.getElementById("watchlist-items"); const singleGrid = document.querySelector<HTMLElement>("#watchlist-items");
if (singleGrid) { if (singleGrid) {
grids.push(singleGrid); grids.push(singleGrid);
} }
@@ -238,7 +253,7 @@ const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void =
.forEach((grid) => grids.push(grid)); .forEach((grid) => grids.push(grid));
const sortItemsInGrid = (grid: HTMLElement): void => { const sortItemsInGrid = (grid: HTMLElement): void => {
const items = Array.from(grid.querySelectorAll<HTMLElement>(".watchlist-item")); const items = [...grid.querySelectorAll<HTMLElement>(".watchlist-item")];
items.sort((a, b) => { items.sort((a, b) => {
let comparison = 0; let comparison = 0;
if (sortBy === "title") { if (sortBy === "title") {
@@ -252,7 +267,9 @@ const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void =
} }
return desc ? -comparison : comparison; return desc ? -comparison : comparison;
}); });
items.forEach((item) => grid.appendChild(item)); items.forEach((item) => {
grid.append(item);
});
}; };
grids.forEach(sortItemsInGrid); grids.forEach(sortItemsInGrid);
@@ -260,7 +277,9 @@ const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void =
const setActiveFilterButton = (clicked: HTMLButtonElement): void => { const setActiveFilterButton = (clicked: HTMLButtonElement): void => {
const parent = clicked.parentElement; const parent = clicked.parentElement;
if (!parent) return; if (!parent) {
return;
}
parent.querySelectorAll("button").forEach((b) => { parent.querySelectorAll("button").forEach((b) => {
b.classList.remove("text-foreground"); b.classList.remove("text-foreground");
b.classList.add("text-foreground-muted"); b.classList.add("text-foreground-muted");
@@ -274,8 +293,8 @@ const setActiveFilterButton = (clicked: HTMLButtonElement): void => {
}; };
const applyWatchlistFilter = (status: string): void => { const applyWatchlistFilter = (status: string): void => {
const sections = Array.from(document.querySelectorAll<HTMLElement>(".watchlist-section")); const sections = [...document.querySelectorAll<HTMLElement>(".watchlist-section")];
if (sections.length) { if (sections.length > 0) {
sections.forEach((section) => { sections.forEach((section) => {
if (status === "all") { if (status === "all") {
section.style.display = "block"; section.style.display = "block";
@@ -296,9 +315,8 @@ const applyWatchlistFilter = (status: string): void => {
}; };
const exportWatchlistCsv = (): void => { const exportWatchlistCsv = (): void => {
const rows = watchlistItems() const rows = [...watchlistItems()]
.slice() .toSorted((a, b) => {
.sort((a, b) => {
const dateA = Number.parseInt(a.dataset.updatedAt ?? "0", 10) || 0; const dateA = Number.parseInt(a.dataset.updatedAt ?? "0", 10) || 0;
const dateB = Number.parseInt(b.dataset.updatedAt ?? "0", 10) || 0; const dateB = Number.parseInt(b.dataset.updatedAt ?? "0", 10) || 0;
return dateB - dateA; return dateB - dateA;
@@ -319,7 +337,7 @@ const exportWatchlistCsv = (): void => {
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
link.download = "watchlist.csv"; link.download = "watchlist.csv";
document.body.appendChild(link); document.body.append(link);
link.click(); link.click();
link.remove(); link.remove();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
@@ -332,8 +350,10 @@ const initWatchlistPage = (): void => {
sortVisibleWatchlistItems(currentSortBy, sortOrderDesc); sortVisibleWatchlistItems(currentSortBy, sortOrderDesc);
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const target = e.target; const { target } = e;
if (!(target instanceof Element)) return; if (!(target instanceof Element)) {
return;
}
const filterBtn = target.closest<HTMLButtonElement>("button[data-watchlist-filter]"); const filterBtn = target.closest<HTMLButtonElement>("button[data-watchlist-filter]");
if (filterBtn) { if (filterBtn) {
@@ -348,7 +368,7 @@ const initWatchlistPage = (): void => {
if (sortBtn) { if (sortBtn) {
const sortBy = sortBtn.dataset.watchlistSort === "title" ? "title" : "date"; const sortBy = sortBtn.dataset.watchlistSort === "title" ? "title" : "date";
currentSortBy = sortBy; currentSortBy = sortBy;
const display = document.getElementById("sort-by-display"); const display = document.querySelector("#sort-by-display");
if (display) { if (display) {
display.textContent = currentSortBy === "date" ? "Date Added" : "Title"; display.textContent = currentSortBy === "date" ? "Date Added" : "Title";
} }
@@ -396,7 +416,9 @@ const updateWatchlist = async (
syncIconsForId(id); syncIconsForId(id);
syncRemoveButtonVisibility(id); syncRemoveButtonVisibility(id);
}); });
if (!rollback) return; if (!rollback) {
return;
}
setBusy(id, true); setBusy(id, true);
@@ -439,7 +461,9 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement):
syncIconsForId(id); syncIconsForId(id);
syncWatchlistDropdown(id, watchlistStore.has(id)); syncWatchlistDropdown(id, watchlistStore.has(id));
}); });
if (!rollback) return; if (!rollback) {
return;
}
setBusy(id, true); setBusy(id, true);
@@ -462,7 +486,9 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement):
card.remove(); card.remove();
const remaining = document.querySelectorAll(".watchlist-item").length; const remaining = document.querySelectorAll(".watchlist-item").length;
if (remaining === 0) { if (remaining === 0) {
window.setTimeout(() => window.location.reload(), 50); window.setTimeout(() => {
window.location.reload();
}, 50);
} }
} }
} catch (error) { } catch (error) {
@@ -480,7 +506,9 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement):
onReady(initWatchlistPage); onReady(initWatchlistPage);
const initWatchlist = (ids: number[]): void => { const initWatchlist = (ids: number[]): void => {
ids.forEach((id) => watchlistStore.add(id)); ids.forEach((id) => {
watchlistStore.add(id);
});
ids.forEach((id) => { ids.forEach((id) => {
syncRemoveButtonVisibility(id); syncRemoveButtonVisibility(id);
syncIconsForId(id); syncIconsForId(id);
@@ -496,26 +524,32 @@ const getRenderedWatchlistIds = (): number[] => {
) )
.forEach((button) => { .forEach((button) => {
const id = toInt(button.dataset.malId); const id = toInt(button.dataset.malId);
if (id === null) return; if (id === null) {
return;
}
ids.add(id); ids.add(id);
}); });
return Array.from(ids); return [...ids];
}; };
const installDelegatedHandlers = (): void => { const installDelegatedHandlers = (): void => {
document.addEventListener("click", (event) => { document.addEventListener("click", (event) => {
const target = event.target; const { target } = event;
if (!(target instanceof Element)) return; if (!(target instanceof Element)) {
return;
}
const toggleButton = target.closest("[data-watchlist-toggle]") as HTMLElement | null; const toggleButton = target.closest("[data-watchlist-toggle]") as HTMLElement | null;
if (toggleButton) { if (toggleButton) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const id = toInt(toggleButton.getAttribute("data-mal-id") ?? undefined); const id = toInt(toggleButton.dataset.malId ?? undefined);
if (id === null) return; if (id === null) {
const title = toggleButton.getAttribute("data-watchlist-title") ?? "anime"; return;
}
const title = toggleButton.dataset.watchlistTitle ?? "anime";
toggleWatchlist(id, title, toggleButton.dataset.watchlistState).catch((error) => { toggleWatchlist(id, title, toggleButton.dataset.watchlistState).catch((error) => {
console.error("unhandled watchlist toggle failure:", error); console.error("unhandled watchlist toggle failure:", error);
}); });
@@ -526,14 +560,16 @@ const installDelegatedHandlers = (): void => {
if (updateButton) { if (updateButton) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const id = toInt(updateButton.getAttribute("data-mal-id") ?? undefined); const id = toInt(updateButton.dataset.malId ?? undefined);
if (id === null) return; if (id === null) {
const status = updateButton.getAttribute("data-watchlist-status") as WatchlistStatus | null; return;
const display = updateButton.getAttribute( }
"data-watchlist-display", const status = updateButton.dataset.watchlistStatus as WatchlistStatus | null;
) as WatchlistUpdateDisplay | null; const display = updateButton.dataset.watchlistDisplay as WatchlistUpdateDisplay | null;
const title = updateButton.getAttribute("data-watchlist-title") ?? "anime"; const title = updateButton.dataset.watchlistTitle ?? "anime";
if (!status || !display) return; if (!status || !display) {
return;
}
updateWatchlist(id, status, display, title, updateButton).catch((error) => { updateWatchlist(id, status, display, title, updateButton).catch((error) => {
console.error("unhandled watchlist update failure:", error); console.error("unhandled watchlist update failure:", error);
}); });
@@ -544,9 +580,11 @@ const installDelegatedHandlers = (): void => {
if (removeButton) { if (removeButton) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const id = toInt(removeButton.getAttribute("data-mal-id") ?? undefined); const id = toInt(removeButton.dataset.malId ?? undefined);
if (id === null) return; if (id === null) {
const title = removeButton.getAttribute("data-watchlist-title") ?? "anime"; return;
}
const title = removeButton.dataset.watchlistTitle ?? "anime";
removeWatchlist(id, title, removeButton).catch((error) => { removeWatchlist(id, title, removeButton).catch((error) => {
console.error("unhandled watchlist remove failure:", error); console.error("unhandled watchlist remove failure:", error);
}); });
@@ -563,7 +601,7 @@ declare global {
window.initWatchlist = initWatchlist; window.initWatchlist = initWatchlist;
onReady(() => { onReady(() => {
const raw = document.getElementById("watchlist-ids-json")?.textContent; const raw = document.querySelector("#watchlist-ids-json")?.textContent;
if (raw) { if (raw) {
let parsed: unknown = null; let parsed: unknown = null;
try { try {