Statistics page redesign #60

Merged
quoije merged 17 commits from cursor/statistics-page-redesign-d08a into 2026 2025-12-22 18:16:37 -05:00
5 changed files with 70 additions and 32 deletions
Showing only changes of commit c207a03f39 - Show all commits
+9
View File
@@ -236,6 +236,15 @@ footer {
border-radius: 14px;
}
.stats-movie-item--clickable {
cursor: pointer;
}
.stats-movie-item--clickable:hover {
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0, 0, 0, 0.14);
}
.stats-movie-poster {
width: 56px;
height: 84px;
+30 -8
View File
@@ -348,15 +348,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 +375,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">
${
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>
</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 +402,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>` : ''}
+27
View File
@@ -435,6 +435,7 @@ window.addEventListener('DOMContentLoaded', async function () {
moviesEmptyEl.classList.add('d-none');
sorted.forEach((movie) => {
const movieId = safeStr(movie?.movieId).trim();
const imdbId = safeStr(movie?.imdb_id).trim();
const title = safeStr(movie?.title).trim() || '(sans titre)';
const category = safeStr(movie?.category).trim();
@@ -445,6 +446,12 @@ window.addEventListener('DOMContentLoaded', async function () {
const item = document.createElement('div');
item.className = 'stats-movie-item';
if (movieId) {
item.classList.add('stats-movie-item--clickable');
item.setAttribute('role', 'link');
item.setAttribute('tabindex', '0');
item.setAttribute('aria-label', `Ouvrir le lecteur: ${title}`);
}
const poster = document.createElement('img');
poster.className = 'stats-movie-poster';
@@ -461,6 +468,26 @@ window.addEventListener('DOMContentLoaded', async function () {
try { poster.removeAttribute('src'); } catch (_) {}
});
function openPlayerIfPossible(evt) {
if (!movieId) return;
// Don't hijack clicks inside controls (IMDb button, details summary, etc.)
const target = evt?.target;
if (target && typeof target.closest === 'function') {
if (target.closest('a, button, summary, details')) return;
}
window.location.href = `/player.html?id=${encodeURIComponent(movieId)}`;
}
if (movieId) {
item.addEventListener('click', openPlayerIfPossible);
item.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openPlayerIfPossible(e);
}
});
}
const main = document.createElement('div');
main.className = 'flex-grow-1';
-21
View File
@@ -361,16 +361,6 @@ router.post("/add", async (req, res) => {
if (normalizedMode === null) {
return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" });
}
if (
!hasAnySourceIncludingFile({
vod_link: normalizedVod || "",
video_src: normalizedVideo || "",
embed_src: normalizedEmbed || "",
video_file: normalizedFile || "",
})
) {
return res.status(400).json({ message: "Ajoute au moins une source (VOD / video_src / embed_src)." });
}
// Fetch movie details from OMDb API
let movieDetails;
@@ -584,17 +574,6 @@ router.put("/:id", async (req, res) => {
if (poster !== undefined) movie.poster = poster;
}
if (
!hasAnySourceIncludingFile({
vod_link: movie.vod_link || "",
video_src: movie.video_src || "",
embed_src: movie.embed_src || "",
video_file: movie.video_file || "",
})
) {
return res.status(400).json({ message: "Ajoute au moins une source (VOD / video_src / embed_src / video_file)." });
}
await movie.save();
cacheClear();
return res.status(200).json(movie);
+1
View File
@@ -151,6 +151,7 @@ router.get("/stats", verifyToken, async (req, res) => {
const movie = movieByImdbId.get(imdbId);
if (!movie) return null;
return {
movieId: movie?._id ? String(movie._id) : null,
imdb_id: movie.imdb_id,
title: movie.title || "",
poster: movie.poster || "",