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 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
9ac9b1bd04
commit
e0ec4760ce
+70
-1
@@ -122,16 +122,85 @@
|
||||
<th>Titre</th>
|
||||
<th style="width: 20%;">Catégorie</th>
|
||||
<th style="width: 12%;">Année</th>
|
||||
<th style="width: 1%;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-movies-body">
|
||||
<tr><td colspan="4" class="text-muted">Charge la liste…</td></tr>
|
||||
<tr><td colspan="5" class="text-muted">Charge la liste…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit movie modal -->
|
||||
<div class="modal fade" id="editMovieModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editMovieModalTitle">Modifier le film</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit_movie_id">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="edit_imdb_id" class="form-label">IMDB ID (ex: tt1234567)</label>
|
||||
<input type="text" class="form-control" id="edit_imdb_id" placeholder="tt..." required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit_year" class="form-label">Oscar year</label>
|
||||
<input type="number" class="form-control" id="edit_year" placeholder="2026" min="1900" max="3000">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="edit_category" class="form-label">Catégorie</label>
|
||||
<input type="text" class="form-control" id="edit_category" placeholder="Best Picture, etc." required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit_vod_link" class="form-label">Lien VOD</label>
|
||||
<input type="url" class="form-control" id="edit_vod_link" placeholder="https://..." required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="edit_refresh_omdb">
|
||||
<label class="form-check-label" for="edit_refresh_omdb">
|
||||
Recharger depuis OMDb (écrase titre / plot / rating / poster)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Si coché, le serveur refait un appel OMDb via l’IMDB ID au moment de l’enregistrement.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="edit_title" class="form-label">Titre</label>
|
||||
<input type="text" class="form-control" id="edit_title" placeholder="Title">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit_rating" class="form-label">Rating (imdbRating)</label>
|
||||
<input type="text" class="form-control" id="edit_rating" placeholder="7.9">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="edit_poster" class="form-label">Poster URL</label>
|
||||
<input type="url" class="form-control" id="edit_poster" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="edit_description" class="form-label">Description (Plot)</label>
|
||||
<textarea class="form-control" id="edit_description" rows="4" placeholder="Plot..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="button" class="btn btn-primary" id="save-movie-changes">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="./js/app_control.js"></script>
|
||||
|
||||
|
||||
+172
-13
@@ -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 = `<tr><td colspan="4" class="text-muted">Chargement…</td></tr>`;
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="5" class="text-muted">Chargement…</td></tr>`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method: 'GET' });
|
||||
if (!res.ok) {
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="4" class="text-danger">Erreur lors du chargement (${res.status})</td></tr>`;
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="5" class="text-danger">Erreur lors du chargement (${res.status})</td></tr>`;
|
||||
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 = `<tr><td colspan="4" class="text-muted">Aucun film.</td></tr>`;
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="5" class="text-muted">Aucun film.</td></tr>`;
|
||||
updateSelectionUI();
|
||||
return;
|
||||
}
|
||||
@@ -188,6 +334,11 @@ window.onload = async function () {
|
||||
</td>
|
||||
<td>${m.category || ''}</td>
|
||||
<td>${m.year || ''}</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-edit-movie-id="${m._id}">
|
||||
Éditer
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
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 = `<tr><td colspan="4" class="text-danger">${err.message || 'Erreur réseau'}</td></tr>`;
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="5" class="text-danger">${err.message || 'Erreur réseau'}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +404,10 @@ window.onload = async function () {
|
||||
}
|
||||
});
|
||||
|
||||
if (saveMovieChangesBtn) {
|
||||
saveMovieChangesBtn.addEventListener('click', saveMovieChanges);
|
||||
}
|
||||
|
||||
// Initial load
|
||||
await refreshYears();
|
||||
await loadMoviesForManagement();
|
||||
|
||||
Reference in New Issue
Block a user