feat: Implement dynamic Oscar year selection
This commit introduces the ability to select and manage the active Oscar year. This change affects page titles, API requests, and data filtering across the application, ensuring that all views and statistics are relevant to the chosen year. Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
dda483b757
commit
7ff02207f6
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<h2>Checklist</h2>
|
<h2>Checklist</h2>
|
||||||
<div id="watched-ratio" class="ratio"></div>
|
<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>
|
<h3 class="fs-6"><span id="movies-per-day"></span></h3>
|
||||||
<div class="progress my-3">
|
<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>
|
<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
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -52,6 +52,24 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-5" style="max-width: 720px;">
|
<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>
|
<h2>Admin - Ajouter un film</h2>
|
||||||
<p class="text-muted mb-4">
|
<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>.
|
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
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<!-- Footer Section -->
|
<!-- Footer Section -->
|
||||||
<footer>
|
<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>
|
<p id="footer-text"></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
window.onload = async function () {
|
window.onload = async function () {
|
||||||
const token = localStorage.getItem('auth_token');
|
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 currentDate = new Date();
|
||||||
const timeDifference = targetDate - currentDate;
|
const timeDifference = targetDate - currentDate;
|
||||||
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
@@ -29,7 +46,7 @@ window.onload = async function () {
|
|||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch('/api/movies', {
|
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
@@ -55,9 +72,13 @@ window.onload = async function () {
|
|||||||
watchedMovies = await watchedRes.json();
|
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 totalMoviesCount = movies.length;
|
||||||
const watchedMoviesCount = watchedMovies.length;
|
const watchedMoviesCount = watchedMoviesInYear.length;
|
||||||
document.getElementById('watched-ratio').innerText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1)}%)`;
|
const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||||||
|
document.getElementById('watched-ratio').innerText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${ratioPct}%)`;
|
||||||
|
|
||||||
function launchConfetti() {
|
function launchConfetti() {
|
||||||
const duration = 3 * 1000; // 3 seconds
|
const duration = 3 * 1000; // 3 seconds
|
||||||
@@ -111,7 +132,7 @@ window.onload = async function () {
|
|||||||
|
|
||||||
const movieTableBody = document.getElementById('movie-table-body');
|
const movieTableBody = document.getElementById('movie-table-body');
|
||||||
movies.forEach(movie => {
|
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 watchedDate = watchedMovie ? new Date(watchedMovie.watchedDate).toLocaleString() : '';
|
||||||
const movieRow = document.createElement('tr');
|
const movieRow = document.createElement('tr');
|
||||||
movieRow.innerHTML = `
|
movieRow.innerHTML = `
|
||||||
@@ -169,8 +190,10 @@ window.onload = async function () {
|
|||||||
|
|
||||||
if (updatedWatchedMoviesRes.ok) {
|
if (updatedWatchedMoviesRes.ok) {
|
||||||
const updatedWatchedMovies = await updatedWatchedMoviesRes.json();
|
const updatedWatchedMovies = await updatedWatchedMoviesRes.json();
|
||||||
const updatedWatchedCount = updatedWatchedMovies.length;
|
watchedMoviesInYear = updatedWatchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
|
||||||
document.getElementById('watched-ratio').innerText = `Vu: ${updatedWatchedCount} / ${totalMoviesCount} (${((updatedWatchedCount / totalMoviesCount) * 100).toFixed(1)}%)`;
|
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);
|
updateProgressBar(updatedWatchedCount, totalMoviesCount);
|
||||||
|
|
||||||
// Recalculate and update movies per day
|
// Recalculate and update movies per day
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ window.onload = async function () {
|
|||||||
const form = document.getElementById('add-movie-form');
|
const form = document.getElementById('add-movie-form');
|
||||||
const yearInput = document.getElementById('oscar_year');
|
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 manageYearSelect = document.getElementById('manage_year');
|
||||||
const adminMoviesBody = document.getElementById('admin-movies-body');
|
const adminMoviesBody = document.getElementById('admin-movies-body');
|
||||||
const refreshMoviesBtn = document.getElementById('refresh-movies');
|
const refreshMoviesBtn = document.getElementById('refresh-movies');
|
||||||
@@ -57,6 +60,7 @@ window.onload = async function () {
|
|||||||
const editDescriptionEl = document.getElementById('edit_description');
|
const editDescriptionEl = document.getElementById('edit_description');
|
||||||
|
|
||||||
let moviesById = new Map();
|
let moviesById = new Map();
|
||||||
|
let activeYear = null;
|
||||||
|
|
||||||
function showResponse(kind, message) {
|
function showResponse(kind, message) {
|
||||||
responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
|
responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
|
||||||
@@ -71,6 +75,40 @@ window.onload = async function () {
|
|||||||
return n;
|
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) {
|
function isValidImdbId(value) {
|
||||||
return /^tt\d{5,}$/.test(String(value || '').trim());
|
return /^tt\d{5,}$/.test(String(value || '').trim());
|
||||||
}
|
}
|
||||||
@@ -157,9 +195,19 @@ window.onload = async function () {
|
|||||||
const stillExists = years.map(String).includes(previousSelection);
|
const stillExists = years.map(String).includes(previousSelection);
|
||||||
manageYearSelect.value = stillExists ? previousSelection : '';
|
manageYearSelect.value = stillExists ? previousSelection : '';
|
||||||
|
|
||||||
// Default add-year to latest year if empty
|
// Prefer defaulting UI to active year (if selection is empty/invalid).
|
||||||
if (years.length > 0 && !yearInput.value) {
|
const hasSelection = !!manageYearSelect.value;
|
||||||
yearInput.value = String(years[0]);
|
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 (_) {
|
} catch (_) {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -409,6 +457,39 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initial load
|
// 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 refreshYears();
|
||||||
await loadMoviesForManagement();
|
await loadMoviesForManagement();
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-7
@@ -1,5 +1,22 @@
|
|||||||
window.onload = async function () {
|
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 currentDate = new Date();
|
||||||
const timeDifference = targetDate - currentDate;
|
const timeDifference = targetDate - currentDate;
|
||||||
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
@@ -29,7 +46,7 @@ window.onload = async function () {
|
|||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch('/api/movies', {
|
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
@@ -56,14 +73,18 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moviesList = document.getElementById('movies-list');
|
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 totalMoviesCount = movies.length;
|
||||||
const watchedMoviesCount = watchedMovies.length;
|
const watchedMoviesCount = watchedMoviesInYear.length;
|
||||||
const ratioText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1)}%)`;
|
const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||||||
|
const ratioText = `Vu: ${watchedMoviesCount} / ${totalMoviesCount} (${ratioPct}%)`;
|
||||||
document.getElementById('watched-ratio').innerText = ratioText;
|
document.getElementById('watched-ratio').innerText = ratioText;
|
||||||
|
|
||||||
movies.forEach(movie => {
|
movies.forEach(movie => {
|
||||||
const isChecked = watchedMovies.some(watchedMovie => watchedMovie.imdb_id === movie.imdb_id);
|
const isChecked = watchedMoviesInYear.some(watchedMovie => watchedMovie.imdb_id === movie.imdb_id);
|
||||||
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 watchedDate = watchedMovie ? new Date(watchedMovie.watchedDate).toLocaleString() : '';
|
||||||
|
|
||||||
const movieDiv = document.createElement('div');
|
const movieDiv = document.createElement('div');
|
||||||
@@ -108,7 +129,7 @@ window.onload = async function () {
|
|||||||
|
|
||||||
movieCards.forEach(card => {
|
movieCards.forEach(card => {
|
||||||
const imdbId = card.getAttribute('data-imdb-id');
|
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) {
|
if (showUnwatchedOnly && isWatched) {
|
||||||
card.style.display = 'none';
|
card.style.display = 'none';
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
window.onload = async function () {
|
window.onload = async function () {
|
||||||
const token = localStorage.getItem('auth_token');
|
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) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const decoded = JSON.parse(atob(token.split('.')[1])); // Manually decoding JWT
|
const decoded = JSON.parse(atob(token.split('.')[1])); // Manually decoding JWT
|
||||||
@@ -28,7 +40,15 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<!-- This will display the ratio -->
|
<!-- This will display the ratio -->
|
||||||
</div>
|
</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 class="fs-6 mb-0">
|
||||||
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.navbar-custom {
|
.navbar-custom {
|
||||||
|
|||||||
+28
-3
@@ -3,6 +3,7 @@ const bcrypt = require("bcryptjs");
|
|||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
const User = require("../models/User");
|
const User = require("../models/User");
|
||||||
const Movie = require("../models/Movie");
|
const Movie = require("../models/Movie");
|
||||||
|
const Setting = require("../models/Setting");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -11,6 +12,8 @@ const router = express.Router();
|
|||||||
|
|
||||||
const woof = process.env.DOG_NAMES ? process.env.DOG_NAMES.split(",") : [];
|
const woof = process.env.DOG_NAMES ? process.env.DOG_NAMES.split(",") : [];
|
||||||
|
|
||||||
|
const ACTIVE_YEAR_KEY = "active_oscar_year";
|
||||||
|
|
||||||
// Fetch movie details from OMDb API
|
// Fetch movie details from OMDb API
|
||||||
async function fetchMovieDetailsFromOmdb(imdb_id) {
|
async function fetchMovieDetailsFromOmdb(imdb_id) {
|
||||||
const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
|
const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
|
||||||
@@ -57,16 +60,38 @@ function parseOscarYear(raw) {
|
|||||||
return n;
|
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
|
// 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 {
|
||||||
// Fetch all users
|
// Fetch all users
|
||||||
const users = await User.find();
|
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)
|
// Fetch movies (optionally filtered by year)
|
||||||
const allMovies = await Movie.find(year ? { year } : {});
|
const allMovies = await Movie.find({ year });
|
||||||
const totalMoviesCount = allMovies.length;
|
const totalMoviesCount = allMovies.length;
|
||||||
|
|
||||||
if (totalMoviesCount === 0) {
|
if (totalMoviesCount === 0) {
|
||||||
@@ -92,7 +117,7 @@ router.get("/stats", verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
year: year || null,
|
year,
|
||||||
watchedCount,
|
watchedCount,
|
||||||
totalMoviesCount,
|
totalMoviesCount,
|
||||||
watchedRatio: `${watchedRatio}%`,
|
watchedRatio: `${watchedRatio}%`,
|
||||||
|
|||||||
Reference in New Issue
Block a user