summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/app.js107
1 files changed, 107 insertions, 0 deletions
diff --git a/static/app.js b/static/app.js
index b942d7e..02f70f8 100644
--- a/static/app.js
+++ b/static/app.js
@@ -201,3 +201,110 @@ document.querySelectorAll(".star-control").forEach((control) => {
});
});
});
+
+// Infinite scroll for film lists
+const feedSentinel = document.querySelector("#feed-sentinel");
+if (feedSentinel) {
+ 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;
+
+ const hasMore = response.headers.get("X-Has-More") === "true";
+ const html = await response.text();
+
+ if (html.trim()) {
+ feedContainer.insertAdjacentHTML("beforeend", html);
+
+ // 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();
+ }
+ }
+ }
+
+ // 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;
+
+ 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 }),
+ });
+
+ const data = await response.json();
+ if (!response.ok) return;
+
+ syncStarControl(control, Number(data.stars || 0));
+ } catch (error) {
+ console.error("Failed to update stars", error);
+ } finally {
+ button.disabled = false;
+ }
+ });
+ });
+ }
+ });
+ }
+
+ if (!hasMore) {
+ observer.disconnect();
+ feedSentinel.remove();
+ } else {
+ feedSentinel.dataset.offset = String(offset + 20);
+ }
+ } catch (error) {
+ console.error("Failed to load more films", error);
+ } finally {
+ loading = false;
+ }
+ };
+
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ loadMore();
+ }
+ });
+ }, { rootMargin: "100px" });
+
+ observer.observe(feedSentinel);
+}