diff options
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 235 |
1 files changed, 145 insertions, 90 deletions
diff --git a/static/app.js b/static/app.js index 7b5a88f..7cd501a 100644 --- a/static/app.js +++ b/static/app.js @@ -229,109 +229,164 @@ document.querySelectorAll(".star-control").forEach((control) => { }); }); -// Infinite scroll for film lists -const feedSentinel = document.querySelector("#feed-sentinel"); -if (feedSentinel) { +// Infinite scroll + search/filter/sort +(function () { const feedContainer = document.querySelector("#film-feed"); - let loading = false; - - const loadMore = async () => { - if (loading) return; - loading = true; - - const shelf = feedSentinel.dataset.shelf; - const offset = Number(feedSentinel.dataset.offset); - const total = Number(feedSentinel.dataset.total); - - try { - const response = await fetch(`/films/partial?shelf=${encodeURIComponent(shelf)}&offset=${offset}&limit=20`); - if (!response.ok) return; + if (!feedContainer) return; - const hasMore = response.headers.get("X-Has-More") === "true"; - const html = await response.text(); + const shelf = feedContainer.dataset.shelf; + if (!shelf) return; - if (html.trim()) { - feedContainer.insertAdjacentHTML("beforeend", html); + let currentQ = ""; + let currentSort = ""; - // For diary shelf: merge month groups if they span batch boundaries - if (shelf === "diary") { - const monthGroups = feedContainer.querySelectorAll(".month-group"); - for (let i = monthGroups.length - 1; i > 0; i--) { - const current = monthGroups[i]; - const prev = monthGroups[i - 1]; - if (current.dataset.month === prev.dataset.month) { - const prevList = prev.querySelectorAll(".film-card"); - const currentCards = current.querySelectorAll(".film-card"); - currentCards.forEach((card) => prev.appendChild(card)); - current.remove(); - } + function attachStarListeners(root) { + root.querySelectorAll(".star-control").forEach((control) => { + if (control.dataset.listenerAttached) return; + control.dataset.listenerAttached = "true"; + syncStarControl(control, Number(control.dataset.currentStars || 0)); + control.addEventListener("pointerleave", () => clearStarPreview(control)); + control.addEventListener("focusout", (event) => { + if (!control.contains(event.relatedTarget)) clearStarPreview(control); + }); + control.querySelectorAll(".star-button").forEach((button) => { + button.addEventListener("pointerenter", () => previewStarControl(control, Number(button.dataset.stars || 0))); + button.addEventListener("focus", () => previewStarControl(control, Number(button.dataset.stars || 0))); + button.addEventListener("click", async () => { + const currentStars = Number(control.dataset.currentStars || 0); + const selectedStars = Number(button.dataset.stars || 0); + const nextStars = currentStars === selectedStars ? 0 : selectedStars; + button.disabled = true; + try { + const resp = await fetch(`/films/${control.dataset.filmId}/stars`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stars: nextStars }), + }); + const data = await resp.json(); + if (!resp.ok) return; + syncStarControl(control, Number(data.stars || 0)); + } catch (err) { + console.error("Failed to update stars", err); + } finally { + button.disabled = false; } - } + }); + }); + }); + } - // Re-attach star control listeners to newly added films - feedContainer.querySelectorAll(".star-control").forEach((control) => { - if (!control.dataset.listenerAttached) { - control.dataset.listenerAttached = "true"; - syncStarControl(control, Number(control.dataset.currentStars || 0)); - control.addEventListener("pointerleave", () => clearStarPreview(control)); - control.addEventListener("focusout", (event) => { - if (!control.contains(event.relatedTarget)) { - clearStarPreview(control); - } - }); - control.querySelectorAll(".star-button").forEach((button) => { - button.addEventListener("pointerenter", () => previewStarControl(control, Number(button.dataset.stars || 0))); - button.addEventListener("focus", () => previewStarControl(control, Number(button.dataset.stars || 0))); - button.addEventListener("click", async () => { - const currentStars = Number(control.dataset.currentStars || 0); - const selectedStars = Number(button.dataset.stars || 0); - const nextStars = currentStars === selectedStars ? 0 : selectedStars; + function mergeMonthGroups() { + const groups = feedContainer.querySelectorAll(".month-group"); + for (let i = groups.length - 1; i > 0; i--) { + const cur = groups[i]; + const prev = groups[i - 1]; + if (cur.dataset.month === prev.dataset.month) { + cur.querySelectorAll(".film-card").forEach((card) => prev.appendChild(card)); + cur.remove(); + } + } + } - button.disabled = true; - try { - const response = await fetch(`/films/${control.dataset.filmId}/stars`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ stars: nextStars }), - }); + let observer = null; + let loading = false; - const data = await response.json(); - if (!response.ok) return; + function startInfiniteScroll(sentinel) { + if (observer) observer.disconnect(); + loading = false; - syncStarControl(control, Number(data.stars || 0)); - } catch (error) { - console.error("Failed to update stars", error); - } finally { - button.disabled = false; - } - }); - }); - } - }); + const loadMore = async () => { + if (loading) return; + loading = true; + const offset = Number(sentinel.dataset.offset); + const params = new URLSearchParams({ shelf, offset, limit: 20 }); + if (currentQ) params.set("q", currentQ); + if (currentSort) params.set("sort", currentSort); + try { + const response = await fetch(`/films/partial?${params}`); + if (!response.ok) return; + const hasMore = response.headers.get("X-Has-More") === "true"; + const html = await response.text(); + if (html.trim()) { + feedContainer.insertAdjacentHTML("beforeend", html); + if (shelf === "diary" && !currentQ) mergeMonthGroups(); + attachStarListeners(feedContainer); + } + if (!hasMore) { + observer.disconnect(); + sentinel.remove(); + } else { + sentinel.dataset.offset = String(offset + 20); + } + } catch (err) { + console.error("Failed to load more films", err); + } finally { + loading = false; } + }; - if (!hasMore) { - observer.disconnect(); - feedSentinel.remove(); - } else { - feedSentinel.dataset.offset = String(offset + 20); + observer = new IntersectionObserver( + (entries) => entries.forEach((e) => { if (e.isIntersecting) loadMore(); }), + { rootMargin: "100px" } + ); + observer.observe(sentinel); + } + + const initialSentinel = document.querySelector("#feed-sentinel"); + if (initialSentinel) startInfiniteScroll(initialSentinel); + + async function resetFeed() { + if (observer) { observer.disconnect(); observer = null; } + const params = new URLSearchParams({ shelf, offset: 0, limit: 20 }); + if (currentQ) params.set("q", currentQ); + if (currentSort) params.set("sort", currentSort); + let html = ""; + let hasMore = false; + try { + const response = await fetch(`/films/partial?${params}`); + if (response.ok) { + hasMore = response.headers.get("X-Has-More") === "true"; + html = await response.text(); } - } catch (error) { - console.error("Failed to load more films", error); - } finally { - loading = false; + } catch (err) { + console.error("Failed to reset feed", err); + } + feedContainer.replaceChildren(); + if (html.trim()) { + feedContainer.insertAdjacentHTML("beforeend", html); + attachStarListeners(feedContainer); } - }; + const oldSentinel = document.querySelector("#feed-sentinel"); + if (oldSentinel) oldSentinel.remove(); + if (hasMore) { + const newSentinel = document.createElement("div"); + newSentinel.id = "feed-sentinel"; + newSentinel.dataset.shelf = shelf; + newSentinel.dataset.offset = "20"; + newSentinel.style.cssText = "height: 1px; margin: 20px 0;"; + feedContainer.insertAdjacentElement("afterend", newSentinel); + startInfiniteScroll(newSentinel); + } + } - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - loadMore(); - } + const searchInput = document.querySelector("#film-search"); + const sortSelect = document.querySelector("#film-sort"); + + if (searchInput) { + let debounceTimer = null; + searchInput.addEventListener("input", () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + currentQ = searchInput.value.trim(); + resetFeed(); + }, 300); }); - }, { rootMargin: "100px" }); + } - observer.observe(feedSentinel); -} + if (sortSelect) { + sortSelect.addEventListener("change", () => { + currentSort = sortSelect.value; + resetFeed(); + }); + } +})(); |
