Global movie year setting #7

Merged
quoije merged 1 commits from cursor/global-movie-year-setting-3e2d into 2026 2025-12-20 17:48:44 -05:00
11 changed files with 218 additions and 30 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars 2026 - Checklist</title>
<title>Pool Oscars - Checklist</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
<style>
@@ -79,7 +79,7 @@
<div class="container mt-5">
<h2>Checklist</h2>
<div id="watched-ratio" class="ratio"></div>
<h3 class="fs-6" style="margin-bottom: -1px;">Temps avant les Oscars (15 mars 2026) - <span id="time-left"></span></h3>
<h3 class="fs-6" style="margin-bottom: -1px;">Temps avant les Oscars (15 mars <span id="oscar-year">—</span>) - <span id="time-left"></span></h3>
<h3 class="fs-6"><span id="movies-per-day"></span></h3>
<div class="progress my-3">
<div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0%; background-color: red;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
+19 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars 2026 - Admin</title>
<title>Pool Oscars - Admin</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
<style>
@@ -52,6 +52,24 @@
</nav>
<div class="container mt-5" style="max-width: 720px;">
<h2>Admin - Année active</h2>
<p class="text-muted mb-3">
Cette année pilote tout le site (films affichés, checklist et statistiques).
</p>
<div class="card p-3 mb-4">
<div class="row g-2 align-items-end">
<div class="col-md-6">
<label for="active_oscar_year" class="form-label">Année active</label>
<input type="number" class="form-control" id="active_oscar_year" placeholder="2026" min="1900" max="3000">
<div class="form-text">Ex: mets 2025 → tout le monde voit les stats/films 2025.</div>
</div>
<div class="col-md-6">
<button type="button" id="save-active-year" class="btn btn-warning w-100">Appliquer l’année</button>
</div>
</div>
</div>
<h2>Admin - Ajouter un film</h2>
<p class="text-muted mb-4">
Ajoute un film via l’API (<code>/api/movies/add</code>). Le serveur récupère automatiquement le titre/plot/affiche depuis OMDb avec l’<code>imdb_id</code>.
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars 2026</title>
<title>Pool Oscars</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
<style>
@@ -46,7 +46,7 @@
<!-- Footer Section -->
<footer>
<p id="footer-copy"><a id="copy" href="https://github.com/quoije/oscars-pool">quoije</a> - deux mille vingt-six</p>
<p id="footer-copy"><a id="copy" href="https://github.com/quoije/oscars-pool">quoije</a></p>
<p id="footer-text"></p>
</footer>
+30 -7
View File
@@ -1,7 +1,24 @@
window.onload = async function () {
const token = localStorage.getItem('auth_token');
const targetDate = new Date('March 15, 2026');
async function fetchActiveYear() {
try {
const res = await fetch('/api/settings/year', { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : new Date().getFullYear();
} catch (_) {
return new Date().getFullYear();
}
}
const activeYear = await fetchActiveYear();
document.title = `Pool Oscars ${activeYear} - Checklist`;
const oscarYearEl = document.getElementById('oscar-year');
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
const targetDate = new Date(`March 15, ${activeYear}`);
const currentDate = new Date();
const timeDifference = targetDate - currentDate;
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
@@ -29,7 +46,7 @@ window.onload = async function () {
window.location.href = '/';
}
const res = await fetch('/api/movies', {
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
@@ -55,9 +72,13 @@ window.onload = async function () {
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));
const totalMoviesCount = movies.length;
const watchedMoviesCount = watchedMovies.length;
document.getElementById('watched-ratio').innerText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1)}%)`;
const watchedMoviesCount = watchedMoviesInYear.length;
const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
document.getElementById('watched-ratio').innerText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${ratioPct}%)`;
function launchConfetti() {
const duration = 3 * 1000; // 3 seconds
@@ -111,7 +132,7 @@ window.onload = async function () {
const movieTableBody = document.getElementById('movie-table-body');
movies.forEach(movie => {
const watchedMovie = watchedMovies.find(wm => wm.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 movieRow = document.createElement('tr');
movieRow.innerHTML = `
@@ -169,8 +190,10 @@ window.onload = async function () {
if (updatedWatchedMoviesRes.ok) {
const updatedWatchedMovies = await updatedWatchedMoviesRes.json();
const updatedWatchedCount = updatedWatchedMovies.length;
document.getElementById('watched-ratio').innerText = `Vu: ${updatedWatchedCount} / ${totalMoviesCount} (${((updatedWatchedCount / totalMoviesCount) * 100).toFixed(1)}%)`;
watchedMoviesInYear = updatedWatchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
const updatedWatchedCount = watchedMoviesInYear.length;
const updatedPct = totalMoviesCount > 0 ? ((updatedWatchedCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
document.getElementById('watched-ratio').innerText = `Vu: ${updatedWatchedCount} / ${totalMoviesCount} (${updatedPct}%)`;
updateProgressBar(updatedWatchedCount, totalMoviesCount);
// Recalculate and update movies per day
+83 -2
View File
@@ -33,6 +33,9 @@ window.onload = async function () {
const form = document.getElementById('add-movie-form');
const yearInput = document.getElementById('oscar_year');
const activeYearInput = document.getElementById('active_oscar_year');
const saveActiveYearBtn = document.getElementById('save-active-year');
const manageYearSelect = document.getElementById('manage_year');
const adminMoviesBody = document.getElementById('admin-movies-body');
const refreshMoviesBtn = document.getElementById('refresh-movies');
@@ -57,6 +60,7 @@ window.onload = async function () {
const editDescriptionEl = document.getElementById('edit_description');
let moviesById = new Map();
let activeYear = null;
function showResponse(kind, message) {
responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
@@ -71,6 +75,40 @@ window.onload = async function () {
return n;
}
async function fetchActiveYear() {
try {
const res = await fetch('/api/settings/year', { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : null;
} catch (_) {
return null;
}
}
async function setActiveYear(newYear) {
const res = await fetch('/api/settings/year', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ year: newYear })
});
if (!res.ok) {
let msg = `Erreur (${res.status})`;
try {
const data = await res.json();
msg = data.message || data.error || msg;
} catch (_) {}
throw new Error(msg);
}
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : newYear;
}
function isValidImdbId(value) {
return /^tt\d{5,}$/.test(String(value || '').trim());
}
@@ -157,10 +195,20 @@ window.onload = async function () {
const stillExists = years.map(String).includes(previousSelection);
manageYearSelect.value = stillExists ? previousSelection : '';
// Default add-year to latest year if empty
if (years.length > 0 && !yearInput.value) {
// Prefer defaulting UI to active year (if selection is empty/invalid).
const hasSelection = !!manageYearSelect.value;
if (!hasSelection && activeYear && years.map(String).includes(String(activeYear))) {
manageYearSelect.value = String(activeYear);
}
// Default add-year to active year (or latest year) if empty
if (!yearInput.value) {
if (activeYear) {
yearInput.value = String(activeYear);
} else if (years.length > 0) {
yearInput.value = String(years[0]);
}
}
} catch (_) {
// ignore
}
@@ -409,6 +457,39 @@ window.onload = async function () {
}
// Initial load
activeYear = await fetchActiveYear();
if (activeYearInput && activeYear) {
activeYearInput.value = String(activeYear);
document.title = `Pool Oscars ${activeYear} - Admin`;
}
if (saveActiveYearBtn && activeYearInput) {
saveActiveYearBtn.addEventListener('click', async () => {
const y = parseYear(activeYearInput.value);
if (!y) {
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
return;
}
saveActiveYearBtn.disabled = true;
const oldText = saveActiveYearBtn.textContent;
saveActiveYearBtn.textContent = 'Application...';
try {
activeYear = await setActiveYear(y);
activeYearInput.value = String(activeYear);
yearInput.value = String(activeYear);
document.title = `Pool Oscars ${activeYear} - Admin`;
showResponse('success', `Année active appliquée: ${activeYear}`);
await refreshYears();
await loadMoviesForManagement();
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
} finally {
saveActiveYearBtn.disabled = false;
saveActiveYearBtn.textContent = oldText;
}
});
}
await refreshYears();
await loadMoviesForManagement();
};
+28 -7
View File
@@ -1,5 +1,22 @@
window.onload = async function () {
const targetDate = new Date('March 15, 2026');
async function fetchActiveYear() {
try {
const res = await fetch('/api/settings/year', { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : new Date().getFullYear();
} catch (_) {
return new Date().getFullYear();
}
}
const activeYear = await fetchActiveYear();
document.title = `Pool Oscars ${activeYear} - Films`;
const oscarYearEl = document.getElementById('oscar-year');
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
const targetDate = new Date(`March 15, ${activeYear}`);
const currentDate = new Date();
const timeDifference = targetDate - currentDate;
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
@@ -29,7 +46,7 @@ window.onload = async function () {
window.location.href = '/';
}
const res = await fetch('/api/movies', {
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` },
});
@@ -56,14 +73,18 @@ window.onload = async function () {
}
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 = watchedMovies.length;
const ratioText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1)}%)`;
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 = watchedMovies.some(watchedMovie => watchedMovie.imdb_id === movie.imdb_id);
const watchedMovie = watchedMovies.find(wm => wm.imdb_id === movie.imdb_id);
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 movieDiv = document.createElement('div');
@@ -108,7 +129,7 @@ window.onload = async function () {
movieCards.forEach(card => {
const imdbId = card.getAttribute('data-imdb-id');
const isWatched = watchedMovies.some(watchedMovie => watchedMovie.imdb_id === imdbId);
const isWatched = watchedMoviesInYear.some(watchedMovie => watchedMovie.imdb_id === imdbId);
if (showUnwatchedOnly && isWatched) {
card.style.display = 'none';
+21 -1
View File
@@ -1,6 +1,18 @@
window.onload = async function () {
const token = localStorage.getItem('auth_token');
async function fetchActiveYear() {
try {
const res = await fetch('/api/settings/year', { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : null;
} catch (_) {
return null;
}
}
if (token) {
try {
const decoded = JSON.parse(atob(token.split('.')[1])); // Manually decoding JWT
@@ -28,7 +40,15 @@ window.onload = async function () {
}
try {
const statsRes = await fetch('/api/users/stats', {
const activeYear = await fetchActiveYear();
if (activeYear) {
document.title = `Pool Oscars ${activeYear} - Statistiques des utilisateurs`;
const h2 = document.querySelector('h2');
if (h2) h2.textContent = `Statistiques des utilisateurs (${activeYear})`;
}
const statsUrl = activeYear ? `/api/users/stats?year=${encodeURIComponent(String(activeYear))}` : '/api/users/stats';
const statsRes = await fetch(statsUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars 2026 - Films</title>
<title>Pool Oscars - Films</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
<style>
@@ -106,7 +106,7 @@
<!-- This will display the ratio -->
</div>
<h3 class="fs-6">Temps avant les Oscars (15 mars 2026) - <span id="time-left"></span></h3>
<h3 class="fs-6">Temps avant les Oscars (15 mars <span id="oscar-year">—</span>) - <span id="time-left"></span></h3>
<h3 class="fs-6 mb-0">
</h3>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars 2026 - Enregistrement</title>
<title>Pool Oscars - Enregistrement</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
</head>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pool Oscars 2026 - Statistiques des utilisateurs</title>
<title>Pool Oscars - Statistiques des utilisateurs</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.navbar-custom {
+28 -3
View File
@@ -3,6 +3,7 @@ const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const Movie = require("../models/Movie");
const Setting = require("../models/Setting");
const axios = require("axios");
const router = express.Router();
@@ -11,6 +12,8 @@ const router = express.Router();
const woof = process.env.DOG_NAMES ? process.env.DOG_NAMES.split(",") : [];
const ACTIVE_YEAR_KEY = "active_oscar_year";
// Fetch movie details from OMDb API
async function fetchMovieDetailsFromOmdb(imdb_id) {
const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
@@ -57,16 +60,38 @@ function parseOscarYear(raw) {
return n;
}
async function getOrInitActiveYear() {
const existing = await Setting.findOne({ key: ACTIVE_YEAR_KEY });
if (existing && typeof existing.value === "number") return existing.value;
// Initialize from latest movie year if available, else from current year.
const latestMovie = await Movie.findOne({ year: { $type: "number" } })
.sort({ year: -1 })
.select("year");
const fallbackYear = latestMovie?.year || new Date().getFullYear();
const year = parseOscarYear(fallbackYear) || new Date().getFullYear();
await Setting.findOneAndUpdate(
{ key: ACTIVE_YEAR_KEY },
{ $set: { value: year } },
{ upsert: true, new: true }
);
return year;
}
// 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();
const year = parseOscarYear(req.query.year);
// 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 ? { year } : {});
const allMovies = await Movie.find({ year });
const totalMoviesCount = allMovies.length;
if (totalMoviesCount === 0) {
@@ -92,7 +117,7 @@ router.get("/stats", verifyToken, async (req, res) => {
return {
name: user.name,
year: year || null,
year,
watchedCount,
totalMoviesCount,
watchedRatio: `${watchedRatio}%`,