All checks were successful
Docker / docker (push) Successful in 1m51s
Bouton ✕ sur les films notés 10/10 pour supprimer la note via DELETE /api/rate/:id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
/* ── 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
|
|
? `<img class="movie-poster" src="${movie.poster}" alt="" loading="lazy">`
|
|
: `<div class="poster-ph">🎬</div>`;
|
|
|
|
// Info
|
|
const meta = [movie.year, fmtDate(movie.watched_at)].filter(Boolean).join(' · ');
|
|
const badge = movie.current_rating === 10
|
|
? `<span class="badge-10">★ noté 10/10</span>` : '';
|
|
|
|
const info = `
|
|
<div class="movie-info">
|
|
<div class="movie-title">${esc(movie.title_fr)}</div>
|
|
<div class="movie-meta">${esc(meta)}</div>
|
|
${badge}
|
|
</div>`;
|
|
|
|
// Synopsis
|
|
const synopsis = movie.overview
|
|
? `<div class="movie-synopsis"><div class="synopsis-text">${esc(movie.overview)}</div></div>`
|
|
: `<div class="movie-synopsis"><span class="synopsis-empty">Aucun résumé disponible</span></div>`;
|
|
|
|
// Rating buttons + skip button
|
|
const btns = Array.from({ length: 10 }, (_, i) =>
|
|
`<button class="r-btn" data-n="${i + 1}">${i + 1}</button>`
|
|
).join('');
|
|
|
|
const skipLabel = isSkipped ? 'Remettre' : 'Passer';
|
|
const skipBtn = `<button class="skip-btn">${skipLabel}</button>`;
|
|
const removeBtn = movie.current_rating !== null
|
|
? `<button class="remove-btn" title="Supprimer la note">✕</button>` : '';
|
|
|
|
const ratingEl = `<div class="movie-rating">${btns}${removeBtn}${skipBtn}</div>`;
|
|
|
|
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();
|