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
+43 -13
View File
@@ -122,14 +122,20 @@ async function getOrInitActiveYear() {
// Get user stats with watched movie titles and details from the movies collection
router.get("/stats", verifyToken, async (req, res) => {
try {
// Fetch all users
const users = await User.find();
// Auth-scoped response: never allow caching.
res.set("Cache-Control", "private, no-store, must-revalidate");
res.set("Vary", "Authorization");
// Fetch all users (only needed fields)
const users = await User.find().select("name watchedMovies").lean();
// If a year is explicitly provided, use it; otherwise default to the global active year.
const year = parseOscarYear(req.query.year) || await getOrInitActiveYear();
// Fetch movies (optionally filtered by year)
const allMovies = await Movie.find({ year });
const allMovies = await Movie.find({ year })
.select("imdb_id title poster rating category year vod_link player_mode video_src embed_src video_file")
.lean();
const totalMoviesCount = allMovies.length;
if (totalMoviesCount === 0) {
@@ -140,19 +146,35 @@ router.get("/stats", verifyToken, async (req, res) => {
const userStats = users.map((user) => {
const movieByImdbId = new Map(allMovies.map((m) => [m.imdb_id, m]));
// Get watched movies with details from the database
const watchedMovies = user.watchedMovies
.map((wm) => movieByImdbId.get(wm.imdb_id)) // Match movie details
.filter(Boolean); // Remove any `undefined` values (if movie is not found)
// Join watched movies with movie details for the selected year (keep watchedDate)
const watchedRaw = Array.isArray(user?.watchedMovies) ? user.watchedMovies : [];
const watchedMoviesDetails = watchedRaw
.map((wm) => {
const imdbId = typeof wm?.imdb_id === "string" ? wm.imdb_id : "";
if (!imdbId) return null;
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 || "",
rating: movie.rating || "",
category: movie.category || "",
year: Number.isInteger(movie.year) ? movie.year : year,
vod_link: movie.vod_link || "",
player_mode: movie.player_mode || "auto",
video_src: movie.video_src || "",
embed_src: movie.embed_src || "",
video_file: movie.video_file || "",
watchedDate: wm?.watchedDate ? new Date(wm.watchedDate).toISOString() : null,
};
})
.filter(Boolean);
const watchedCount = watchedMovies.length;
const watchedCount = watchedMoviesDetails.length;
const watchedRatio = ((watchedCount / totalMoviesCount) * 100).toFixed(1);
const watchedMoviesDetails = watchedMovies.map((movie) => ({
imdb_id: movie.imdb_id,
title: movie.title
}));
return {
name: user.name,
year,
@@ -174,6 +196,10 @@ router.get("/stats", verifyToken, async (req, res) => {
// Returns: { years: number[], totals: { [year]: number }, completersByYear: { [year]: {name, watchedCount, totalMoviesCount}[] } }
router.get("/completions", verifyToken, async (req, res) => {
try {
// Auth-scoped response: never allow caching.
res.set("Cache-Control", "private, no-store, must-revalidate");
res.set("Vary", "Authorization");
const yearsRaw = await Movie.distinct("year");
const years = (Array.isArray(yearsRaw) ? yearsRaw : [])
.map((y) => Number(y))
@@ -376,6 +402,10 @@ router.get("/admin/list", verifyToken, async (req, res) => {
}
try {
// Admin + auth-scoped data: never allow caching.
res.set("Cache-Control", "private, no-store, must-revalidate");
res.set("Vary", "Authorization");
const users = await User.find()
.select("name email role mustChangePassword")
.sort({ name: 1, email: 1 });