feat: sync watchlist state across quick add and dropdown

This commit is contained in:
2026-05-02 15:39:19 +02:00
committed by Mikkel Elvers
parent cc9ca1ba9e
commit b03336a710
6 changed files with 81 additions and 9 deletions

View File

@@ -182,6 +182,7 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
user, _ := r.Context().Value(ctxpkg.UserKey).(*database.User)
var status string
var watchlistIDs []int64
if user != nil {
entry, err := h.db.GetWatchListEntry(r.Context(), database.GetWatchListEntryParams{
UserID: user.ID,
@@ -190,13 +191,19 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
if err == nil {
status = entry.Status
}
watchlist, _ := h.db.GetUserWatchList(r.Context(), user.ID)
watchlistIDs = make([]int64, len(watchlist))
for i, e := range watchlist {
watchlistIDs[i] = e.AnimeID
}
}
if err := templates.GetRenderer().ExecuteTemplate(w, "anime.gohtml", map[string]any{
"Anime": anime,
"User": user,
"Status": status,
"CurrentPath": r.URL.Path,
"Anime": anime,
"User": user,
"Status": status,
"CurrentPath": r.URL.Path,
"WatchlistIDs": watchlistIDs,
}); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -218,9 +225,19 @@ func (h *Handler) HandleHTMLWatchOrder(w http.ResponseWriter, r *http.Request) {
return
}
user, _ := r.Context().Value(ctxpkg.UserKey).(*database.User)
watchlistMap := make(map[int64]bool)
if user != nil {
watchlist, _ := h.db.GetUserWatchList(r.Context(), user.ID)
for _, entry := range watchlist {
watchlistMap[entry.AnimeID] = true
}
}
if err := templates.GetRenderer().ExecuteFragment(w, "anime.gohtml", "watch_order", map[string]any{
"Relations": relations,
"AnimeID": id,
"Relations": relations,
"AnimeID": id,
"WatchlistMap": watchlistMap,
}); err != nil {
log.Printf("render error: %v", err)
}

View File

@@ -1,5 +1,6 @@
{{define "title"}}{{.Anime.DisplayTitle}}{{end}}
{{define "content"}}
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
{{$anime := .Anime}}
<div class="flex flex-col gap-10">

View File

@@ -48,15 +48,47 @@
watchlistIds.delete(id)
btn.classList.remove('in-watchlist')
btn.setAttribute('aria-label', 'Add to Watchlist')
// Update dropdown status if on anime page
syncWatchlistDropdown(id, false)
} else {
watchlistIds.add(id)
btn.classList.add('in-watchlist')
btn.setAttribute('aria-label', 'Remove from Watchlist')
// Update dropdown status if on anime page
syncWatchlistDropdown(id, true)
}
// Update all other watchlist icons on the page for this anime
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
const button = icon.closest('button')
if (button && button !== btn) {
const malId = button.dataset.malId
if (malId && parseInt(malId) === id) {
if (watchlistIds.has(id)) {
button.classList.add('in-watchlist')
} else {
button.classList.remove('in-watchlist')
}
}
}
})
}
})
}
function syncWatchlistDropdown(id, inWatchlist) {
const statusDisplay = document.getElementById('watchlist-status-display-' + id)
if (statusDisplay) {
statusDisplay.textContent = inWatchlist ? 'Plan to Watch' : 'Add to Watchlist'
const removeContainer = document.getElementById('remove-watchlist-container-' + id)
if (removeContainer) {
removeContainer.classList.toggle('hidden', !inWatchlist)
}
}
}
function removeFromWatchlist(id, btn) {
fetch(`/api/watchlist/${id}`, { method: 'DELETE' }).then(res => {
if (res.ok) {

View File

@@ -37,7 +37,7 @@
{{end}}
<div class="mt-auto flex items-center justify-start pb-2 pl-2">
<button type="button" onclick="event.preventDefault(); event.stopPropagation(); toggleWatchlist({{$anime.MalID}}, this)" class="text-accent hover:text-accent/80 transition-colors focus:outline-none {{if $isWatchlist}}in-watchlist{{end}}" aria-label="{{if $isWatchlist}}Remove from Watchlist{{else}}Add to Watchlist{{end}}">
<button type="button" data-mal-id="{{$anime.MalID}}" onclick="event.preventDefault(); event.stopPropagation(); toggleWatchlist({{$anime.MalID}}, this)" class="text-accent hover:text-accent/80 transition-colors focus:outline-none {{if $isWatchlist}}in-watchlist{{end}}" aria-label="{{if $isWatchlist}}Remove from Watchlist{{else}}Add to Watchlist{{end}}">
<svg class="size-6 shadow-black drop-shadow-md watchlist-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /></svg>
</button>
</div>

View File

@@ -4,7 +4,7 @@
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range .Relations}}
<div class="group relative">
{{template "anime_card" dict "Anime" .Anime "WithActions" true "Compact" true "HasTopBadge" true}}
{{template "anime_card" dict "Anime" .Anime "WithActions" true "Compact" true "HasTopBadge" true "IsWatchlist" (index $.WatchlistMap .Anime.MalID)}}
{{if eq .Anime.MalID $.AnimeID}}
<div class="bg-accent absolute -top-2 -right-2 z-20 px-2 py-0.5 text-[10px] font-bold text-white shadow-md">
CURRENT

View File

@@ -11,7 +11,7 @@
{{if $status}}
{{if eq $status "watching"}}Watching{{end}}
{{if eq $status "completed"}}Completed{{end}}
{{if eq $status "plan to watch"}}Plan to Watch{{end}}
{{if eq $status "plan_to_watch"}}Plan to Watch{{end}}
{{if eq $status "dropped"}}Dropped{{end}}
{{else}}
Add to Watchlist
@@ -63,6 +63,17 @@
document.getElementById('watchlist-status-display-' + id).textContent = display;
document.getElementById('remove-watchlist-container-' + id).classList.remove('hidden');
// Update all watchlist icons on the page
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
const button = icon.closest('button')
if (button) {
const malId = button.dataset.malId
if (malId && parseInt(malId) === id) {
button.classList.add('in-watchlist')
}
}
});
// Close dropdown after a small delay to let click event finish
requestAnimationFrame(() => {
const dropdown = btn.closest('ui-dropdown');
@@ -79,6 +90,17 @@
document.getElementById('watchlist-status-display-' + id).textContent = 'Add to Watchlist';
document.getElementById('remove-watchlist-container-' + id).classList.add('hidden');
// Update all watchlist icons on the page
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
const button = icon.closest('button')
if (button) {
const malId = button.dataset.malId
if (malId && parseInt(malId) === id) {
button.classList.remove('in-watchlist')
}
}
});
// Close dropdown
const btn = document.getElementById('watchlist-status-display-' + id);
if (btn) {