Checklist styling update (#53)
* Refactor checklist styling and structure Co-authored-by: mathieumoran1 <[email protected]> * Refactor movie display and styling for better UI Co-authored-by: mathieumoran1 <[email protected]> * feat: Add page loader component and integrate into player Co-authored-by: mathieumoran1 <[email protected]> * Refactor: Adjust IMDB icon size and alignment Co-authored-by: mathieumoran1 <[email protected]> * Feat: Show admin status on player page Co-authored-by: mathieumoran1 <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]>
This commit was merged in pull request #53.
This commit is contained in:
co-authored by
Cursor Agent
parent
1a460ca06c
commit
421206d280
@@ -453,12 +453,9 @@ window.addEventListener('DOMContentLoaded', async function () {
|
||||
<td id="watched-date-${movie.imdb_id}">${watchedDate}</td>
|
||||
`;
|
||||
|
||||
// Set initial row color based on watched state
|
||||
if (watchedMovie) {
|
||||
movieRow.style.backgroundColor = 'rgba(144, 238, 144, 0.6)'; // Lighter light green with transparency
|
||||
} else {
|
||||
movieRow.style.backgroundColor = 'rgba(255, 99, 71, 0.6)'; // Lighter light red with transparency
|
||||
}
|
||||
// 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);
|
||||
|
||||
@@ -478,10 +475,16 @@ window.addEventListener('DOMContentLoaded', async function () {
|
||||
|
||||
if (isChecked) {
|
||||
watchedDateCell.textContent = new Date().toLocaleString();
|
||||
row.style.backgroundColor = 'rgba(144, 238, 144, 0.6)'; // Apply light green with transparency
|
||||
if (row) {
|
||||
row.classList.add('table-success');
|
||||
row.classList.remove('table-danger');
|
||||
}
|
||||
} else {
|
||||
watchedDateCell.textContent = '';
|
||||
row.style.backgroundColor = 'rgba(255, 99, 71, 0.6)'; // Apply light red with transparency
|
||||
if (row) {
|
||||
row.classList.add('table-danger');
|
||||
row.classList.remove('table-success');
|
||||
}
|
||||
}
|
||||
|
||||
await fetch('/api/movies/users/updateWatchedMovies', {
|
||||
|
||||
+22
-13
@@ -359,22 +359,31 @@ window.addEventListener('DOMContentLoaded', async function () {
|
||||
: (hasLegacy ? movie.vod_link : (movie && movie._id ? `/player.html?id=${encodeURIComponent(movie._id)}` : '#'));
|
||||
|
||||
const movieDiv = document.createElement('div');
|
||||
movieDiv.classList.add('col-md-4', 'mb-4', 'movie-card');
|
||||
movieDiv.classList.add('col-12', 'col-sm-6', 'col-lg-4', 'movie-card');
|
||||
movieDiv.setAttribute('data-imdb-id', movie.imdb_id);
|
||||
movieDiv.innerHTML = `
|
||||
<div class="card">
|
||||
${isChecked ? '<div class="watched-banner">VISIONNÉ</div>' : ''}
|
||||
<a href="${playerUrl}" target="_self" rel="noopener noreferrer">
|
||||
<img src="${movie.poster}" class="card-img-top" alt="${movie.title}" style="width: 75%; display: block; margin: 0 auto; padding: 10px;">
|
||||
<div class="card h-100 shadow-sm">
|
||||
${isChecked ? '<span class="badge bg-success position-absolute top-0 start-0 m-2 watched-badge">VISIONNÉ</span>' : ''}
|
||||
<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none">
|
||||
<div class="text-center pt-3 px-3">
|
||||
<img src="${movie.poster}" class="img-fluid rounded movie-poster" alt="${movie.title}">
|
||||
</div>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title d-flex justify-content-between align-items-center">
|
||||
<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none text-dark">${movie.title}</a>
|
||||
<span>⭐ ${movie.rating} <a href="https://www.imdb.com/title/${movie.imdb_id}/"><img src="/img/imdb.png"></a></span>
|
||||
</h5>
|
||||
<p class="fw-bold fst-italic card-text" style="font-size: 0.75rem;">${movie.category}</p>
|
||||
<p class="card-text">${movie.description}</p>
|
||||
${isChecked ? `<p class="card-text"><strong>Regardé le:</strong> ${watchedDate}</p>` : ''}
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-start gap-2">
|
||||
<h5 class="card-title mb-1 flex-grow-1">
|
||||
<a href="${playerUrl}" target="_self" rel="noopener noreferrer" class="text-decoration-none text-dark">${movie.title}</a>
|
||||
</h5>
|
||||
<div class="text-nowrap small text-muted">
|
||||
⭐ ${movie.rating}
|
||||
<a href="https://www.imdb.com/title/${movie.imdb_id}/" target="_blank" rel="noopener noreferrer" aria-label="Voir sur IMDb">
|
||||
<img src="/img/imdb.png" class="imdb-icon" alt="IMDb">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small fw-semibold fst-italic mb-2">${movie.category}</div>
|
||||
<p class="card-text small flex-grow-1 mb-2">${movie.description}</p>
|
||||
${isChecked ? `<div class="text-muted small"><span class="fw-semibold text-dark">Regardé le:</span> ${watchedDate}</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
moviesList.appendChild(movieDiv);
|
||||
|
||||
@@ -7,6 +7,110 @@ function decodeJwt(token) {
|
||||
return JSON.parse(atob(padded));
|
||||
}
|
||||
|
||||
function createPageLoader(options = {}) {
|
||||
const title = String(options.title || 'Chargement…');
|
||||
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 {
|
||||
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 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() {
|
||||
overlay.style.setProperty('--page-loader-top', `${getNavHeightPx()}px`);
|
||||
}
|
||||
|
||||
function ensureMounted() {
|
||||
if (removed) return;
|
||||
updateOverlayTopOffset();
|
||||
document.body.classList.add('page-loading');
|
||||
if (!overlay.isConnected) document.body.appendChild(overlay);
|
||||
|
||||
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() {
|
||||
setProgress(Math.max(progress, 95));
|
||||
window.setTimeout(() => hideAndRemoveSoon(), 200);
|
||||
}
|
||||
|
||||
ensureMounted();
|
||||
setProgress(8);
|
||||
|
||||
return { setProgress, done, fail };
|
||||
}
|
||||
|
||||
function safeJsonParse(v) {
|
||||
try { return JSON.parse(v); } catch (_) { return null; }
|
||||
}
|
||||
@@ -754,12 +858,15 @@ function resolveMovieSource(movie) {
|
||||
}
|
||||
|
||||
window.onload = async function () {
|
||||
const pageLoader = createPageLoader({ title: 'Chargement du lecteur' });
|
||||
|
||||
// Basic auth UX: require a valid local token (like the rest of the app).
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
pageLoader.setProgress(14);
|
||||
|
||||
// Load the active Oscar year in parallel with the movie fetch.
|
||||
const activeYearPromise = fetchActiveOscarYear();
|
||||
@@ -782,6 +889,8 @@ window.onload = async function () {
|
||||
if (decoded.admin) {
|
||||
const adminLink = document.getElementById('admin-control-link');
|
||||
if (adminLink) adminLink.classList.remove('d-none');
|
||||
const adminStatus = document.getElementById('player-admin-status');
|
||||
if (adminStatus) adminStatus.classList.remove('d-none');
|
||||
}
|
||||
|
||||
const logoffBtn = document.getElementById('log-off');
|
||||
@@ -796,14 +905,17 @@ window.onload = async function () {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
pageLoader.setProgress(26);
|
||||
|
||||
const id = getQueryParam('id');
|
||||
if (!id) {
|
||||
showAlert('Missing movie id.');
|
||||
pageLoader.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
pageLoader.setProgress(36);
|
||||
const res = await fetch(`/api/movies/${encodeURIComponent(id)}`, {
|
||||
method: 'GET',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : undefined,
|
||||
@@ -811,17 +923,20 @@ window.onload = async function () {
|
||||
const movie = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
showAlert(movie?.message || movie?.error || `Failed to load movie (${res.status}).`);
|
||||
pageLoader.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
const activeYear = await activeYearPromise;
|
||||
setHeader(movie, { activeYear });
|
||||
pageLoader.setProgress(58);
|
||||
|
||||
const resolved = resolveMovieSource(movie);
|
||||
if (!resolved?.src) {
|
||||
setSourceLabel('—');
|
||||
hideAllPlayers();
|
||||
showAlert('No playable source configured for this movie.');
|
||||
pageLoader.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -832,10 +947,13 @@ window.onload = async function () {
|
||||
setSourceLabel('Legacy link');
|
||||
hideAllPlayers();
|
||||
showAlertVariant('Legacy source: this link is not playable in the in-app player.', 'warning');
|
||||
pageLoader.done();
|
||||
return;
|
||||
}
|
||||
|
||||
pageLoader.setProgress(70);
|
||||
await playVodLink(resolved.src, { token });
|
||||
pageLoader.setProgress(82);
|
||||
|
||||
// Only the <video> player supports reliable progress tracking (mp4/hls).
|
||||
const videoEl = document.getElementById('video');
|
||||
@@ -851,8 +969,11 @@ window.onload = async function () {
|
||||
} else {
|
||||
setProgressUi({ state: 'info', text: '—', showText: true });
|
||||
}
|
||||
pageLoader.setProgress(92);
|
||||
pageLoader.done();
|
||||
} catch (err) {
|
||||
showAlert(err.message || 'Network error.');
|
||||
pageLoader.fail();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user