From e318e25ed123fd363be6bce545e1e31c5d57014b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 21:29:21 +0000 Subject: [PATCH] Add loading and error states for movie lists Co-authored-by: mathieumoran1 --- public/js/app_checklist.js | 98 ++++++++++++++------- public/js/app_movies.js | 170 +++++++++++++++++++++---------------- 2 files changed, 168 insertions(+), 100 deletions(-) diff --git a/public/js/app_checklist.js b/public/js/app_checklist.js index 85c51a2..b0779ab 100644 --- a/public/js/app_checklist.js +++ b/public/js/app_checklist.js @@ -1,5 +1,35 @@ window.onload = async function () { const token = localStorage.getItem('auth_token'); + const movieTableBody = document.getElementById('movie-table-body'); + + function showTableLoading() { + if (!movieTableBody) return; + movieTableBody.innerHTML = ` + + +
+
+ Chargement... +
+ Chargement… +
+ + + `; + } + + function showTableError(message) { + if (!movieTableBody) return; + movieTableBody.innerHTML = ` + + + + + + `; + } const DEFAULT_COMPLETION_MODAL = Object.freeze({ title: 'Félicitations! very nice 🎉🎉🎉', @@ -155,39 +185,49 @@ window.onload = async function () { window.location.href = '/'; } - // Fetch remaining data in parallel (Render latency is high). - const [completionModalContent, moviesRes, watchedRes] = await Promise.all([ - fetchCompletionModalContent(), - fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } - }), - fetch('/api/movies/watchedMovies', { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } - }) - ]); + showTableLoading(); - applyCompletionModalContent(completionModalContent); + let movies = []; + let watchedMovies = []; + let watchedMoviesInYear = []; + let movieImdbIds = new Set(); + try { + // Fetch remaining data in parallel (Render latency is high). + const [completionModalContent, moviesRes, watchedRes] = await Promise.all([ + fetchCompletionModalContent(), + fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }), + fetch('/api/movies/watchedMovies', { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }) + ]); - if (!moviesRes.ok) { - localStorage.removeItem('auth_token'); - window.location.href = '/'; + applyCompletionModalContent(completionModalContent); + + if (!moviesRes.ok) { + localStorage.removeItem('auth_token'); + window.location.href = '/'; + return; + } + + movies = await moviesRes.json(); + + // Sort movies alphabetically by title + movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' })); + + if (watchedRes.ok) { + watchedMovies = await watchedRes.json(); + } + } catch (e) { + showTableError('Impossible de charger la checklist. Réessaie dans quelques instants.'); return; } - const movies = await moviesRes.json(); - - // Sort movies alphabetically by title - movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' })); - - let watchedMovies = []; - if (watchedRes.ok) { - watchedMovies = await watchedRes.json(); - } - - const movieImdbIds = new Set(movies.map((m) => m.imdb_id).filter(Boolean)); - let watchedMoviesInYear = watchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id)); + movieImdbIds = new Set(movies.map((m) => m.imdb_id).filter(Boolean)); + watchedMoviesInYear = watchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id)); const totalMoviesCount = movies.length; const watchedMoviesCount = watchedMoviesInYear.length; @@ -248,7 +288,7 @@ window.onload = async function () { updateProgressBar(watchedMoviesCount, totalMoviesCount); - const movieTableBody = document.getElementById('movie-table-body'); + if (movieTableBody) movieTableBody.innerHTML = ''; movies.forEach(movie => { const watchedMovie = watchedMoviesInYear.find(wm => wm.imdb_id === movie.imdb_id); const watchedDate = watchedMovie ? new Date(watchedMovie.watchedDate).toLocaleString() : ''; diff --git a/public/js/app_movies.js b/public/js/app_movies.js index b871423..7755eb4 100644 --- a/public/js/app_movies.js +++ b/public/js/app_movies.js @@ -1,4 +1,24 @@ window.onload = async function () { + function setLoading(el) { + if (!el) return; + el.innerHTML = ` +
+
+ Chargement... +
+
+ `; + } + + function setLoadError(el, message) { + if (!el) return; + el.innerHTML = ` + + `; + } + async function fetchActiveYear() { try { const res = await fetch('/api/settings/year', { method: 'GET' }); @@ -105,83 +125,91 @@ window.onload = async function () { window.location.href = '/'; } + const moviesList = document.getElementById('movies-list'); + setLoading(moviesList); + // Populate "Dernière mise à jour des films" banner (per active year) await fetchMoviesLastUpdated(activeYear, token); - // Fetch movies + watched list in parallel (Render latency is high). - const [res, watchedRes] = await Promise.all([ - fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` }, - }), - fetch('/api/movies/watchedMovies', { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` }, - }) - ]); + try { + // Fetch movies + watched list in parallel (Render latency is high). + const [res, watchedRes] = await Promise.all([ + fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` }, + }), + fetch('/api/movies/watchedMovies', { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` }, + }) + ]); - if (!res.ok) { - localStorage.removeItem('auth_token'); - window.location.href = '/'; - return; + if (!res.ok) { + localStorage.removeItem('auth_token'); + window.location.href = '/'; + return; + } + + const movies = await res.json(); + + // Sort movies alphabetically by title (server also sorts, but keep client as a fallback) + movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' })); + + let watchedMovies = []; + if (watchedRes.ok) { + watchedMovies = await watchedRes.json(); + } + + const movieImdbIds = new Set(movies.map((m) => m.imdb_id).filter(Boolean)); + const watchedMoviesInYear = watchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id)); + + const totalMoviesCount = movies.length; + const watchedMoviesCount = watchedMoviesInYear.length; + const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0'; + const ratioText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${ratioPct}%)`; + document.getElementById('watched-ratio').innerText = ratioText; + + if (moviesList) moviesList.innerHTML = ''; + + movies.forEach(movie => { + const isChecked = watchedMoviesInYear.some(watchedMovie => watchedMovie.imdb_id === movie.imdb_id); + const watchedMovie = watchedMoviesInYear.find(wm => wm.imdb_id === movie.imdb_id); + const watchedDate = watchedMovie ? new Date(watchedMovie.watchedDate).toLocaleString() : ''; + const hasVideoSrc = !!(movie && typeof movie.video_src === 'string' && movie.video_src.trim()); + const hasEmbedSrc = !!(movie && typeof movie.embed_src === 'string' && movie.embed_src.trim()); + const hasNewPlayerSource = hasVideoSrc || hasEmbedSrc; + const hasLegacy = !!(movie && typeof movie.vod_link === 'string' && movie.vod_link.trim()); + + // If the movie only has legacy `vod_link`, skip the player entirely and open the original URL in a new tab. + const playerUrl = (movie && movie._id && hasNewPlayerSource) + ? `/player.html?id=${encodeURIComponent(movie._id)}` + : (hasLegacy ? movie.vod_link : (movie && movie._id ? `/player.html?id=${encodeURIComponent(movie._id)}` : '#')); + + const movieDiv = document.createElement('div'); + movieDiv.classList.add('col-md-4', 'mb-4', 'movie-card'); + movieDiv.setAttribute('data-imdb-id', movie.imdb_id); + movieDiv.innerHTML = ` +
+ ${isChecked ? '
VISIONNÉ
' : ''} + + ${movie.title} + +
+
+ ${movie.title} + ⭐ ${movie.rating} +
+

${movie.category}

+

${movie.description}

+ ${isChecked ? `

Regardé le: ${watchedDate}

` : ''} +
+
`; + moviesList.appendChild(movieDiv); + }); + } catch (e) { + setLoadError(moviesList, 'Impossible de charger la liste des films. Réessaie dans quelques instants.'); } - const movies = await res.json(); - - // Sort movies alphabetically by title (server also sorts, but keep client as a fallback) - movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' })); - - let watchedMovies = []; - if (watchedRes.ok) { - watchedMovies = await watchedRes.json(); - } - - const moviesList = document.getElementById('movies-list'); - const movieImdbIds = new Set(movies.map((m) => m.imdb_id).filter(Boolean)); - const watchedMoviesInYear = watchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id)); - - const totalMoviesCount = movies.length; - const watchedMoviesCount = watchedMoviesInYear.length; - const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0'; - const ratioText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${ratioPct}%)`; - document.getElementById('watched-ratio').innerText = ratioText; - - movies.forEach(movie => { - const isChecked = watchedMoviesInYear.some(watchedMovie => watchedMovie.imdb_id === movie.imdb_id); - const watchedMovie = watchedMoviesInYear.find(wm => wm.imdb_id === movie.imdb_id); - const watchedDate = watchedMovie ? new Date(watchedMovie.watchedDate).toLocaleString() : ''; - const hasVideoSrc = !!(movie && typeof movie.video_src === 'string' && movie.video_src.trim()); - const hasEmbedSrc = !!(movie && typeof movie.embed_src === 'string' && movie.embed_src.trim()); - const hasNewPlayerSource = hasVideoSrc || hasEmbedSrc; - const hasLegacy = !!(movie && typeof movie.vod_link === 'string' && movie.vod_link.trim()); - - // If the movie only has legacy `vod_link`, skip the player entirely and open the original URL in a new tab. - const playerUrl = (movie && movie._id && hasNewPlayerSource) - ? `/player.html?id=${encodeURIComponent(movie._id)}` - : (hasLegacy ? movie.vod_link : (movie && movie._id ? `/player.html?id=${encodeURIComponent(movie._id)}` : '#')); - - const movieDiv = document.createElement('div'); - movieDiv.classList.add('col-md-4', 'mb-4', 'movie-card'); - movieDiv.setAttribute('data-imdb-id', movie.imdb_id); - movieDiv.innerHTML = ` -
- ${isChecked ? '
VISIONNÉ
' : ''} - - ${movie.title} - -
-
- ${movie.title} - ⭐ ${movie.rating} -
-

${movie.category}

-

${movie.description}

- ${isChecked ? `

Regardé le: ${watchedDate}

` : ''} -
-
`; - moviesList.appendChild(movieDiv); - }); - document.getElementById('log-off').addEventListener('click', function () { localStorage.removeItem('auth_token'); window.location.href = '/';