fix: sync server-rendered watchlist state to client

This commit is contained in:
2026-05-26 20:29:19 +02:00
parent 749a275dc0
commit 50159286b4
5 changed files with 46 additions and 4 deletions

View File

@@ -316,8 +316,8 @@ func (h *AnimeHandler) HandleScheduleSection(c *gin.Context) {
watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, animes)
c.HTML(http.StatusOK, "schedule.gohtml", gin.H{
"_fragment": "schedule_section",
"Animes": animes,
"_fragment": "schedule_section",
"Animes": animes,
"WatchlistMap": watchlistMap,
})
}

View File

@@ -58,3 +58,13 @@ body {
background-color: var(--color-background);
color: var(--text);
}
[data-watchlist-toggle] .watchlist-icon,
[data-watchlist-toggle] .watchlist-icon path {
fill: none;
}
[data-watchlist-toggle][data-watchlist-state='in'] .watchlist-icon,
[data-watchlist-toggle][data-watchlist-state='in'] .watchlist-icon path {
fill: currentColor;
}

View File

@@ -66,6 +66,7 @@ const syncIconsForId = (id: number): void => {
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'
@@ -109,8 +110,18 @@ const closeClosestDropdown = (from: HTMLElement): void => {
});
};
const toggleWatchlist = async (id: number, title: string): Promise<void> => {
const toggleWatchlist = async (
id: number,
title: string,
renderedState: string | undefined
): Promise<void> => {
if (inflight.has(id)) return;
if (renderedState === 'in') {
watchlistIds.add(id);
} else if (renderedState === 'out') {
watchlistIds.delete(id);
}
const isInWatchlist = watchlistIds.has(id);
setBusy(id, true);
@@ -250,6 +261,20 @@ const initWatchlist = (ids: number[]): void => {
});
};
const getRenderedWatchlistIds = (): number[] => {
const ids = new Set<number>();
document
.querySelectorAll<HTMLElement>('[data-watchlist-toggle][data-watchlist-state="in"][data-mal-id]')
.forEach(button => {
const id = toInt(button.dataset.malId);
if (id === null) return;
ids.add(id);
});
return Array.from(ids);
};
const onReady = (fn: () => void): void => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn, { once: true });
@@ -272,7 +297,7 @@ const installDelegatedHandlers = (): void => {
const id = toInt(toggleButton.getAttribute('data-mal-id') ?? undefined);
if (id === null) return;
const title = toggleButton.getAttribute('data-watchlist-title') ?? 'anime';
void toggleWatchlist(id, title);
void toggleWatchlist(id, title, toggleButton.dataset.watchlistState);
return;
}
@@ -322,5 +347,10 @@ onReady(() => {
}
}
const renderedWatchlistIds = getRenderedWatchlistIds();
if (renderedWatchlistIds.length > 0) {
initWatchlist(renderedWatchlistIds);
}
installDelegatedHandlers();
});

View File

@@ -43,6 +43,7 @@
data-watchlist-toggle
data-mal-id="{{$anime.MalID}}"
data-watchlist-title="{{$anime.DisplayTitle}}"
data-watchlist-state="{{if $isWatchlist}}in{{else}}out{{end}}"
class="text-accent hover:text-accent/80 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:opacity-50 {{if $isWatchlist}}in-watchlist{{end}}"
aria-label="{{if $isWatchlist}}Remove from Watchlist{{else}}Add to Watchlist{{end}}"
>

View File

@@ -55,6 +55,7 @@
data-watchlist-toggle
data-mal-id="{{$anime.MalID}}"
data-watchlist-title="{{$anime.DisplayTitle}}"
data-watchlist-state="{{if (index $.WatchlistMap $anime.MalID)}}in{{else}}out{{end}}"
class="shrink-0 text-accent hover:text-accent/80 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:opacity-50 {{if (index $.WatchlistMap $anime.MalID)}}in-watchlist{{end}}"
aria-label="{{if (index $.WatchlistMap $anime.MalID)}}Remove from Watchlist{{else}}Add to Watchlist{{end}}"
>