/* ── State ──────────────────────────────────────────────── */ let currentPage = 1; let currentFilter = 'all'; let currentSort = 'watched_at'; let totalPages = 1; let totalCount = 0; let showSkipped = false; /* Rating value → color (index 0 unused) */ const COLORS = [ null, '#f43f5e', '#f43f5e', // 1-2 '#fb7185', // 3 '#fb923c', // 4 '#f59e0b', // 5 '#eab308', // 6 '#84cc16', // 7 '#22c55e', // 8 '#10b981', // 9 '#06b6d4', // 10 ]; /* ── Skipped (localStorage) ─────────────────────────────── */ function getSkipped() { try { return new Set(JSON.parse(localStorage.getItem('skipped') || '[]')); } catch { return new Set(); } } function saveSkipped(set) { localStorage.setItem('skipped', JSON.stringify([...set])); document.getElementById('skipped-count').textContent = set.size; } function skipMovie(traktId) { const s = getSkipped(); s.add(traktId); saveSkipped(s); } function unskipMovie(traktId) { const s = getSkipped(); s.delete(traktId); saveSkipped(s); } function toggleSkipped() { showSkipped = !showSkipped; const btn = document.getElementById('skipped-toggle'); btn.style.color = showSkipped ? 'var(--accent)' : ''; btn.style.borderColor = showSkipped ? 'var(--accent)' : ''; currentPage = 1; loadMovies(); } /* ── Boot ───────────────────────────────────────────────── */ async function init() { const { authenticated } = await fetch('/api/auth/status').then(r => r.json()); if (!authenticated) { show('login-screen'); } else { show('app'); document.getElementById('skipped-count').textContent = getSkipped().size; document.getElementById('filter-select').addEventListener('change', e => { currentFilter = e.target.value; currentPage = 1; loadMovies(); }); document.getElementById('sort-select').addEventListener('change', e => { currentSort = e.target.value; currentPage = 1; loadMovies(); }); loadMovies(); } } function show(id) { document.getElementById(id).classList.remove('hidden'); } function hide(id) { document.getElementById(id).classList.add('hidden'); } /* ── Load page ──────────────────────────────────────────── */ async function loadMovies() { const list = document.getElementById('movie-list'); list.innerHTML = ''; hide('empty'); document.getElementById('pagination').innerHTML = ''; show('loading'); try { const skipped = getSkipped(); const params = new URLSearchParams({ page: currentPage, per_page: 20, sort: currentSort, filter_type: currentFilter, exclude: showSkipped ? '' : [...skipped].join(','), }); const data = await fetch(`/api/movies?${params}`).then(r => r.json()); hide('loading'); // In "skipped" view, filter client-side to only show skipped ones const movies = showSkipped ? data.movies.filter(m => skipped.has(m.trakt_id)) : data.movies; totalPages = data.total_pages; document.getElementById('count-badge').textContent = `${data.total} film${data.total !== 1 ? 's' : ''}`; if (!movies.length) { show('empty'); document.getElementById('empty').querySelector('p').textContent = showSkipped ? 'Aucun film passé.' : 'Tous les films sont notés !'; return; } movies.forEach(m => list.appendChild(buildRow(m))); renderPagination(); } catch (err) { hide('loading'); console.error(err); showToast('Erreur de chargement', true); } } /* ── Build movie row ────────────────────────────────────── */ function buildRow(movie) { const skipped = getSkipped(); const isSkipped = skipped.has(movie.trakt_id); const row = document.createElement('div'); row.className = 'movie-row' + (isSkipped ? ' is-skipped' : ''); row.dataset.id = movie.trakt_id; // Poster const poster = movie.poster ? `` : `
🎬
`; // Info const meta = [movie.year, fmtDate(movie.watched_at)].filter(Boolean).join(' · '); const badge = movie.current_rating === 10 ? `★ noté 10/10` : ''; const info = `
${esc(movie.title_fr)}
${esc(meta)}
${badge}
`; // Synopsis const synopsis = movie.overview ? `
${esc(movie.overview)}
` : `
Aucun résumé disponible
`; // Rating buttons + skip button const btns = Array.from({ length: 10 }, (_, i) => `` ).join(''); const skipLabel = isSkipped ? 'Remettre' : 'Passer'; const skipBtn = ``; const removeBtn = movie.current_rating !== null ? `` : ''; const ratingEl = `
${btns}${removeBtn}${skipBtn}
`; row.innerHTML = poster + info + synopsis + ratingEl; // Rating hover & click const container = row.querySelector('.movie-rating'); const buttons = [...container.querySelectorAll('.r-btn')]; buttons.forEach((btn, idx) => { btn.addEventListener('mouseenter', () => { buttons.forEach((b, i) => { if (i <= idx) { const c = COLORS[i + 1]; b.style.background = c + '28'; b.style.borderColor = c; b.style.color = c; } else { b.style.background = ''; b.style.borderColor = ''; b.style.color = ''; } }); }); btn.addEventListener('click', () => rateMovie(movie, parseInt(btn.dataset.n), row)); }); container.addEventListener('mouseleave', () => { buttons.forEach(b => { b.style.background = ''; b.style.borderColor = ''; b.style.color = ''; }); }); // Remove rating button const removeBtnEl = container.querySelector('.remove-btn'); if (removeBtnEl) { removeBtnEl.addEventListener('click', () => removeRating(movie, row)); } // Skip button container.querySelector('.skip-btn').addEventListener('click', () => { if (isSkipped) { unskipMovie(movie.trakt_id); showToast(`${movie.title_fr} — remis dans la liste`); } else { skipMovie(movie.trakt_id); showToast(`${movie.title_fr} — passé`); } animateOut(row); }); return row; } /* ── Remove rating ──────────────────────────────────────── */ async function removeRating(movie, row) { row.style.pointerEvents = 'none'; try { const r = await fetch(`/api/rate/${movie.trakt_id}`, { method: 'DELETE' }); if (!r.ok) throw new Error('Failed'); showToast(`${movie.title_fr} — note supprimée`); animateOut(row); } catch { row.style.pointerEvents = ''; showToast('Erreur lors de la suppression', true); } } /* ── Rate ───────────────────────────────────────────────── */ async function rateMovie(movie, rating, row) { row.style.pointerEvents = 'none'; try { const r = await fetch(`/api/rate/${movie.trakt_id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rating }), }); if (!r.ok) throw new Error('Failed'); showToast(`${movie.title_fr} — noté ${rating}/10`); animateOut(row); } catch { row.style.pointerEvents = ''; showToast('Erreur lors de la notation', true); } } /* ── Animate row out ────────────────────────────────────── */ function animateOut(row) { row.style.transition = 'opacity .28s ease, transform .28s ease'; row.style.opacity = '0'; row.style.transform = 'translateX(16px)'; setTimeout(() => { const h = row.offsetHeight; row.style.height = h + 'px'; row.style.overflow = 'hidden'; row.style.transition = 'height .22s ease, padding .22s ease, margin .22s ease, border-width .22s ease'; requestAnimationFrame(() => { row.style.height = '0'; row.style.paddingTop = '0'; row.style.paddingBottom = '0'; row.style.marginBottom = '0'; row.style.borderWidth = '0'; }); setTimeout(() => { row.remove(); if (!document.querySelectorAll('.movie-row').length) { if (currentPage > 1) currentPage--; loadMovies(); } }, 240); }, 280); } /* ── Pagination ─────────────────────────────────────────── */ function renderPagination() { if (totalPages <= 1) return; const el = document.getElementById('pagination'); const prev = mkBtn('← Précédent', currentPage === 1, () => { currentPage--; reload(); }); const info = document.createElement('span'); info.className = 'page-info'; info.textContent = `Page ${currentPage} / ${totalPages}`; const next = mkBtn('Suivant →', currentPage === totalPages, () => { currentPage++; reload(); }); el.append(prev, info, next); } function mkBtn(label, disabled, onClick) { const b = document.createElement('button'); b.className = 'page-btn'; b.textContent = label; b.disabled = disabled; b.addEventListener('click', onClick); return b; } function reload() { window.scrollTo({ top: 0, behavior: 'smooth' }); loadMovies(); } /* ── Toast ──────────────────────────────────────────────── */ let toastTimer; function showToast(msg, isError = false) { const t = document.getElementById('toast'); t.textContent = msg; t.style.borderColor = isError ? 'rgba(244,63,94,.35)' : ''; t.style.color = isError ? '#f87191' : ''; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove('show'), 2800); } /* ── Utils ──────────────────────────────────────────────── */ function fmtDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric', }); } function esc(str) { const d = document.createElement('div'); d.appendChild(document.createTextNode(str ?? '')); return d.innerHTML; } init();