feat: Allow configuring Oscar date per year

Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
Cursor Agent
2025-12-21 21:03:55 +00:00
co-authored by mathieumoran1
parent d732e1a013
commit e326ead817
7 changed files with 440 additions and 4 deletions
+1 -1
View File
@@ -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 <span id="oscar-year">—</span>) - <span id="time-left"></span></h3>
<h3 class="fs-6" style="margin-bottom: -1px;">Temps avant les Oscars (<span id="oscar-date-day-month">—</span> <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>
+32
View File
@@ -111,6 +111,38 @@
</div>
</div>
</div>
<div class="col-12 col-xl-8">
<div class="card p-3">
<h4 class="mb-1">Date des Oscars (par année)</h4>
<p class="text-muted mb-3">
Configure la date exacte de la cérémonie pour le <strong>compte à rebours</strong> sur Films &amp; Checklist.
Si non défini, la date par défaut reste <code>15 mars</code> (comportement actuel).
</p>
<div class="row g-3">
<div class="col-md-4">
<label for="oscar_date_year" class="form-label">Année</label>
<select class="form-select" id="oscar_date_year"></select>
<div class="form-text">Basé sur les années présentes dans les films.</div>
</div>
<div class="col-md-4">
<label for="oscar_date_value" class="form-label">Date</label>
<input type="date" class="form-control" id="oscar_date_value">
<div class="form-text">Format: YYYY-MM-DD.</div>
</div>
<div class="col-md-4 d-flex align-items-end justify-content-end gap-2">
<button type="button" id="clear-oscar-date" class="btn btn-outline-danger">Effacer</button>
<button type="button" id="save-oscar-date" class="btn btn-warning">Enregistrer</button>
</div>
</div>
<div class="mt-3">
<div class="text-muted small mb-1">Dates configurées</div>
<div id="oscar-date-current" class="border rounded p-2 bg-light">—</div>
</div>
</div>
</div>
</div>
</div>
+31 -1
View File
@@ -88,6 +88,23 @@ window.onload = async function () {
}
}
async function fetchOscarEffectiveDate(year) {
try {
const res = await fetch(`/api/settings/oscar-date?year=${encodeURIComponent(String(year))}`, { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch oscar date');
const data = await res.json();
const effectiveDate = typeof data?.effectiveDate === 'string' ? data.effectiveDate : null;
return effectiveDate && /^\d{4}-\d{2}-\d{2}$/.test(effectiveDate) ? effectiveDate : `${year}-03-15`;
} catch (_) {
return `${year}-03-15`;
}
}
function parseLocalNoonFromIsoDate(dateStr) {
const d = new Date(`${dateStr}T12:00:00`);
return Number.isNaN(d.getTime()) ? null : d;
}
const activeYear = await fetchActiveYear();
document.title = `Pool Oscars ${activeYear} - Checklist`;
const oscarYearEl = document.getElementById('oscar-year');
@@ -97,7 +114,20 @@ window.onload = async function () {
const completionModalContent = await fetchCompletionModalContent();
applyCompletionModalContent(completionModalContent);
const targetDate = new Date(`March 15, ${activeYear}`);
const oscarEffectiveDate = await fetchOscarEffectiveDate(activeYear);
const targetDate =
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
new Date(`March 15, ${activeYear} 12:00:00`);
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
if (oscarDayMonthEl) {
try {
oscarDayMonthEl.textContent = targetDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
} catch (_) {
oscarDayMonthEl.textContent = '15 mars';
}
}
const currentDate = new Date();
const timeDifference = targetDate - currentDate;
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
+207
View File
@@ -41,6 +41,13 @@ window.onload = async function () {
const activeYearInput = document.getElementById('active_oscar_year');
const saveActiveYearBtn = document.getElementById('save-active-year');
// Oscar date per year (settings tab)
const oscarDateYearSelect = document.getElementById('oscar_date_year');
const oscarDateValueInput = document.getElementById('oscar_date_value');
const saveOscarDateBtn = document.getElementById('save-oscar-date');
const clearOscarDateBtn = document.getElementById('clear-oscar-date');
const oscarDateCurrentEl = document.getElementById('oscar-date-current');
// Modal 100% elements
const completionModalTitleEl = document.getElementById('completion_modal_title');
const completionModalBodyTextEl = document.getElementById('completion_modal_body_text');
@@ -97,6 +104,8 @@ window.onload = async function () {
let winnerUsersLoadedOnce = false;
let winnersByYear = new Map(); // year -> [{ year, userId, name, points }]
let winnerUsers = []; // admin list: {id, name, email, ...}
let oscarDatesLoadedOnce = false;
let oscarDatesByYear = {}; // { "2026": "2026-03-15" }
const DEFAULT_COMPLETION_MODAL = Object.freeze({
title: 'Félicitations! very nice 🎉🎉🎉',
@@ -474,6 +483,81 @@ window.onload = async function () {
return n;
}
function isIsoDateString(value) {
return /^\d{4}-\d{2}-\d{2}$/.test(String(value || '').trim());
}
async function fetchOscarDates() {
const res = await fetch('/api/settings/oscar-dates', { method: 'GET' });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const msg = data.message || data.error || `Erreur (${res.status})`;
throw new Error(msg);
}
return data?.dates && typeof data.dates === 'object' ? data.dates : {};
}
async function ensureOscarDatesLoaded(options = {}) {
const force = !!options.force;
if (oscarDatesLoadedOnce && !force) return;
try {
oscarDatesByYear = await fetchOscarDates();
oscarDatesLoadedOnce = true;
} catch (_) {
oscarDatesByYear = {};
oscarDatesLoadedOnce = true;
}
}
async function setOscarDateForYear(year, dateStrOrEmpty) {
const res = await fetch(`/api/settings/oscar-date/${encodeURIComponent(String(year))}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ date: dateStrOrEmpty }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const msg = data.message || data.error || `Erreur (${res.status})`;
throw new Error(msg);
}
return data;
}
function renderOscarDatesCurrent(cleanedYears) {
if (!oscarDateCurrentEl) return;
const years = Array.isArray(cleanedYears) ? cleanedYears : [];
if (!years.length) {
oscarDateCurrentEl.textContent = '—';
return;
}
const lines = years.map((y) => {
const key = String(y);
const val = oscarDatesByYear && typeof oscarDatesByYear === 'object' ? oscarDatesByYear[key] : null;
const date = (typeof val === 'string' && isIsoDateString(val)) ? val : '— (défaut: 03-15)';
return `${key}: ${date}`;
});
oscarDateCurrentEl.textContent = lines.join('\n');
oscarDateCurrentEl.style.whiteSpace = 'pre';
}
function applyOscarDateSelection() {
if (!oscarDateYearSelect || !oscarDateValueInput) return;
const y = parseYear(oscarDateYearSelect.value);
if (!y) {
oscarDateValueInput.value = '';
return;
}
const key = String(y);
const val = oscarDatesByYear && typeof oscarDatesByYear === 'object' ? oscarDatesByYear[key] : null;
oscarDateValueInput.value = (typeof val === 'string' && isIsoDateString(val)) ? val : '';
}
async function fetchWinners() {
const res = await fetch('/api/settings/winners', { method: 'GET' });
const data = await res.json().catch(() => ({}));
@@ -803,6 +887,84 @@ window.onload = async function () {
}
}
// Oscar date (settings tab) wiring
if (oscarDateYearSelect) {
oscarDateYearSelect.addEventListener('change', () => {
applyOscarDateSelection();
});
}
if (saveOscarDateBtn && oscarDateYearSelect && oscarDateValueInput) {
saveOscarDateBtn.addEventListener('click', async () => {
const y = parseYear(oscarDateYearSelect.value);
if (!y) {
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
return;
}
const dateValue = String(oscarDateValueInput.value || '').trim();
if (dateValue && !isIsoDateString(dateValue)) {
showResponse('warning', 'Date invalide. Format attendu: YYYY-MM-DD');
return;
}
saveOscarDateBtn.disabled = true;
if (clearOscarDateBtn) clearOscarDateBtn.disabled = true;
const oldText = saveOscarDateBtn.textContent;
saveOscarDateBtn.textContent = 'Enregistrement...';
try {
const result = await setOscarDateForYear(y, dateValue);
if (result?.date) {
oscarDatesByYear[String(y)] = String(result.date);
showResponse('success', `Date Oscars enregistrée pour ${y}: ${result.date}`);
} else {
delete oscarDatesByYear[String(y)];
showResponse('success', `Date Oscars effacée pour ${y} (retour au défaut: 15 mars).`);
}
await refreshYears();
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
} finally {
saveOscarDateBtn.disabled = false;
if (clearOscarDateBtn) clearOscarDateBtn.disabled = false;
saveOscarDateBtn.textContent = oldText;
applyOscarDateSelection();
}
});
}
if (clearOscarDateBtn && oscarDateYearSelect && oscarDateValueInput) {
clearOscarDateBtn.addEventListener('click', async () => {
const y = parseYear(oscarDateYearSelect.value);
if (!y) {
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
return;
}
clearOscarDateBtn.disabled = true;
if (saveOscarDateBtn) saveOscarDateBtn.disabled = true;
const oldText = clearOscarDateBtn.textContent;
clearOscarDateBtn.textContent = 'Effacement...';
try {
oscarDateValueInput.value = '';
await setOscarDateForYear(y, '');
delete oscarDatesByYear[String(y)];
showResponse('success', `Date Oscars effacée pour ${y} (retour au défaut: 15 mars).`);
await refreshYears();
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
} finally {
clearOscarDateBtn.disabled = false;
if (saveOscarDateBtn) saveOscarDateBtn.disabled = false;
clearOscarDateBtn.textContent = oldText;
applyOscarDateSelection();
}
});
}
if (deleteUserVerifyInputEl && confirmDeleteUserBtn) {
deleteUserVerifyInputEl.addEventListener('input', () => {
const expected = String(currentDeleteTarget?.email || '').trim();
@@ -1402,6 +1564,9 @@ window.onload = async function () {
.filter((y) => Number.isInteger(y) && y >= 1900 && y <= 3000)
.sort((a, b) => b - a);
// Ensure Oscar dates cache is loaded (for settings UI).
await ensureOscarDatesLoaded();
// Rebuild manage dropdown from scratch (so removed years disappear)
const previousSelection = manageYearSelect.value;
manageYearSelect.innerHTML = '';
@@ -1471,6 +1636,48 @@ window.onload = async function () {
}
}
// Oscar date year dropdown (settings tab)
if (oscarDateYearSelect && oscarDateValueInput && saveOscarDateBtn && clearOscarDateBtn) {
const previousOscarYearSelection = String(oscarDateYearSelect.value || '');
oscarDateYearSelect.innerHTML = '';
if (cleanedYears.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Aucune année (ajoute un film)';
opt.disabled = true;
opt.selected = true;
oscarDateYearSelect.appendChild(opt);
oscarDateYearSelect.disabled = true;
oscarDateValueInput.disabled = true;
saveOscarDateBtn.disabled = true;
clearOscarDateBtn.disabled = true;
} else {
cleanedYears.forEach((y) => {
const opt = document.createElement('option');
opt.value = String(y);
opt.textContent = String(y);
oscarDateYearSelect.appendChild(opt);
});
const desired =
(previousOscarYearSelection && cleanedYears.map(String).includes(previousOscarYearSelection))
? previousOscarYearSelection
: (activeYear && cleanedYears.map(String).includes(String(activeYear)))
? String(activeYear)
: String(cleanedYears[0]);
oscarDateYearSelect.disabled = false;
oscarDateValueInput.disabled = false;
saveOscarDateBtn.disabled = false;
clearOscarDateBtn.disabled = false;
oscarDateYearSelect.value = desired;
}
}
applyOscarDateSelection();
renderOscarDatesCurrent(cleanedYears);
// Winners year dropdown (optional tab)
setWinnerYearOptions(cleanedYears);
applyWinnerFormFromCache();
+32 -1
View File
@@ -11,6 +11,24 @@ window.onload = async function () {
}
}
async function fetchOscarEffectiveDate(year) {
try {
const res = await fetch(`/api/settings/oscar-date?year=${encodeURIComponent(String(year))}`, { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch oscar date');
const data = await res.json();
const effectiveDate = typeof data?.effectiveDate === 'string' ? data.effectiveDate : null;
return effectiveDate && /^\d{4}-\d{2}-\d{2}$/.test(effectiveDate) ? effectiveDate : `${year}-03-15`;
} catch (_) {
return `${year}-03-15`;
}
}
function parseLocalNoonFromIsoDate(dateStr) {
// Use local noon to avoid DST/midnight edge cases for "days left" math.
const d = new Date(`${dateStr}T12:00:00`);
return Number.isNaN(d.getTime()) ? null : d;
}
async function fetchMoviesLastUpdated(year, token) {
const el = document.getElementById('movies-last-updated');
if (!el) return;
@@ -40,7 +58,20 @@ window.onload = async function () {
const oscarYearEl = document.getElementById('oscar-year');
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
const targetDate = new Date(`March 15, ${activeYear}`);
const oscarEffectiveDate = await fetchOscarEffectiveDate(activeYear);
const targetDate =
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
new Date(`March 15, ${activeYear} 12:00:00`);
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
if (oscarDayMonthEl) {
try {
oscarDayMonthEl.textContent = targetDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
} catch (_) {
oscarDayMonthEl.textContent = '15 mars';
}
}
const currentDate = new Date();
const timeDifference = targetDate - currentDate;
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
+1 -1
View File
@@ -106,7 +106,7 @@
<!-- This will display the ratio -->
</div>
<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">Temps avant les Oscars (<span id="oscar-date-day-month">—</span> <span id="oscar-year">—</span>) - <span id="time-left"></span></h3>
<h3 class="fs-6 mb-0">
</h3>