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 30 additions and 12 deletions
Showing only changes of commit bacd25f1ac - Show all commits
+9 -6
View File
@@ -159,7 +159,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchCompletionModalContent() { async function fetchCompletionModalContent() {
try { try {
const res = await fetch('/api/settings/completion-modal', { method: 'GET' }); const res = await fetch('/api/settings/completion-modal', { method: 'GET', cache: 'no-cache' });
if (!res.ok) throw new Error('Failed to fetch completion modal'); if (!res.ok) throw new Error('Failed to fetch completion modal');
const data = await res.json(); const data = await res.json();
return { return {
@@ -217,7 +217,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchActiveYear() { async function fetchActiveYear() {
try { 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'); if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json(); const data = await res.json();
const year = Number(data?.year); const year = Number(data?.year);
@@ -229,7 +229,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchOscarEffectiveDate(year) { async function fetchOscarEffectiveDate(year) {
try { 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'); if (!res.ok) throw new Error('Failed to fetch oscar date');
const data = await res.json(); const data = await res.json();
const effectiveDate = typeof data?.effectiveDate === 'string' ? data.effectiveDate : null; const effectiveDate = typeof data?.effectiveDate === 'string' ? data.effectiveDate : null;
@@ -247,7 +247,8 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchMoviesSummary(year) { async function fetchMoviesSummary(year) {
const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, { const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}); });
if (!res.ok) throw new Error('Failed to fetch movies summary'); if (!res.ok) throw new Error('Failed to fetch movies summary');
return await res.json(); return await res.json();
@@ -346,7 +347,8 @@ window.addEventListener('DOMContentLoaded', async function () {
fetchCompletionModalContent(), fetchCompletionModalContent(),
fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, { fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}) })
]); ]);
@@ -498,7 +500,8 @@ window.addEventListener('DOMContentLoaded', async function () {
const updatedWatchedMoviesRes = await fetch('/api/movies/watchedMovies', { const updatedWatchedMoviesRes = await fetch('/api/movies/watchedMovies', {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}); });
if (updatedWatchedMoviesRes.ok) { if (updatedWatchedMoviesRes.ok) {
+4 -2
View File
@@ -162,7 +162,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchActiveYear() { async function fetchActiveYear() {
try { 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'); if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json(); const data = await res.json();
const year = Number(data?.year); const year = Number(data?.year);
@@ -174,7 +174,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchOscarEffectiveDate(year) { async function fetchOscarEffectiveDate(year) {
try { 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'); if (!res.ok) throw new Error('Failed to fetch oscar date');
const data = await res.json(); const data = await res.json();
const effectiveDate = typeof data?.effectiveDate === 'string' ? data.effectiveDate : null; 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))}`, { const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }, headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}); });
if (!res.ok) throw new Error('Failed to fetch movies summary'); if (!res.ok) throw new Error('Failed to fetch movies summary');
return await res.json(); return await res.json();
@@ -319,6 +320,7 @@ window.addEventListener('DOMContentLoaded', async function () {
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, { const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }, headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}); });
if (!res.ok) { if (!res.ok) {
+6 -4
View File
@@ -130,7 +130,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchActiveYear() { async function fetchActiveYear() {
try { 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'); if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json(); const data = await res.json();
const year = Number(data?.year); const year = Number(data?.year);
@@ -142,7 +142,7 @@ window.addEventListener('DOMContentLoaded', async function () {
async function fetchWinners() { async function fetchWinners() {
try { try {
const res = await fetch('/api/settings/winners', { method: 'GET' }); const res = await fetch('/api/settings/winners', { method: 'GET', cache: 'no-cache' });
if (!res.ok) throw new Error('Failed to fetch winners'); if (!res.ok) throw new Error('Failed to fetch winners');
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
return Array.isArray(data?.winners) ? data.winners : []; return Array.isArray(data?.winners) ? data.winners : [];
@@ -155,7 +155,8 @@ window.addEventListener('DOMContentLoaded', async function () {
try { try {
const res = await fetch('/api/users/completions', { const res = await fetch('/api/users/completions', {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}); });
if (!res.ok) throw new Error('Failed to fetch completions'); if (!res.ok) throw new Error('Failed to fetch completions');
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
@@ -650,7 +651,8 @@ window.addEventListener('DOMContentLoaded', async function () {
const [statsRes, winners, completions] = await Promise.all([ const [statsRes, winners, completions] = await Promise.all([
fetch(statsUrl, { fetch(statsUrl, {
method: 'GET', method: 'GET',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-cache',
}), }),
fetchWinners(), fetchWinners(),
fetchCompletions(), fetchCompletions(),
+3
View File
@@ -291,6 +291,9 @@ router.get("/watchedMovies", async (req, res) => {
const user = await User.findById(decoded.id).select("watchedMovies").lean(); // Get user by ID const user = await User.findById(decoded.id).select("watchedMovies").lean(); // Get user by ID
if (!user) return res.status(404).json({ message: "User not found" }); if (!user) return res.status(404).json({ message: "User not found" });
// User-specific: never allow shared/proxy caching.
res.set("Cache-Control", "private, no-store, must-revalidate");
res.set("Vary", "Authorization");
res.status(200).json(user.watchedMovies); // Return the list of watched movies res.status(200).json(user.watchedMovies); // Return the list of watched movies
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
+8
View File
@@ -122,6 +122,10 @@ async function getOrInitActiveYear() {
// Get user stats with watched movie titles and details from the movies collection // Get user stats with watched movie titles and details from the movies collection
router.get("/stats", verifyToken, async (req, res) => { router.get("/stats", verifyToken, async (req, res) => {
try { try {
// 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) // Fetch all users (only needed fields)
const users = await User.find().select("name watchedMovies").lean(); const users = await User.find().select("name watchedMovies").lean();
@@ -192,6 +196,10 @@ router.get("/stats", verifyToken, async (req, res) => {
// Returns: { years: number[], totals: { [year]: number }, completersByYear: { [year]: {name, watchedCount, totalMoviesCount}[] } } // Returns: { years: number[], totals: { [year]: number }, completersByYear: { [year]: {name, watchedCount, totalMoviesCount}[] } }
router.get("/completions", verifyToken, async (req, res) => { router.get("/completions", verifyToken, async (req, res) => {
try { 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 yearsRaw = await Movie.distinct("year");
const years = (Array.isArray(yearsRaw) ? yearsRaw : []) const years = (Array.isArray(yearsRaw) ? yearsRaw : [])
.map((y) => Number(y)) .map((y) => Number(y))