feat: Allow configuring Oscar date per year
Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
d732e1a013
commit
e326ead817
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user