diff --git a/public/css/app.css b/public/css/app.css index ed307b1..c38d719 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -234,27 +234,91 @@ footer { color: rgba(0,0,0,0.55); } + .movie-rating-avg { + margin-bottom: 5px; + } + + .movie-rating-sub--empty { + display: none; + } + .movie-rating-block .imdb-icon { width: 18px; height: 18px; margin-left: 4px; } + .movie-imdb-wrap { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0; + } + + .movie-rating-stars { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 2px; + } + + .movie-star { + border: 0; + background: transparent; + color: rgba(0,0,0,0.25); + font-size: 0.9rem; + line-height: 1; + padding: 0 1px; + } + + .movie-star.is-active { + color: #f4b400; + } + .movie-info-box { - margin-top: 10px; + margin-top: 6px; margin-bottom: 10px; - padding: 8px 10px; + padding: 2px 10px 8px; border-radius: 10px; background: rgba(0, 0, 0, 0.03); border: 1px solid rgba(0, 0, 0, 0.06); } + .movie-info-categories { + margin: 0 0 2px 0; + } + + .movie-info-summary { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + list-style: none; + color: rgba(0, 0, 0, 0.65); + font-size: 0.78rem; + font-weight: 600; + } + + .movie-info-summary::-webkit-details-marker { + display: none; + } + + .movie-info-summary-arrow { + display: inline-block; + transition: transform 120ms ease; + } + + .movie-info-categories[open] .movie-info-summary-arrow { + transform: rotate(180deg); + } + .movie-info-category { font-size: 0.78rem; color: rgba(0, 0, 0, 0.65); font-weight: 600; - font-style: italic; margin-bottom: 6px; + margin-top: 6px; } .movie-info-desc { diff --git a/public/js/app_movies.js b/public/js/app_movies.js index 7105f31..ac53294 100644 --- a/public/js/app_movies.js +++ b/public/js/app_movies.js @@ -195,6 +195,83 @@ window.addEventListener('DOMContentLoaded', async function () { `; } + function normalizeRatingValue(v) { + const n = Number(v); + if (!Number.isFinite(n)) return null; + const rounded = Math.round(n); + if (rounded < 1 || rounded > 5) return null; + return rounded; + } + + async function saveUserRating(movieId, rating, token) { + try { + const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/ratings`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ rating }), + }); + if (!res.ok) return null; + return await res.json().catch(() => null); + } catch (_) { + return null; + } + } + + async function deleteUserRating(movieId, token) { + try { + const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/ratings`, { + method: 'DELETE', + headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, + }); + if (!res.ok) return null; + return await res.json().catch(() => null); + } catch (_) { + return null; + } + } + + function updateStarUi(wrap, rating) { + if (!wrap) return; + const value = Number(rating); + const user = Number.isFinite(value) ? value : null; + if (wrap?.dataset) { + wrap.dataset.currentRating = user ? String(user) : ''; + } + wrap.querySelectorAll('.movie-star').forEach((btn) => { + const starValue = Number(btn.getAttribute('data-value')); + const isActive = user && starValue <= user; + btn.classList.toggle('is-active', !!isActive); + btn.setAttribute('aria-pressed', isActive ? 'true' : 'false'); + }); + } + + function updateRatingTexts(cardEl, avgRating, ratingsCount, userRating) { + if (!cardEl) return; + const avgEl = cardEl.querySelector('[data-role="avg-rating"]'); + const userEl = cardEl.querySelector('[data-role="user-rating"]'); + const avg = Number.isFinite(Number(avgRating)) ? Number(avgRating) : null; + const count = Number.isFinite(Number(ratingsCount)) ? Number(ratingsCount) : 0; + const user = Number.isFinite(Number(userRating)) ? Number(userRating) : null; + + if (avgEl) { + if (avg && count > 0) { + avgEl.textContent = `Note utilisateurs ${avg.toFixed(1)}${count ? ` (${count})` : ''}`; + avgEl.classList.remove('movie-rating-sub--empty'); + } else { + avgEl.textContent = ''; + avgEl.classList.add('movie-rating-sub--empty'); + } + } + + if (userEl) { + userEl.textContent = ''; + userEl.classList.add('movie-rating-sub--empty'); + } + } + async function fetchActiveYear() { try { const res = await fetch('/api/settings/year', { method: 'GET', cache: 'no-cache' }); @@ -412,6 +489,11 @@ window.addEventListener('DOMContentLoaded', async function () { : ''; const userRatingOwn = Number.isFinite(userRating) && userRating > 0 ? `${userRating}/5` : ''; + const starsHtml = Array.from({ length: 5 }, (_, idx) => { + const value = idx + 1; + const isActive = Number.isFinite(userRating) && userRating >= value; + return ``; + }).join(''); // Navigation: // - Prefer in-app player when we have a non-legacy source @@ -490,20 +572,30 @@ window.addEventListener('DOMContentLoaded', async function () { }
-
-
+
+
+ ${movie.description}