// Hamburger menu toggle const menuToggle = document.querySelector("#menu-toggle"); const navActions = document.querySelector("#nav-actions"); if (menuToggle && navActions) { menuToggle.addEventListener("click", () => { const isOpen = navActions.classList.toggle("is-open"); menuToggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); }); // Close menu when a link is clicked navActions.querySelectorAll("a").forEach((link) => { link.addEventListener("click", () => { navActions.classList.remove("is-open"); menuToggle.setAttribute("aria-expanded", "false"); }); }); // Close menu when clicking outside document.addEventListener("click", (event) => { if (!navActions.contains(event.target) && !menuToggle.contains(event.target)) { navActions.classList.remove("is-open"); menuToggle.setAttribute("aria-expanded", "false"); } }); } const tmdbQuery = document.querySelector("#tmdb-query"); const tmdbButton = document.querySelector("#tmdb-search"); const tmdbResults = document.querySelector("#tmdb-results"); const syncStarControl = (control, stars) => { control.dataset.currentStars = String(stars); control.dataset.previewStars = ""; control.querySelectorAll(".star-button").forEach((button) => { const value = Number(button.dataset.stars || 0); const active = stars >= value; button.classList.toggle("is-active", active); button.classList.remove("is-preview"); button.setAttribute("aria-pressed", active ? "true" : "false"); }); }; const previewStarControl = (control, stars) => { control.dataset.previewStars = String(stars); control.querySelectorAll(".star-button").forEach((button) => { const value = Number(button.dataset.stars || 0); const previewed = stars >= value; button.classList.toggle("is-preview", previewed); }); }; const clearStarPreview = (control) => { control.dataset.previewStars = ""; const stars = Number(control.dataset.currentStars || 0); control.querySelectorAll(".star-button").forEach((button) => { const value = Number(button.dataset.stars || 0); button.classList.toggle("is-preview", false); button.classList.toggle("is-active", stars >= value); }); }; const setValue = (selector, value) => { const element = document.querySelector(selector); if (element && value !== null && value !== undefined) { element.value = value; } }; const setPoster = (url) => { setValue("#poster_url", url || ""); const preview = document.querySelector("#poster-preview"); if (!preview) return; if (url) { preview.src = url; preview.classList.add("is-visible"); } else { preview.removeAttribute("src"); preview.classList.remove("is-visible"); } }; const clearResults = () => { if (tmdbResults) { tmdbResults.replaceChildren(); } }; const renderMessage = (message) => { clearResults(); const node = document.createElement("p"); node.className = "tmdb-message"; node.textContent = message; tmdbResults.appendChild(node); }; const applyResult = (film) => { setValue("#title", film.title || ""); setValue("#original_title", film.original_title || ""); setValue("#director", film.director || ""); setValue("#year", film.year || ""); setValue("#country", film.country || ""); setValue("#language", film.language || ""); setValue("#runtime", film.runtime || ""); setValue("#tmdb_id", film.tmdb_id || ""); setPoster(film.poster_url); clearResults(); }; const renderResults = (films) => { clearResults(); if (!films.length) { renderMessage("No matches found."); return; } films.forEach((film) => { const button = document.createElement("button"); button.type = "button"; button.className = "tmdb-result"; button.addEventListener("click", () => applyResult(film)); if (film.poster_url) { const image = document.createElement("img"); image.src = film.poster_url; image.alt = ""; button.appendChild(image); } const text = document.createElement("span"); const title = document.createElement("strong"); title.textContent = film.year ? `${film.title} (${film.year})` : film.title; text.appendChild(title); if (film.director) { const director = document.createElement("small"); director.textContent = film.director; text.appendChild(director); } button.appendChild(text); tmdbResults.appendChild(button); }); }; const searchTmdb = async () => { if (!tmdbQuery || !tmdbResults) return; const query = tmdbQuery.value.trim(); if (query.length < 2) { renderMessage("Enter at least two characters."); return; } renderMessage("Searching..."); try { const response = await fetch(`/tmdb/search?q=${encodeURIComponent(query)}`); const data = await response.json(); if (!response.ok) { renderMessage(data.error || "Search failed."); return; } renderResults(data.results || []); } catch (error) { renderMessage("Search failed."); } }; if (tmdbButton && tmdbQuery) { tmdbButton.addEventListener("click", searchTmdb); tmdbQuery.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); searchTmdb(); } }); } document.querySelectorAll("form[data-confirm]").forEach((form) => { form.addEventListener("submit", (event) => { const message = form.dataset.confirm; if (message && !window.confirm(message)) { event.preventDefault(); } }); }); document.querySelectorAll(".star-control").forEach((control) => { 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; } }); }); }); // 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); }