summaryrefslogtreecommitdiff
path: root/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js235
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();
+ });
+ }
+})();