From e0ec4760ce7c0f38770fa5e92a9f2568d8661163 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 20 Dec 2025 22:24:38 +0000 Subject: [PATCH] feat: Add movie editing functionality and modal This commit introduces the ability to edit movie details through a modal interface. It includes backend API endpoints for updating movie information and frontend logic to display and manage the edit modal. The changes also refine the year selection and movie loading processes in the admin panel. Co-authored-by: mathieumoran1 --- public/control.html | 71 ++++++++++++++- public/js/app_control.js | 185 ++++++++++++++++++++++++++++++++++++--- routes/movies.js | 85 ++++++++++++++++++ 3 files changed, 327 insertions(+), 14 deletions(-) diff --git a/public/control.html b/public/control.html index 4bafe42..f3128d9 100644 --- a/public/control.html +++ b/public/control.html @@ -122,16 +122,85 @@ Titre Catégorie Année + Actions - Charge la liste… + Charge la liste… + + + diff --git a/public/js/app_control.js b/public/js/app_control.js index 7df46a2..f1bd0a8 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -40,6 +40,24 @@ window.onload = async function () { const selectAllBox = document.getElementById('select-all-movies'); const selectedCountEl = document.getElementById('selected-count'); + const editMovieModalEl = document.getElementById('editMovieModal'); + const editMovieModal = editMovieModalEl ? new bootstrap.Modal(editMovieModalEl) : null; + const editMovieModalTitleEl = document.getElementById('editMovieModalTitle'); + const saveMovieChangesBtn = document.getElementById('save-movie-changes'); + + const editMovieIdEl = document.getElementById('edit_movie_id'); + const editImdbIdEl = document.getElementById('edit_imdb_id'); + const editYearEl = document.getElementById('edit_year'); + const editCategoryEl = document.getElementById('edit_category'); + const editVodLinkEl = document.getElementById('edit_vod_link'); + const editRefreshOmdbEl = document.getElementById('edit_refresh_omdb'); + const editTitleEl = document.getElementById('edit_title'); + const editRatingEl = document.getElementById('edit_rating'); + const editPosterEl = document.getElementById('edit_poster'); + const editDescriptionEl = document.getElementById('edit_description'); + + let moviesById = new Map(); + function showResponse(kind, message) { responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning'); responseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger'); @@ -53,6 +71,10 @@ window.onload = async function () { return n; } + function isValidImdbId(value) { + return /^tt\d{5,}$/.test(String(value || '').trim()); + } + document.getElementById('reset-form').addEventListener('click', function () { form.reset(); responseEl.classList.add('d-none'); @@ -117,18 +139,24 @@ window.onload = async function () { const yearsRes = await fetch('/api/movies/years'); const years = yearsRes.ok ? await yearsRes.json() : []; - // Populate both the add form (default) and manage dropdown - const existingValues = new Set([...manageYearSelect.options].map((o) => o.value)); + // Rebuild manage dropdown from scratch (so removed years disappear) + const previousSelection = manageYearSelect.value; + manageYearSelect.innerHTML = ''; + const allOpt = document.createElement('option'); + allOpt.value = ''; + allOpt.textContent = 'Toutes'; + manageYearSelect.appendChild(allOpt); + years.forEach((y) => { - const value = String(y); - if (!existingValues.has(value)) { - const opt = document.createElement('option'); - opt.value = value; - opt.textContent = value; - manageYearSelect.appendChild(opt); - } + const opt = document.createElement('option'); + opt.value = String(y); + opt.textContent = String(y); + manageYearSelect.appendChild(opt); }); + const stillExists = years.map(String).includes(previousSelection); + manageYearSelect.value = stillExists ? previousSelection : ''; + // Default add-year to latest year if empty if (years.length > 0 && !yearInput.value) { yearInput.value = String(years[0]); @@ -155,22 +183,140 @@ window.onload = async function () { selectAllBox.checked = allBoxes.length > 0 && checked === allBoxes.length; } + function openEditModal(movieId) { + if (!editMovieModal) return; + const movie = moviesById.get(movieId); + if (!movie) return; + + editMovieIdEl.value = movie._id || ''; + editImdbIdEl.value = movie.imdb_id || ''; + editYearEl.value = movie.year ? String(movie.year) : ''; + editCategoryEl.value = movie.category || ''; + editVodLinkEl.value = movie.vod_link || ''; + editRefreshOmdbEl.checked = false; + + editTitleEl.value = movie.title || ''; + editRatingEl.value = movie.rating || ''; + editPosterEl.value = movie.poster || ''; + editDescriptionEl.value = movie.description || ''; + + if (editMovieModalTitleEl) { + const label = movie.title ? `Modifier: ${movie.title}` : 'Modifier le film'; + editMovieModalTitleEl.textContent = label; + } + + editMovieModal.show(); + } + + async function saveMovieChanges() { + const movieId = (editMovieIdEl.value || '').trim(); + if (!movieId) return; + + const imdb_id = (editImdbIdEl.value || '').trim(); + const yearRaw = (editYearEl.value || '').trim(); + const category = (editCategoryEl.value || '').trim(); + const vod_link = (editVodLinkEl.value || '').trim(); + const refreshOmdb = !!editRefreshOmdbEl.checked; + + const title = editTitleEl.value; + const rating = editRatingEl.value; + const poster = editPosterEl.value; + const description = editDescriptionEl.value; + + if (!isValidImdbId(imdb_id)) { + showResponse('warning', 'IMDB ID invalide. Exemple attendu: tt1234567'); + return; + } + + let year = null; + if (yearRaw !== '') { + year = parseYear(yearRaw); + if (!year) { + showResponse('warning', 'Année invalide. Exemple attendu: 2026'); + return; + } + } + + if (!category) { + showResponse('warning', 'Catégorie invalide.'); + return; + } + + if (!vod_link) { + showResponse('warning', 'Lien VOD invalide.'); + return; + } + + const body = { + imdb_id, + year: yearRaw === '' ? null : year, + category, + vod_link, + refreshOmdb + }; + + // Only send manual fields if we're not refreshing from OMDb + if (!refreshOmdb) { + body.title = title; + body.description = description; + body.rating = rating; + body.poster = poster; + } + + try { + saveMovieChangesBtn.disabled = true; + saveMovieChangesBtn.textContent = 'Enregistrement...'; + + const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(body) + }); + + if (!res.ok) { + let msg = `Erreur (${res.status})`; + try { + const data = await res.json(); + msg = data.message || data.error || msg; + } catch (_) {} + showResponse('danger', msg); + return; + } + + const updated = await res.json(); + moviesById.set(updated._id, updated); + showResponse('success', 'Film mis à jour avec succès.'); + editMovieModal.hide(); + await refreshYears(); + await loadMoviesForManagement(); + } catch (err) { + showResponse('danger', err.message || 'Erreur réseau'); + } finally { + saveMovieChangesBtn.disabled = false; + saveMovieChangesBtn.textContent = 'Enregistrer'; + } + } + async function loadMoviesForManagement() { const year = manageYearSelect.value; const url = year ? `/api/movies?year=${encodeURIComponent(year)}` : '/api/movies'; - adminMoviesBody.innerHTML = `Chargement…`; + adminMoviesBody.innerHTML = `Chargement…`; try { const res = await fetch(url, { method: 'GET' }); if (!res.ok) { - adminMoviesBody.innerHTML = `Erreur lors du chargement (${res.status})`; + adminMoviesBody.innerHTML = `Erreur lors du chargement (${res.status})`; return; } const movies = await res.json(); movies.sort((a, b) => (a.title || '').localeCompare((b.title || ''), 'fr', { sensitivity: 'base' })); + moviesById = new Map(movies.map((m) => [m._id, m])); if (!movies.length) { - adminMoviesBody.innerHTML = `Aucun film.`; + adminMoviesBody.innerHTML = `Aucun film.`; updateSelectionUI(); return; } @@ -188,6 +334,11 @@ window.onload = async function () { ${m.category || ''} ${m.year || ''} + + + `; adminMoviesBody.appendChild(tr); }); @@ -196,9 +347,13 @@ window.onload = async function () { cb.addEventListener('change', updateSelectionUI); }); + adminMoviesBody.querySelectorAll('button[data-edit-movie-id]').forEach((btn) => { + btn.addEventListener('click', () => openEditModal(btn.getAttribute('data-edit-movie-id'))); + }); + updateSelectionUI(); } catch (err) { - adminMoviesBody.innerHTML = `${err.message || 'Erreur réseau'}`; + adminMoviesBody.innerHTML = `${err.message || 'Erreur réseau'}`; } } @@ -249,6 +404,10 @@ window.onload = async function () { } }); + if (saveMovieChangesBtn) { + saveMovieChangesBtn.addEventListener('click', saveMovieChanges); + } + // Initial load await refreshYears(); await loadMoviesForManagement(); diff --git a/routes/movies.js b/routes/movies.js index 6642d5b..f941274 100644 --- a/routes/movies.js +++ b/routes/movies.js @@ -14,6 +14,10 @@ function parseOscarYear(raw) { return n; } +function isValidImdbId(value) { + return typeof value === "string" && /^tt\d{5,}$/.test(value.trim()); +} + // Fetch movie details from OMDb API async function fetchMovieDetailsFromOmdb(imdb_id) { const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key @@ -233,4 +237,85 @@ router.delete("/delete", async (req, res) => { } }); +// Admin: update a movie (edit fields, optionally refresh from OMDb) +router.put("/:id", async (req, res) => { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) return res.status(401).json({ message: "Authentication token is required" }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!decoded.admin) { + return res.status(403).json({ message: "You do not have admin privileges" }); + } + + const { id } = req.params; + const { + imdb_id, + title, + description, + rating, + poster, + year, + category, + vod_link, + refreshOmdb, + } = req.body || {}; + + if (imdb_id !== undefined && imdb_id !== null && !isValidImdbId(imdb_id)) { + return res.status(400).json({ message: "IMDB ID invalide. Exemple attendu: tt1234567" }); + } + + if (year !== undefined && year !== null && year !== "") { + const parsedYear = parseOscarYear(year); + if (!parsedYear) { + return res.status(400).json({ message: "Année invalide (ex: 2026)" }); + } + } + + if (typeof category === "string" && category.trim() === "") { + return res.status(400).json({ message: "Catégorie invalide." }); + } + + if (typeof vod_link === "string" && vod_link.trim() === "") { + return res.status(400).json({ message: "Lien VOD invalide." }); + } + + const movie = await Movie.findById(id); + if (!movie) return res.status(404).json({ message: "Movie not found" }); + + if (imdb_id !== undefined) movie.imdb_id = String(imdb_id).trim(); + if (category !== undefined) movie.category = String(category).trim(); + if (vod_link !== undefined) movie.vod_link = String(vod_link).trim(); + + if (year !== undefined && year !== null && year !== "") { + movie.year = parseOscarYear(year); + } else if (year === null || year === "") { + // allow clearing the year explicitly + movie.year = null; + } + + // If refreshOmdb is true, overwrite OMDb-derived fields from the imdb_id + if (refreshOmdb) { + const details = await fetchMovieDetailsFromOmdb(movie.imdb_id); + movie.title = details.title; + movie.description = details.description; + movie.rating = details.rating; + movie.poster = details.poster; + } else { + if (title !== undefined) movie.title = title; + if (description !== undefined) movie.description = description; + if (rating !== undefined) movie.rating = rating; + if (poster !== undefined) movie.poster = poster; + } + + await movie.save(); + return res.status(200).json(movie); + } catch (err) { + if (err instanceof jwt.JsonWebTokenError) { + return res.status(401).json({ message: "Invalid or expired token" }); + } + return res.status(500).json({ error: err.message }); + } +}); + module.exports = router;