From a4cf0375b7ca74d8c875e691958d0a38058b12f4 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Thu, 28 May 2026 11:28:25 +0200 Subject: [PATCH] chore: format watchlist --- static/watchlist.ts | 268 ++++++++++++++++++++++---------------------- 1 file changed, 135 insertions(+), 133 deletions(-) diff --git a/static/watchlist.ts b/static/watchlist.ts index d6fbac2..75809fc 100644 --- a/static/watchlist.ts +++ b/static/watchlist.ts @@ -1,13 +1,13 @@ export {}; -type WatchlistStatus = 'watching' | 'completed' | 'plan_to_watch' | 'dropped'; +type WatchlistStatus = "watching" | "completed" | "plan_to_watch" | "dropped"; type WatchlistUpdateDisplay = - | 'Watching' - | 'Completed' - | 'Plan to Watch' - | 'Dropped' - | 'Add to Watchlist'; + | "Watching" + | "Completed" + | "Plan to Watch" + | "Dropped" + | "Add to Watchlist"; const watchlistIds = new Set(); const inflight = new Set(); @@ -16,7 +16,7 @@ const getShowToast = (): ((opts: { message: string; duration?: number }) => void const anyWindow = window as unknown as { showToast?: (opts: { message: string; duration?: number }) => void; }; - return typeof anyWindow.showToast === 'function' ? anyWindow.showToast : null; + return typeof anyWindow.showToast === "function" ? anyWindow.showToast : null; }; const toast = (message: string): void => { @@ -32,13 +32,13 @@ const toInt = (value: string | undefined): number | null => { const withTimeout = async (promise: Promise, ms: number): Promise => { let timeoutId: number | undefined; const timeout = new Promise((_, reject) => { - timeoutId = window.setTimeout(() => reject(new Error('timeout')), ms); + timeoutId = window.setTimeout(() => reject(new Error("timeout")), ms); }); try { return await Promise.race([promise, timeout]); } finally { - if (typeof timeoutId === 'number') { + if (typeof timeoutId === "number") { window.clearTimeout(timeoutId); } } @@ -50,29 +50,31 @@ const requestJson = async (input: string, init: RequestInit): Promise const syncRemoveButtonVisibility = (id: number): void => { const container = document.getElementById(`remove-watchlist-container-${id}`); if (!container) return; - container.classList.toggle('hidden', !watchlistIds.has(id)); + container.classList.toggle("hidden", !watchlistIds.has(id)); }; const syncWatchlistDropdown = (id: number, inWatchlist: boolean): void => { const statusDisplay = document.getElementById(`watchlist-status-display-${id}`); if (!statusDisplay) return; - statusDisplay.textContent = inWatchlist ? 'Plan to Watch' : 'Add to Watchlist'; + statusDisplay.textContent = inWatchlist ? "Plan to Watch" : "Add to Watchlist"; syncRemoveButtonVisibility(id); }; const syncIconsForId = (id: number): void => { const shouldBeInWatchlist = watchlistIds.has(id); - document.querySelectorAll('[data-watchlist-toggle][data-mal-id]').forEach(button => { - const malId = toInt(button.dataset.malId); - if (malId !== id) return; - button.classList.toggle('in-watchlist', shouldBeInWatchlist); - button.dataset.watchlistState = shouldBeInWatchlist ? 'in' : 'out'; - button.setAttribute( - 'aria-label', - shouldBeInWatchlist ? 'Remove from Watchlist' : 'Add to Watchlist' - ); - button.toggleAttribute('aria-busy', inflight.has(id)); - }); + document + .querySelectorAll("[data-watchlist-toggle][data-mal-id]") + .forEach((button) => { + const malId = toInt(button.dataset.malId); + if (malId !== id) return; + button.classList.toggle("in-watchlist", shouldBeInWatchlist); + button.dataset.watchlistState = shouldBeInWatchlist ? "in" : "out"; + button.setAttribute( + "aria-label", + shouldBeInWatchlist ? "Remove from Watchlist" : "Add to Watchlist", + ); + button.toggleAttribute("aria-busy", inflight.has(id)); + }); }; const setBusy = (id: number, busy: boolean): void => { @@ -83,29 +85,29 @@ const setBusy = (id: number, busy: boolean): void => { } document - .querySelectorAll('[data-watchlist-toggle][data-mal-id]') - .forEach(button => { + .querySelectorAll("[data-watchlist-toggle][data-mal-id]") + .forEach((button) => { const malId = toInt(button.dataset.malId); if (malId !== id) return; button.disabled = busy; - button.toggleAttribute('aria-busy', busy); + button.toggleAttribute("aria-busy", busy); }); document .querySelectorAll( - '[data-watchlist-update][data-mal-id], [data-watchlist-remove][data-mal-id]' + "[data-watchlist-update][data-mal-id], [data-watchlist-remove][data-mal-id]", ) - .forEach(button => { + .forEach((button) => { const malId = toInt(button.dataset.malId); if (malId !== id) return; button.disabled = busy; - button.toggleAttribute('aria-busy', busy); + button.toggleAttribute("aria-busy", busy); }); }; const closeClosestDropdown = (from: HTMLElement): void => { requestAnimationFrame(() => { - const dropdown = from.closest('ui-dropdown') as { close?: () => void } | null; + const dropdown = from.closest("ui-dropdown") as { close?: () => void } | null; dropdown?.close?.(); }); }; @@ -113,12 +115,12 @@ const closeClosestDropdown = (from: HTMLElement): void => { const toggleWatchlist = async ( id: number, title: string, - renderedState: string | undefined + renderedState: string | undefined, ): Promise => { if (inflight.has(id)) return; - if (renderedState === 'in') { + if (renderedState === "in") { watchlistIds.add(id); - } else if (renderedState === 'out') { + } else if (renderedState === "out") { watchlistIds.delete(id); } @@ -135,21 +137,21 @@ const toggleWatchlist = async ( syncIconsForId(id); syncWatchlistDropdown(id, optimisticNext); - const url = isInWatchlist ? `/api/watchlist/${id}` : '/api/watchlist'; - const method: 'DELETE' | 'POST' = isInWatchlist ? 'DELETE' : 'POST'; + const url = isInWatchlist ? `/api/watchlist/${id}` : "/api/watchlist"; + const method: "DELETE" | "POST" = isInWatchlist ? "DELETE" : "POST"; const body = isInWatchlist ? null - : JSON.stringify({ animeId: id, status: 'plan_to_watch' satisfies WatchlistStatus }); + : JSON.stringify({ animeId: id, status: "plan_to_watch" satisfies WatchlistStatus }); try { const response = await requestJson(url, { method, - headers: body ? { 'Content-Type': 'application/json' } : {}, + headers: body ? { "Content-Type": "application/json" } : {}, body: body ?? undefined, }); if (!response.ok) { - throw new Error('not ok'); + throw new Error("not ok"); } toast(optimisticNext ? `Added ${title} to watchlist` : `Removed ${title} from watchlist`); @@ -161,7 +163,7 @@ const toggleWatchlist = async ( } syncIconsForId(id); syncWatchlistDropdown(id, watchlistIds.has(id)); - toast('Failed to update watchlist'); + toast("Failed to update watchlist"); } finally { setBusy(id, false); syncIconsForId(id); @@ -169,10 +171,10 @@ const toggleWatchlist = async ( } }; -type WatchlistSort = 'date' | 'title'; +type WatchlistSort = "date" | "title"; const csvEscape = (value: unknown): string => { - const str = String(value ?? ''); + const str = String(value ?? ""); if (/[",\r\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"`; } @@ -180,36 +182,36 @@ const csvEscape = (value: unknown): string => { }; const watchlistItems = (): HTMLElement[] => - Array.from(document.querySelectorAll('.watchlist-item')); + Array.from(document.querySelectorAll(".watchlist-item")); const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void => { const grids: HTMLElement[] = []; - const singleGrid = document.getElementById('watchlist-items'); + const singleGrid = document.getElementById("watchlist-items"); if (singleGrid) { grids.push(singleGrid); } document - .querySelectorAll('.watchlist-section .grid') - .forEach(grid => grids.push(grid)); + .querySelectorAll(".watchlist-section .grid") + .forEach((grid) => grids.push(grid)); const sortItemsInGrid = (grid: HTMLElement): void => { - const items = Array.from(grid.querySelectorAll('.watchlist-item')); + const items = Array.from(grid.querySelectorAll(".watchlist-item")); items.sort((a, b) => { let comparison = 0; - if (sortBy === 'title') { - const titleA = (a.querySelector('h3')?.textContent ?? '').toLowerCase().trim(); - const titleB = (b.querySelector('h3')?.textContent ?? '').toLowerCase().trim(); + if (sortBy === "title") { + const titleA = (a.querySelector("h3")?.textContent ?? "").toLowerCase().trim(); + const titleB = (b.querySelector("h3")?.textContent ?? "").toLowerCase().trim(); comparison = titleA.localeCompare(titleB); } else { - const dateA = Number.parseInt(a.dataset.updatedAt ?? '0', 10) || 0; - const dateB = Number.parseInt(b.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; comparison = dateA - dateB; } return desc ? -comparison : comparison; }); - items.forEach(item => grid.appendChild(item)); + items.forEach((item) => grid.appendChild(item)); }; grids.forEach(sortItemsInGrid); @@ -218,37 +220,37 @@ const sortVisibleWatchlistItems = (sortBy: WatchlistSort, desc: boolean): void = const setActiveFilterButton = (clicked: HTMLButtonElement): void => { const parent = clicked.parentElement; if (!parent) return; - parent.querySelectorAll('button').forEach(b => { - b.classList.remove('text-foreground'); - b.classList.add('text-foreground-muted'); - b.classList.remove('border-accent'); - b.classList.add('border-transparent'); + parent.querySelectorAll("button").forEach((b) => { + b.classList.remove("text-foreground"); + b.classList.add("text-foreground-muted"); + b.classList.remove("border-accent"); + b.classList.add("border-transparent"); }); - clicked.classList.remove('text-foreground-muted'); - clicked.classList.add('text-foreground'); - clicked.classList.remove('border-transparent'); - clicked.classList.add('border-accent'); + clicked.classList.remove("text-foreground-muted"); + clicked.classList.add("text-foreground"); + clicked.classList.remove("border-transparent"); + clicked.classList.add("border-accent"); }; const applyWatchlistFilter = (status: string): void => { - const sections = Array.from(document.querySelectorAll('.watchlist-section')); + const sections = Array.from(document.querySelectorAll(".watchlist-section")); if (sections.length) { - sections.forEach(section => { - if (status === 'all') { - section.style.display = 'block'; + sections.forEach((section) => { + if (status === "all") { + section.style.display = "block"; return; } - section.style.display = section.dataset.status === status ? 'block' : 'none'; + section.style.display = section.dataset.status === status ? "block" : "none"; }); return; } - watchlistItems().forEach(item => { - if (status === 'all') { - item.style.display = 'flex'; + watchlistItems().forEach((item) => { + if (status === "all") { + item.style.display = "flex"; return; } - item.style.display = item.dataset.status === status ? 'flex' : 'none'; + item.style.display = item.dataset.status === status ? "flex" : "none"; }); }; @@ -256,26 +258,26 @@ const exportWatchlistCsv = (): void => { const rows = watchlistItems() .slice() .sort((a, b) => { - const dateA = Number.parseInt(a.dataset.updatedAt ?? '0', 10) || 0; - const dateB = Number.parseInt(b.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; return dateB - dateA; }) - .map(item => { - const updatedAt = Number.parseInt(item.dataset.updatedAt ?? '0', 10) || 0; - const updatedAtISO = updatedAt > 0 ? new Date(updatedAt * 1000).toISOString() : ''; - const title = item.dataset.title || item.querySelector('h3')?.textContent?.trim() || ''; - return [item.dataset.malId || '', title, item.dataset.status || '', updatedAtISO]; + .map((item) => { + const updatedAt = Number.parseInt(item.dataset.updatedAt ?? "0", 10) || 0; + const updatedAtISO = updatedAt > 0 ? new Date(updatedAt * 1000).toISOString() : ""; + const title = item.dataset.title || item.querySelector("h3")?.textContent?.trim() || ""; + return [item.dataset.malId || "", title, item.dataset.status || "", updatedAtISO]; }); - const csv = [['mal_id', 'title', 'status', 'updated_at'], ...rows] - .map(row => row.map(csvEscape).join(',')) - .join('\r\n'); + const csv = [["mal_id", "title", "status", "updated_at"], ...rows] + .map((row) => row.map(csvEscape).join(",")) + .join("\r\n"); - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = url; - link.download = 'watchlist.csv'; + link.download = "watchlist.csv"; document.body.appendChild(link); link.click(); link.remove(); @@ -283,58 +285,58 @@ const exportWatchlistCsv = (): void => { }; const initWatchlistPage = (): void => { - let currentSortBy: WatchlistSort = 'date'; + let currentSortBy: WatchlistSort = "date"; let sortOrderDesc = true; sortVisibleWatchlistItems(currentSortBy, sortOrderDesc); - document.addEventListener('click', e => { + document.addEventListener("click", (e) => { const target = e.target; if (!(target instanceof Element)) return; - const filterBtn = target.closest('button[data-watchlist-filter]'); + const filterBtn = target.closest("button[data-watchlist-filter]"); if (filterBtn) { - const status = filterBtn.dataset.watchlistFilter ?? 'all'; + const status = filterBtn.dataset.watchlistFilter ?? "all"; setActiveFilterButton(filterBtn); applyWatchlistFilter(status); sortVisibleWatchlistItems(currentSortBy, sortOrderDesc); return; } - const sortBtn = target.closest('button[data-watchlist-sort]'); + const sortBtn = target.closest("button[data-watchlist-sort]"); if (sortBtn) { - const sortBy = sortBtn.dataset.watchlistSort === 'title' ? 'title' : 'date'; + const sortBy = sortBtn.dataset.watchlistSort === "title" ? "title" : "date"; currentSortBy = sortBy; - const display = document.getElementById('sort-by-display'); + const display = document.getElementById("sort-by-display"); if (display) { - display.textContent = currentSortBy === 'date' ? 'Date Added' : 'Title'; + display.textContent = currentSortBy === "date" ? "Date Added" : "Title"; } - const dropdownContent = sortBtn.closest('[data-content]'); - dropdownContent?.querySelectorAll('button').forEach(b => { - b.classList.remove('text-foreground'); - b.classList.add('text-foreground-muted'); + const dropdownContent = sortBtn.closest("[data-content]"); + dropdownContent?.querySelectorAll("button").forEach((b) => { + b.classList.remove("text-foreground"); + b.classList.add("text-foreground-muted"); }); - sortBtn.classList.remove('text-foreground-muted'); - sortBtn.classList.add('text-foreground'); + sortBtn.classList.remove("text-foreground-muted"); + sortBtn.classList.add("text-foreground"); sortVisibleWatchlistItems(currentSortBy, sortOrderDesc); - const parentDropdown = sortBtn.closest('ui-dropdown') as { close?: () => void } | null; + const parentDropdown = sortBtn.closest("ui-dropdown") as { close?: () => void } | null; parentDropdown?.close?.(); return; } - const sortOrderBtn = target.closest('button[data-watchlist-sort-order]'); + const sortOrderBtn = target.closest("button[data-watchlist-sort-order]"); if (sortOrderBtn) { sortOrderDesc = !sortOrderDesc; - const icon = sortOrderBtn.querySelector('svg'); - icon?.classList.toggle('rotate-180', !sortOrderDesc); + const icon = sortOrderBtn.querySelector("svg"); + icon?.classList.toggle("rotate-180", !sortOrderDesc); sortVisibleWatchlistItems(currentSortBy, sortOrderDesc); return; } - const exportBtn = target.closest('button[data-watchlist-export]'); + const exportBtn = target.closest("button[data-watchlist-export]"); if (exportBtn) { exportWatchlistCsv(); return; @@ -347,7 +349,7 @@ const updateWatchlist = async ( status: WatchlistStatus, display: WatchlistUpdateDisplay, title: string, - source: HTMLElement + source: HTMLElement, ): Promise => { if (inflight.has(id)) return; setBusy(id, true); @@ -358,14 +360,14 @@ const updateWatchlist = async ( syncRemoveButtonVisibility(id); try { - const response = await requestJson('/api/watchlist', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await requestJson("/api/watchlist", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ animeId: id, status }), }); if (!response.ok) { - throw new Error('not ok'); + throw new Error("not ok"); } const statusDisplay = document.getElementById(`watchlist-status-display-${id}`); @@ -381,7 +383,7 @@ const updateWatchlist = async ( } syncIconsForId(id); syncRemoveButtonVisibility(id); - toast('Failed to update watchlist'); + toast("Failed to update watchlist"); } finally { setBusy(id, false); } @@ -397,18 +399,18 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement): syncWatchlistDropdown(id, false); try { - const response = await requestJson(`/api/watchlist/${id}`, { method: 'DELETE' }); + const response = await requestJson(`/api/watchlist/${id}`, { method: "DELETE" }); if (!response.ok) { - throw new Error('not ok'); + throw new Error("not ok"); } closeClosestDropdown(source); toast(`Removed ${title} from watchlist`); - const card = source.closest('.watchlist-item'); + const card = source.closest(".watchlist-item"); if (card instanceof HTMLElement) { card.remove(); - const remaining = document.querySelectorAll('.watchlist-item').length; + const remaining = document.querySelectorAll(".watchlist-item").length; if (remaining === 0) { window.setTimeout(() => window.location.reload(), 50); } @@ -419,22 +421,22 @@ const removeWatchlist = async (id: number, title: string, source: HTMLElement): } syncIconsForId(id); syncWatchlistDropdown(id, watchlistIds.has(id)); - toast('Failed to update watchlist'); + toast("Failed to update watchlist"); } finally { setBusy(id, false); syncRemoveButtonVisibility(id); } }; -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initWatchlistPage); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initWatchlistPage); } else { initWatchlistPage(); } const initWatchlist = (ids: number[]): void => { - ids.forEach(id => watchlistIds.add(id)); - ids.forEach(id => { + ids.forEach((id) => watchlistIds.add(id)); + ids.forEach((id) => { syncRemoveButtonVisibility(id); syncIconsForId(id); }); @@ -445,9 +447,9 @@ const getRenderedWatchlistIds = (): number[] => { document .querySelectorAll( - '[data-watchlist-toggle][data-watchlist-state="in"][data-mal-id]' + '[data-watchlist-toggle][data-watchlist-state="in"][data-mal-id]', ) - .forEach(button => { + .forEach((button) => { const id = toInt(button.dataset.malId); if (id === null) return; ids.add(id); @@ -457,8 +459,8 @@ const getRenderedWatchlistIds = (): number[] => { }; const onReady = (fn: () => void): void => { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', fn, { once: true }); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", fn, { once: true }); return; } @@ -466,45 +468,45 @@ const onReady = (fn: () => void): void => { }; const installDelegatedHandlers = (): void => { - document.addEventListener('click', event => { + document.addEventListener("click", (event) => { const target = event.target; 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) { event.preventDefault(); event.stopPropagation(); - const id = toInt(toggleButton.getAttribute('data-mal-id') ?? undefined); + const id = toInt(toggleButton.getAttribute("data-mal-id") ?? undefined); if (id === null) return; - const title = toggleButton.getAttribute('data-watchlist-title') ?? 'anime'; + const title = toggleButton.getAttribute("data-watchlist-title") ?? "anime"; void toggleWatchlist(id, title, toggleButton.dataset.watchlistState); return; } - const updateButton = target.closest('[data-watchlist-update]') as HTMLElement | null; + const updateButton = target.closest("[data-watchlist-update]") as HTMLElement | null; if (updateButton) { event.preventDefault(); event.stopPropagation(); - const id = toInt(updateButton.getAttribute('data-mal-id') ?? undefined); + const id = toInt(updateButton.getAttribute("data-mal-id") ?? undefined); if (id === null) return; - const status = updateButton.getAttribute('data-watchlist-status') as WatchlistStatus | null; + const status = updateButton.getAttribute("data-watchlist-status") as WatchlistStatus | null; const display = updateButton.getAttribute( - 'data-watchlist-display' + "data-watchlist-display", ) as WatchlistUpdateDisplay | null; - const title = updateButton.getAttribute('data-watchlist-title') ?? 'anime'; + const title = updateButton.getAttribute("data-watchlist-title") ?? "anime"; if (!status || !display) return; void updateWatchlist(id, status, display, title, updateButton); return; } - const removeButton = target.closest('[data-watchlist-remove]') as HTMLElement | null; + const removeButton = target.closest("[data-watchlist-remove]") as HTMLElement | null; if (removeButton) { event.preventDefault(); event.stopPropagation(); - const id = toInt(removeButton.getAttribute('data-mal-id') ?? undefined); + const id = toInt(removeButton.getAttribute("data-mal-id") ?? undefined); if (id === null) return; - const title = removeButton.getAttribute('data-watchlist-title') ?? 'anime'; + const title = removeButton.getAttribute("data-watchlist-title") ?? "anime"; void removeWatchlist(id, title, removeButton); } }); @@ -522,7 +524,7 @@ window.initWatchlist = initWatchlist; onReady(() => { const raw = window.__WATCHLIST_IDS__; if (Array.isArray(raw)) { - const ids: number[] = raw.filter((entry): entry is number => typeof entry === 'number'); + const ids: number[] = raw.filter((entry): entry is number => typeof entry === "number"); if (ids.length > 0) { initWatchlist(ids); }