Introduce client-side localization helper and en/fr locale bundles. Route admin control, picks, movies, stats, winners and settings UIs through i18n keys instead of hardcoded text. Remove French text from the English UI and standardize shared messages (saving, network errors, confirmations). Update .gitignore to ignore editor metadata and localization helper artifacts.
654 lines
26 KiB
JavaScript
654 lines
26 KiB
JavaScript
// i18n helper function
|
||
function t(key, fallback = '', params = {}) {
|
||
if (window.i18n && typeof window.i18n.t === 'function') {
|
||
return window.i18n.t(key, params);
|
||
}
|
||
let result = fallback;
|
||
if (params && typeof params === 'object') {
|
||
Object.keys(params).forEach(k => {
|
||
result = result.replace(`{${k}}`, String(params[k]));
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function createPageLoader(options = {}) {
|
||
const title = String(options.title || t('common.loading', 'Loading…'));
|
||
// Subtitle kept for backwards-compat with callers, but we no longer render text in the UI.
|
||
const subtitle = String(options.subtitle || t('common.preparingPage', 'Preparing page…'));
|
||
|
||
let progress = 0;
|
||
let removed = false;
|
||
let navResizeObserver = null;
|
||
|
||
// Reuse a preloaded overlay if present (prevents initial "clear" flash).
|
||
const preloadedOverlay = document.getElementById('page-loader-overlay');
|
||
const overlay = preloadedOverlay || document.createElement('div');
|
||
if (!preloadedOverlay) {
|
||
overlay.className = 'page-loader-overlay';
|
||
overlay.setAttribute('role', 'status');
|
||
overlay.setAttribute('aria-live', 'polite');
|
||
overlay.setAttribute('aria-busy', 'true');
|
||
|
||
overlay.innerHTML = `
|
||
<div class="page-loader-card page-loader-card--baronly">
|
||
<span class="visually-hidden">${title}</span>
|
||
<div class="page-loader-progress" aria-hidden="true">
|
||
<div class="page-loader-bar" id="page-loader-bar"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Keep screen reader label up to date.
|
||
const label = overlay.querySelector('.visually-hidden');
|
||
if (label) label.textContent = title;
|
||
}
|
||
|
||
const barEl = () => overlay.querySelector('#page-loader-bar');
|
||
|
||
function clampPct(p) {
|
||
const n = Number(p);
|
||
if (!Number.isFinite(n)) return 0;
|
||
return Math.max(0, Math.min(100, n));
|
||
}
|
||
|
||
function setProgress(p) {
|
||
progress = clampPct(p);
|
||
const bar = barEl();
|
||
if (bar) bar.style.width = `${progress}%`;
|
||
}
|
||
|
||
function setSubtitle(_) {
|
||
// Intentionally no-op (we only show the bar now).
|
||
}
|
||
|
||
function getNavHeightPx() {
|
||
const nav = document.querySelector('nav.navbar');
|
||
if (!nav) return 0;
|
||
const rect = nav.getBoundingClientRect();
|
||
const h = Number(rect?.height) || 0;
|
||
return h > 0 ? Math.round(h) : 0;
|
||
}
|
||
|
||
function updateOverlayTopOffset() {
|
||
// Scope the offset to this overlay instance (no global CSS var).
|
||
overlay.style.setProperty('--page-loader-top', `${getNavHeightPx()}px`);
|
||
}
|
||
|
||
function ensureMounted() {
|
||
if (removed) return;
|
||
// IMPORTANT: even if the overlay is preloaded in HTML (already connected),
|
||
// we still need to compute the navbar offset. Otherwise it defaults to 0px
|
||
// and the blur covers the header.
|
||
updateOverlayTopOffset();
|
||
document.body.classList.add('page-loading');
|
||
|
||
if (!overlay.isConnected) document.body.appendChild(overlay);
|
||
|
||
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
||
if (!navResizeObserver) {
|
||
const nav = document.querySelector('nav.navbar');
|
||
if (nav && typeof ResizeObserver !== 'undefined') {
|
||
try {
|
||
navResizeObserver = new ResizeObserver(() => updateOverlayTopOffset());
|
||
navResizeObserver.observe(nav);
|
||
} catch (_) {
|
||
navResizeObserver = null;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideAndRemoveSoon() {
|
||
if (removed) return;
|
||
overlay.classList.add('page-loader-hide');
|
||
overlay.setAttribute('aria-busy', 'false');
|
||
document.body.classList.remove('page-loading');
|
||
window.setTimeout(() => {
|
||
removed = true;
|
||
if (navResizeObserver) {
|
||
try { navResizeObserver.disconnect(); } catch (_) {}
|
||
navResizeObserver = null;
|
||
}
|
||
try { overlay.remove(); } catch (_) {}
|
||
}, 220);
|
||
}
|
||
|
||
function done() {
|
||
setProgress(100);
|
||
hideAndRemoveSoon();
|
||
}
|
||
|
||
function fail(_) {
|
||
// Don't block the UI on errors: let the page render its own error message.
|
||
setProgress(Math.max(progress, 95));
|
||
window.setTimeout(() => hideAndRemoveSoon(), 200);
|
||
}
|
||
|
||
ensureMounted();
|
||
setProgress(8);
|
||
|
||
return { setProgress, setSubtitle, done, fail };
|
||
}
|
||
|
||
window.addEventListener('DOMContentLoaded', async function () {
|
||
const pageLoader = createPageLoader({
|
||
title: (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('checklist.loadingChecklist') : 'Loading checklist',
|
||
subtitle: (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('movies.fetchingData') : 'Fetching data…'
|
||
});
|
||
|
||
const token = localStorage.getItem('auth_token');
|
||
const movieTableBody = document.getElementById('movie-table-body');
|
||
const watchedRatioEl = document.getElementById('watched-ratio');
|
||
|
||
function showTableError(message) {
|
||
if (!movieTableBody) return;
|
||
movieTableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="3">
|
||
<div class="alert alert-danger my-2" role="alert">
|
||
${message || t('common.errorLoading', 'Error loading.')}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
const DEFAULT_COMPLETION_MODAL = Object.freeze({
|
||
title: (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('checklist.congratulations') : 'Congratulations! very nice 🎉🎉🎉',
|
||
bodyText: '',
|
||
videoSrc: 'video/reward.mp4',
|
||
bodyHtml: ''
|
||
});
|
||
|
||
function sanitizeHtml(raw) {
|
||
const html = String(raw || '');
|
||
const withoutScripts = html.replace(/<script\b[\s\S]*?<\/script>/gi, '');
|
||
const withoutOnAttrs = withoutScripts
|
||
.replace(/\son\w+="[^"]*"/gi, '')
|
||
.replace(/\son\w+='[^']*'/gi, '')
|
||
.replace(/\son\w+=\S+/gi, '');
|
||
return withoutOnAttrs.trim();
|
||
}
|
||
|
||
async function fetchCompletionModalContent() {
|
||
try {
|
||
const res = await fetch('/api/settings/completion-modal', { method: 'GET', cache: 'no-cache' });
|
||
if (!res.ok) throw new Error('Failed to fetch completion modal');
|
||
const data = await res.json();
|
||
return {
|
||
title: typeof data?.title === 'string' ? data.title : DEFAULT_COMPLETION_MODAL.title,
|
||
bodyText: typeof data?.bodyText === 'string' ? data.bodyText : DEFAULT_COMPLETION_MODAL.bodyText,
|
||
videoSrc: typeof data?.videoSrc === 'string' ? data.videoSrc : DEFAULT_COMPLETION_MODAL.videoSrc,
|
||
bodyHtml: typeof data?.bodyHtml === 'string' ? data.bodyHtml : DEFAULT_COMPLETION_MODAL.bodyHtml,
|
||
};
|
||
} catch (_) {
|
||
return DEFAULT_COMPLETION_MODAL;
|
||
}
|
||
}
|
||
|
||
function applyCompletionModalContent(content) {
|
||
const titleEl = document.getElementById('videoModalLabel');
|
||
const bodyTextEl = document.getElementById('completion-modal-text');
|
||
const bodyCustomEl = document.getElementById('completion-modal-custom');
|
||
const videoEl = document.getElementById('rewardVideo');
|
||
const videoSourceEl = document.getElementById('rewardVideoSource');
|
||
|
||
const title = String(content?.title || '').trim() || DEFAULT_COMPLETION_MODAL.title;
|
||
const bodyText = String(content?.bodyText || '');
|
||
const bodyHtml = sanitizeHtml(String(content?.bodyHtml || ''));
|
||
const videoSrc = String(content?.videoSrc || '').trim();
|
||
|
||
if (titleEl) titleEl.textContent = title;
|
||
|
||
if (bodyCustomEl) {
|
||
if (bodyHtml) {
|
||
bodyCustomEl.innerHTML = bodyHtml;
|
||
bodyCustomEl.classList.remove('d-none');
|
||
} else {
|
||
bodyCustomEl.innerHTML = '';
|
||
bodyCustomEl.classList.add('d-none');
|
||
}
|
||
}
|
||
|
||
if (bodyTextEl) {
|
||
bodyTextEl.textContent = bodyText || '';
|
||
bodyTextEl.classList.toggle('d-none', !bodyText);
|
||
}
|
||
|
||
if (videoEl && videoSourceEl) {
|
||
if (videoSrc) {
|
||
videoSourceEl.setAttribute('src', videoSrc);
|
||
// reload source (so updated src is used)
|
||
try { videoEl.load(); } catch (_) {}
|
||
videoEl.classList.remove('d-none');
|
||
} else {
|
||
try { videoEl.pause(); } catch (_) {}
|
||
videoEl.classList.add('d-none');
|
||
}
|
||
}
|
||
}
|
||
|
||
async function fetchActiveYear() {
|
||
try {
|
||
const res = await fetch('/api/settings/year', { method: 'GET', cache: 'no-cache' });
|
||
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();
|
||
}
|
||
}
|
||
|
||
async function fetchOscarEffectiveDate(year) {
|
||
try {
|
||
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');
|
||
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;
|
||
}
|
||
|
||
async function fetchMoviesSummary(year) {
|
||
const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, {
|
||
method: 'GET',
|
||
headers: { 'Authorization': `Bearer ${token}` },
|
||
cache: 'no-cache',
|
||
});
|
||
if (!res.ok) throw new Error('Failed to fetch movies summary');
|
||
return await res.json();
|
||
}
|
||
|
||
const activeYear = await fetchActiveYear();
|
||
document.title = `Pool Oscars (${activeYear}) - Checklist`;
|
||
const oscarYearEl = document.getElementById('oscar-year');
|
||
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
|
||
|
||
// Render countdown immediately with a deterministic fallback (then refine from settings).
|
||
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
|
||
const timeLeftEl = document.getElementById('time-left');
|
||
const fallbackDate = parseLocalNoonFromIsoDate(`${activeYear}-03-15`) || new Date(`March 15, ${activeYear} 12:00:00`);
|
||
if (oscarDayMonthEl) {
|
||
try {
|
||
const locale = (window.i18n && typeof window.i18n.getLanguage === 'function') ? window.i18n.getLanguage() : (document.documentElement.lang || 'en');
|
||
oscarDayMonthEl.textContent = fallbackDate.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
|
||
} catch (_) {
|
||
oscarDayMonthEl.textContent = (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('checklist.march15') : 'March 15';
|
||
}
|
||
}
|
||
|
||
const currentDate = new Date();
|
||
let timeDifference = fallbackDate - currentDate;
|
||
let daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||
if (timeLeftEl) {
|
||
const daysLabel = (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('movies.daysLeft', { days: daysLeft }) : `${daysLeft} days`;
|
||
timeLeftEl.textContent = daysLabel;
|
||
}
|
||
|
||
fetchOscarEffectiveDate(activeYear).then((oscarEffectiveDate) => {
|
||
const targetDate =
|
||
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
|
||
fallbackDate;
|
||
|
||
if (oscarDayMonthEl) {
|
||
try {
|
||
const locale = (window.i18n && typeof window.i18n.getLanguage === 'function') ? window.i18n.getLanguage() : (document.documentElement.lang || 'en');
|
||
oscarDayMonthEl.textContent = targetDate.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
|
||
} catch (_) {}
|
||
}
|
||
const now = new Date();
|
||
timeDifference = targetDate - now;
|
||
daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||
if (timeLeftEl) {
|
||
const daysLabel = (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('movies.daysLeft', { days: daysLeft }) : `${daysLeft} days`;
|
||
timeLeftEl.textContent = daysLabel;
|
||
}
|
||
}).catch(() => {});
|
||
|
||
if (token) {
|
||
try {
|
||
const decoded = JSON.parse(atob(token.split('.')[1]));
|
||
if (decoded.mustChangePassword) {
|
||
window.location.href = '/change-password.html';
|
||
return;
|
||
}
|
||
const userName = decoded.name;
|
||
document.getElementById('user-name').textContent = userName;
|
||
if (decoded.admin) {
|
||
const adminLink = document.getElementById('admin-control-link');
|
||
if (adminLink) adminLink.classList.remove('d-none');
|
||
}
|
||
const currentTime = Math.floor(Date.now() / 1000);
|
||
if (decoded.exp && decoded.exp < currentTime) {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
}
|
||
} catch (error) {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
}
|
||
} else {
|
||
window.location.href = '/';
|
||
}
|
||
|
||
if (watchedRatioEl) watchedRatioEl.textContent = '—';
|
||
const progressBarEl = document.getElementById('progress-bar');
|
||
pageLoader.setProgress(18);
|
||
|
||
let movies = [];
|
||
let watchedMovies = [];
|
||
let watchedMoviesInYear = [];
|
||
let movieImdbIds = new Set();
|
||
try {
|
||
// Start summary fetch immediately so the header can update without waiting for the full list.
|
||
const summaryPromise = fetchMoviesSummary(activeYear)
|
||
.then((summary) => {
|
||
const totalCount = Number(summary?.totalMoviesCount) || 0;
|
||
const watchedCount = Number(summary?.watchedMoviesCount) || 0;
|
||
const ratioPct = totalCount > 0 ? ((watchedCount / totalCount) * 100).toFixed(1) : '0.0';
|
||
if (watchedRatioEl) {
|
||
const txt = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('movies.watchedRatio', { watched: watchedCount, total: totalCount, percent: ratioPct })
|
||
: `Watched: ${watchedCount} / ${totalCount} (${ratioPct}%)`;
|
||
watchedRatioEl.innerText = txt;
|
||
}
|
||
updateProgressBar(watchedCount, totalCount, false);
|
||
pageLoader.setProgress(32);
|
||
return summary;
|
||
})
|
||
.catch(() => null);
|
||
|
||
// Fetch remaining data in parallel.
|
||
pageLoader.setSubtitle(t('checklist.loadingMovies', 'Loading movies…'));
|
||
pageLoader.setProgress(26);
|
||
const [completionModalContent, moviesRes] = await Promise.all([
|
||
fetchCompletionModalContent(),
|
||
fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, {
|
||
method: 'GET',
|
||
headers: { 'Authorization': `Bearer ${token}` },
|
||
cache: 'no-cache',
|
||
})
|
||
]);
|
||
|
||
applyCompletionModalContent(completionModalContent);
|
||
pageLoader.setProgress(55);
|
||
|
||
if (!moviesRes.ok) {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
return;
|
||
}
|
||
|
||
movies = await moviesRes.json();
|
||
pageLoader.setProgress(62);
|
||
|
||
// Sort movies alphabetically by title
|
||
movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' }));
|
||
|
||
const summary = await summaryPromise;
|
||
watchedMovies = Array.isArray(summary?.watchedMovies) ? summary.watchedMovies : [];
|
||
|
||
if (progressBarEl) progressBarEl.removeAttribute('aria-busy');
|
||
} catch (e) {
|
||
showTableError((window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('checklist.unableToLoad') : 'Unable to load checklist. Please try again later.');
|
||
pageLoader.fail((window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('checklist.unableToLoadNetwork') : 'Unable to load checklist. Check your connection and try again.');
|
||
return;
|
||
}
|
||
|
||
movieImdbIds = new Set(movies.map((m) => m.imdb_id).filter(Boolean));
|
||
watchedMoviesInYear = watchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
|
||
|
||
const totalMoviesCount = movies.length;
|
||
const watchedMoviesCount = watchedMoviesInYear.length;
|
||
const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||
if (document.getElementById('watched-ratio')) {
|
||
const txt = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('movies.watchedRatio', { watched: watchedMoviesCount, total: totalMoviesCount, percent: ratioPct })
|
||
: `Watched: ${watchedMoviesCount} / ${totalMoviesCount} (${ratioPct}%)`;
|
||
document.getElementById('watched-ratio').innerText = txt;
|
||
}
|
||
|
||
function launchConfetti() {
|
||
const duration = 3 * 1000; // 3 seconds
|
||
const animationEnd = Date.now() + duration;
|
||
const colors = ['#bb0000', '#ffffff', '#FFD700'];
|
||
|
||
function frame() {
|
||
const timeLeft = animationEnd - Date.now();
|
||
if (timeLeft <= 0) return;
|
||
confetti({
|
||
particleCount: 5,
|
||
angle: 60,
|
||
spread: 55,
|
||
origin: { x: 0 }
|
||
});
|
||
confetti({
|
||
particleCount: 5,
|
||
angle: 120,
|
||
spread: 55,
|
||
origin: { x: 1 }
|
||
});
|
||
requestAnimationFrame(frame);
|
||
}
|
||
frame();
|
||
}
|
||
|
||
function updateProgressBar(watchedCount, totalCount, celebrate = true) {
|
||
const progressBar = document.getElementById('progress-bar');
|
||
if (!progressBar) return;
|
||
const safeTotal = Number(totalCount);
|
||
const safeWatched = Number(watchedCount);
|
||
const percentage = safeTotal > 0 && Number.isFinite(safeWatched) ? (safeWatched / safeTotal) * 100 : 0;
|
||
progressBar.style.width = `${percentage}%`;
|
||
|
||
if (percentage <= 50) {
|
||
progressBar.style.backgroundColor = `rgb(${255}, ${Math.floor((percentage / 50) * 255)}, 0)`;
|
||
} else {
|
||
progressBar.style.backgroundColor = `rgb(${Math.floor(255 - ((percentage - 50) / 50) * 255)}, 255, 0)`;
|
||
}
|
||
|
||
progressBar.setAttribute('aria-valuenow', Math.round(percentage));
|
||
|
||
if (celebrate) {
|
||
const videoModal = new bootstrap.Modal(document.getElementById("videoModal"));
|
||
if (percentage === 100) {
|
||
videoModal.show();
|
||
launchConfetti();
|
||
const rewardVideo = document.getElementById("rewardVideo");
|
||
// Only attempt autoplay if we actually have a visible <video>.
|
||
if (rewardVideo && !rewardVideo.classList.contains('d-none')) {
|
||
rewardVideo.play().catch(() => {});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
updateProgressBar(watchedMoviesCount, totalMoviesCount);
|
||
|
||
if (movieTableBody) movieTableBody.innerHTML = '';
|
||
movies.forEach(movie => {
|
||
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 = `
|
||
<td>
|
||
<input type="checkbox" class="form-check-input" id="movie-${movie.imdb_id}" ${watchedMovie ? 'checked' : ''} />
|
||
</td>
|
||
<td>${movie.title}</td>
|
||
<td id="watched-date-${movie.imdb_id}">${watchedDate}</td>
|
||
`;
|
||
|
||
// Use Bootstrap table state classes (matches Stats page styling better than custom backgrounds)
|
||
movieRow.classList.toggle('table-success', Boolean(watchedMovie));
|
||
movieRow.classList.toggle('table-danger', !watchedMovie);
|
||
|
||
movieTableBody.appendChild(movieRow);
|
||
|
||
movieRow.addEventListener('click', (event) => {
|
||
const checkbox = document.getElementById(`movie-${movie.imdb_id}`);
|
||
if (event.target.tagName !== 'INPUT') {
|
||
checkbox.checked = !checkbox.checked;
|
||
checkbox.dispatchEvent(new Event('change'));
|
||
}
|
||
});
|
||
|
||
const checkbox = document.getElementById(`movie-${movie.imdb_id}`);
|
||
checkbox.addEventListener('change', async (event) => {
|
||
const isChecked = event.target.checked;
|
||
const watchedDateCell = document.getElementById(`watched-date-${movie.imdb_id}`);
|
||
const row = event.target.closest('tr'); // Get the parent row
|
||
|
||
// If unchecking, show confirmation modal first
|
||
if (!isChecked) {
|
||
// Revert the checkbox temporarily until confirmed
|
||
event.target.checked = true;
|
||
|
||
// Show confirmation modal
|
||
const uncheckModal = new bootstrap.Modal(document.getElementById('uncheckConfirmModal'));
|
||
const movieTitleEl = document.getElementById('uncheckMovieTitle');
|
||
const confirmBtn = document.getElementById('confirmUncheckBtn');
|
||
|
||
if (movieTitleEl) movieTitleEl.textContent = movie.title;
|
||
|
||
// Remove any previous event listener to avoid duplicates
|
||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||
|
||
newConfirmBtn.addEventListener('click', async () => {
|
||
uncheckModal.hide();
|
||
|
||
// Now actually uncheck
|
||
event.target.checked = false;
|
||
watchedDateCell.textContent = '';
|
||
if (row) {
|
||
row.classList.add('table-danger');
|
||
row.classList.remove('table-success');
|
||
}
|
||
|
||
await fetch('/api/movies/users/updateWatchedMovies', {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ imdb_id: movie.imdb_id, isChecked: false })
|
||
});
|
||
|
||
const updatedWatchedMoviesRes = await fetch('/api/movies/watchedMovies', {
|
||
method: 'GET',
|
||
headers: { 'Authorization': `Bearer ${token}` },
|
||
cache: 'no-cache',
|
||
});
|
||
|
||
if (updatedWatchedMoviesRes.ok) {
|
||
const updatedWatchedMovies = await updatedWatchedMoviesRes.json();
|
||
watchedMoviesInYear = updatedWatchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
|
||
const updatedWatchedCount = watchedMoviesInYear.length;
|
||
const updatedPct = totalMoviesCount > 0 ? ((updatedWatchedCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||
if (document.getElementById('watched-ratio')) {
|
||
const updatedTxt = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('movies.watchedRatio', { watched: updatedWatchedCount, total: totalMoviesCount, percent: updatedPct })
|
||
: `Watched: ${updatedWatchedCount} / ${totalMoviesCount} (${updatedPct}%)`;
|
||
document.getElementById('watched-ratio').innerText = updatedTxt;
|
||
}
|
||
updateProgressBar(updatedWatchedCount, totalMoviesCount);
|
||
|
||
const moviesLeft = totalMoviesCount - updatedWatchedCount;
|
||
const moviesPerDay = daysLeft > 0 ? (moviesLeft / daysLeft).toFixed(2) : moviesLeft;
|
||
if (document.getElementById('movies-per-day')) {
|
||
const perDayText = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('checklist.moviesToWatchPerDay', { count: moviesPerDay })
|
||
: `Movies to watch per day: ${moviesPerDay}`;
|
||
document.getElementById('movies-per-day').textContent = perDayText;
|
||
}
|
||
}
|
||
});
|
||
|
||
uncheckModal.show();
|
||
return;
|
||
}
|
||
|
||
// Checking a movie - no confirmation needed
|
||
watchedDateCell.textContent = new Date().toLocaleString();
|
||
if (row) {
|
||
row.classList.add('table-success');
|
||
row.classList.remove('table-danger');
|
||
}
|
||
|
||
await fetch('/api/movies/users/updateWatchedMovies', {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ imdb_id: movie.imdb_id, isChecked })
|
||
});
|
||
|
||
const updatedWatchedMoviesRes = await fetch('/api/movies/watchedMovies', {
|
||
method: 'GET',
|
||
headers: { 'Authorization': `Bearer ${token}` },
|
||
cache: 'no-cache',
|
||
});
|
||
|
||
if (updatedWatchedMoviesRes.ok) {
|
||
const updatedWatchedMovies = await updatedWatchedMoviesRes.json();
|
||
watchedMoviesInYear = updatedWatchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
|
||
const updatedWatchedCount = watchedMoviesInYear.length;
|
||
const updatedPct = totalMoviesCount > 0 ? ((updatedWatchedCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||
if (document.getElementById('watched-ratio')) {
|
||
const updatedTxt = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('movies.watchedRatio', { watched: updatedWatchedCount, total: totalMoviesCount, percent: updatedPct })
|
||
: `Watched: ${updatedWatchedCount} / ${totalMoviesCount} (${updatedPct}%)`;
|
||
document.getElementById('watched-ratio').innerText = updatedTxt;
|
||
}
|
||
updateProgressBar(updatedWatchedCount, totalMoviesCount);
|
||
|
||
// Recalculate and update movies per day
|
||
const moviesLeft = totalMoviesCount - updatedWatchedCount;
|
||
const moviesPerDay = daysLeft > 0 ? (moviesLeft / daysLeft).toFixed(2) : moviesLeft;
|
||
if (document.getElementById('movies-per-day')) {
|
||
const perDayText = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('checklist.moviesToWatchPerDay', { count: moviesPerDay })
|
||
: `Movies to watch per day: ${moviesPerDay}`;
|
||
document.getElementById('movies-per-day').textContent = perDayText;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
const moviesLeft = totalMoviesCount - watchedMoviesCount;
|
||
const moviesPerDay = daysLeft > 0 ? (moviesLeft / daysLeft).toFixed(2) : moviesLeft;
|
||
if (document.getElementById('movies-per-day')) {
|
||
const perDayText = (window.i18n && typeof window.i18n.t === 'function')
|
||
? window.i18n.t('checklist.moviesToWatchPerDay', { count: moviesPerDay })
|
||
: `Movies to watch per day: ${moviesPerDay}`;
|
||
document.getElementById('movies-per-day').textContent = perDayText;
|
||
}
|
||
|
||
pageLoader.setSubtitle('Finalisation de l’affichage…');
|
||
pageLoader.setProgress(92);
|
||
pageLoader.done();
|
||
|
||
document.getElementById('log-off').addEventListener('click', () => {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
});
|
||
|
||
// Stop the video when the modal is closed
|
||
const videoModal = document.getElementById("videoModal");
|
||
const rewardVideo = document.getElementById("rewardVideo");
|
||
videoModal.addEventListener("hidden.bs.modal", function () {
|
||
if (!rewardVideo) return;
|
||
rewardVideo.pause();
|
||
rewardVideo.currentTime = 0;
|
||
});
|
||
}); |