Statistics page redesign (#60)

* feat: Add modal to display watched movies with details

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor movie modal: remove search, improve category display

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor movie category display to use details element

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor movie playback and add clickability to stats

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor: Improve movie linking and player logic

Co-authored-by: mathieumoran1 <[email protected]>

* Prevent caching of sensitive and dynamic API data

Co-authored-by: mathieumoran1 <[email protected]>

* feat: Prevent caching of sensitive admin data

Co-authored-by: mathieumoran1 <[email protected]>

* feat: Add no-cache headers to API requests

Co-authored-by: mathieumoran1 <[email protected]>

* Remove unused movie sorting functionality

The movie sorting dropdown and its associated logic were removed as they are no longer needed.

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor admin settings to use tabs instead of accordion

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor: Adjust button layout for active year selection

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor active year selection UI

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor: Improve admin settings layout for better responsiveness

Co-authored-by: mathieumoran1 <[email protected]>

* Remove redundant completion explanation from user-stat.html

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor user stats page to display year in ranking tab badge

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor: Rename 'Finisseur(s)' column to 'Nombre'

Co-authored-by: mathieumoran1 <[email protected]>

* Refactor: Remove redundant completer count column

Co-authored-by: mathieumoran1 <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
This commit was merged in pull request #60.
This commit is contained in:
Mathieu Moran
2025-12-22 18:16:37 -05:00
committed by GitHub
co-authored by Cursor Agent
parent 82f9462dda
commit 79b9f4a5a3
10 changed files with 603 additions and 209 deletions
+37 -13
View File
@@ -162,7 +162,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchActiveYear() {
try {
const res = await fetch('/api/settings/year', { method: 'GET' });
const res = await fetch('/api/settings/year', { method: 'GET', cache: 'no-cache' });
if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json();
const year = Number(data?.year);
@@ -174,7 +174,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchOscarEffectiveDate(year) {
try {
const res = await fetch(`/api/settings/oscar-date?year=${encodeURIComponent(String(year))}`, { method: 'GET' });
const res = await fetch(`/api/settings/oscar-date?year=${encodeURIComponent(String(year))}`, { method: 'GET', cache: 'no-cache' });
if (!res.ok) throw new Error('Failed to fetch oscar date');
const data = await res.json();
const effectiveDate = typeof data?.effectiveDate === 'string' ? data.effectiveDate : null;
@@ -194,6 +194,7 @@ window.addEventListener('DOMContentLoaded', async function () {
const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
});
if (!res.ok) throw new Error('Failed to fetch movies summary');
return await res.json();
@@ -319,6 +320,7 @@ window.addEventListener('DOMContentLoaded', async function () {
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
});
if (!res.ok) {
@@ -348,15 +350,26 @@ window.addEventListener('DOMContentLoaded', async function () {
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 cleanUrl = (v) => {
const s = (typeof v === 'string' ? v.trim() : '');
// Treat placeholders as "no link" so you never need to store '#'
if (!s || s === '#' || s.toLowerCase() === 'about:blank') return '';
return s;
};
const hasVideoSrc = !!cleanUrl(movie?.video_src);
const hasEmbedSrc = !!cleanUrl(movie?.embed_src);
const hasNewPlayerSource = hasVideoSrc || hasEmbedSrc;
const hasLegacy = !!(movie && typeof movie.vod_link === 'string' && movie.vod_link.trim());
const legacyUrl = cleanUrl(movie?.vod_link);
const hasLegacy = !!legacyUrl;
// If the movie only has legacy `vod_link`, skip the player entirely and open the original URL in a new tab.
// Navigation:
// - Prefer in-app player when we have a non-legacy source
// - If only legacy exists, open the legacy URL
// - If there is no source at all, render as non-clickable (no "#")
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)}` : '#'));
: (hasLegacy ? legacyUrl : '');
const isClickable = !!playerUrl;
const movieDiv = document.createElement('div');
movieDiv.classList.add('col-12', 'col-sm-6', 'col-lg-4', 'movie-card');
@@ -364,15 +377,25 @@ window.addEventListener('DOMContentLoaded', async function () {
movieDiv.innerHTML = `
<div class="card h-100 shadow-sm">
${isChecked ? '<span class="badge bg-success position-absolute top-0 start-0 m-2 watched-badge">VISIONNÉ</span>' : ''}
<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none">
<div class="text-center pt-3 px-3">
<img src="${movie.poster}" class="img-fluid rounded movie-poster" alt="${movie.title}">
</div>
</a>
${
isClickable
? `<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none">
<div class="text-center pt-3 px-3">
<img src="${movie.poster}" class="img-fluid rounded movie-poster" alt="${movie.title}">
</div>
</a>`
: `<div class="text-center pt-3 px-3">
<img src="${movie.poster}" class="img-fluid rounded movie-poster" alt="${movie.title}">
</div>`
}
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start gap-2">
<h5 class="card-title mb-1 flex-grow-1">
<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none text-dark">${movie.title}</a>
${
isClickable
? `<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none text-dark">${movie.title}</a>`
: `<span class="text-dark">${movie.title}</span>`
}
</h5>
<div class="text-nowrap small text-muted">
⭐ ${movie.rating}
@@ -381,6 +404,7 @@ window.addEventListener('DOMContentLoaded', async function () {
</a>
</div>
</div>
${!isClickable ? '<div class="text-muted small mb-1">Aucune source</div>' : ''}
<div class="text-muted small fw-semibold fst-italic mb-2">${movie.category}</div>
<p class="card-text small flex-grow-1 mb-2">${movie.description}</p>
${isChecked ? `<div class="text-muted small"><span class="fw-semibold text-dark">Regardé le:</span> ${watchedDate}</div>` : ''}