feat: Add Oscar year to movies and admin management
Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
da9d746928
commit
eeb337fe08
@@ -60,6 +60,11 @@
|
||||
<div id="response" class="alert d-none" role="alert"></div>
|
||||
|
||||
<form id="add-movie-form" class="card p-3">
|
||||
<div class="mb-3">
|
||||
<label for="oscar_year" class="form-label">Oscar year</label>
|
||||
<input type="number" class="form-control" id="oscar_year" placeholder="2026" min="1900" max="3000" required>
|
||||
<div class="form-text">Permet de séparer les films et les stats par année.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="imdb_id" class="form-label">IMDB ID (ex: tt1234567)</label>
|
||||
<input type="text" class="form-control" id="imdb_id" placeholder="tt..." required>
|
||||
@@ -77,6 +82,54 @@
|
||||
<button type="button" id="reset-form" class="btn btn-outline-secondary">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h2>Admin - Gérer les films</h2>
|
||||
<p class="text-muted mb-3">
|
||||
Sélectionne une année, puis coche les films à supprimer.
|
||||
</p>
|
||||
|
||||
<div class="card p-3">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-6">
|
||||
<label for="manage_year" class="form-label">Oscar year</label>
|
||||
<select id="manage_year" class="form-select">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex gap-2">
|
||||
<button type="button" id="refresh-movies" class="btn btn-outline-primary w-100">Rafraîchir</button>
|
||||
<button type="button" id="delete-selected" class="btn btn-danger w-100" disabled>Supprimer la sélection</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="select-all-movies">
|
||||
<label class="form-check-label" for="select-all-movies">Tout sélectionner</label>
|
||||
</div>
|
||||
<small class="text-muted"><span id="selected-count">0</span> sélectionné(s)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mt-2">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1%;">Sel.</th>
|
||||
<th>Titre</th>
|
||||
<th style="width: 20%;">Catégorie</th>
|
||||
<th style="width: 12%;">Année</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-movies-body">
|
||||
<tr><td colspan="4" class="text-muted">Charge la liste…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
+165
-1
@@ -31,6 +31,14 @@ window.onload = async function () {
|
||||
|
||||
const responseEl = document.getElementById('response');
|
||||
const form = document.getElementById('add-movie-form');
|
||||
const yearInput = document.getElementById('oscar_year');
|
||||
|
||||
const manageYearSelect = document.getElementById('manage_year');
|
||||
const adminMoviesBody = document.getElementById('admin-movies-body');
|
||||
const refreshMoviesBtn = document.getElementById('refresh-movies');
|
||||
const deleteSelectedBtn = document.getElementById('delete-selected');
|
||||
const selectAllBox = document.getElementById('select-all-movies');
|
||||
const selectedCountEl = document.getElementById('selected-count');
|
||||
|
||||
function showResponse(kind, message) {
|
||||
responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
|
||||
@@ -38,6 +46,13 @@ window.onload = async function () {
|
||||
responseEl.textContent = message;
|
||||
}
|
||||
|
||||
function parseYear(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n)) return null;
|
||||
if (n < 1900 || n > 3000) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
document.getElementById('reset-form').addEventListener('click', function () {
|
||||
form.reset();
|
||||
responseEl.classList.add('d-none');
|
||||
@@ -51,10 +66,16 @@ window.onload = async function () {
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const year = parseYear(yearInput.value);
|
||||
const imdb_id = document.getElementById('imdb_id').value.trim();
|
||||
const category = document.getElementById('category').value.trim();
|
||||
const vod_link = document.getElementById('vod_link').value.trim();
|
||||
|
||||
if (!year) {
|
||||
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^tt\d{5,}$/.test(imdb_id)) {
|
||||
showResponse('warning', 'IMDB ID invalide. Exemple attendu: tt1234567');
|
||||
return;
|
||||
@@ -67,12 +88,14 @@ window.onload = async function () {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ imdb_id, category, vod_link })
|
||||
body: JSON.stringify({ year, imdb_id, category, vod_link })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showResponse('success', 'Film ajouté avec succès.');
|
||||
form.reset();
|
||||
await refreshYears();
|
||||
await loadMoviesForManagement();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,5 +111,146 @@ window.onload = async function () {
|
||||
showResponse('danger', err.message || 'Erreur réseau');
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshYears() {
|
||||
try {
|
||||
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));
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// Default add-year to latest year if empty
|
||||
if (years.length > 0 && !yearInput.value) {
|
||||
yearInput.value = String(years[0]);
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedMovieIds() {
|
||||
return Array.from(adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]:checked'))
|
||||
.map((el) => el.getAttribute('data-movie-id'))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function updateSelectionUI() {
|
||||
const selectedIds = getSelectedMovieIds();
|
||||
selectedCountEl.textContent = String(selectedIds.length);
|
||||
deleteSelectedBtn.disabled = selectedIds.length === 0;
|
||||
|
||||
const allBoxes = Array.from(adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]'));
|
||||
const checked = allBoxes.filter((b) => b.checked).length;
|
||||
selectAllBox.indeterminate = checked > 0 && checked < allBoxes.length;
|
||||
selectAllBox.checked = allBoxes.length > 0 && checked === allBoxes.length;
|
||||
}
|
||||
|
||||
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>`;
|
||||
|
||||
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>`;
|
||||
return;
|
||||
}
|
||||
const movies = await res.json();
|
||||
movies.sort((a, b) => (a.title || '').localeCompare((b.title || ''), 'fr', { sensitivity: 'base' }));
|
||||
|
||||
if (!movies.length) {
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="4" class="text-muted">Aucun film.</td></tr>`;
|
||||
updateSelectionUI();
|
||||
return;
|
||||
}
|
||||
|
||||
adminMoviesBody.innerHTML = '';
|
||||
movies.forEach((m) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input" data-movie-id="${m._id}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">${m.title || '(sans titre)'}</div>
|
||||
<div class="text-muted small">${m.imdb_id || ''}</div>
|
||||
</td>
|
||||
<td>${m.category || ''}</td>
|
||||
<td>${m.year || ''}</td>
|
||||
`;
|
||||
adminMoviesBody.appendChild(tr);
|
||||
});
|
||||
|
||||
adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]').forEach((cb) => {
|
||||
cb.addEventListener('change', updateSelectionUI);
|
||||
});
|
||||
|
||||
updateSelectionUI();
|
||||
} catch (err) {
|
||||
adminMoviesBody.innerHTML = `<tr><td colspan="4" class="text-danger">${err.message || 'Erreur réseau'}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
refreshMoviesBtn.addEventListener('click', loadMoviesForManagement);
|
||||
manageYearSelect.addEventListener('change', loadMoviesForManagement);
|
||||
|
||||
selectAllBox.addEventListener('change', () => {
|
||||
const checked = selectAllBox.checked;
|
||||
adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]').forEach((cb) => {
|
||||
cb.checked = checked;
|
||||
});
|
||||
updateSelectionUI();
|
||||
});
|
||||
|
||||
deleteSelectedBtn.addEventListener('click', async () => {
|
||||
const selectedIds = getSelectedMovieIds();
|
||||
if (!selectedIds.length) return;
|
||||
|
||||
const yearLabel = manageYearSelect.value ? ` (${manageYearSelect.value})` : '';
|
||||
const ok = window.confirm(`Supprimer ${selectedIds.length} film(s)${yearLabel} ? Cette action est irréversible.`);
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/movies/delete', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ ids: selectedIds })
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
showResponse('success', 'Film(s) supprimé(s) avec succès.');
|
||||
await refreshYears();
|
||||
await loadMoviesForManagement();
|
||||
} catch (err) {
|
||||
showResponse('danger', err.message || 'Erreur réseau');
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
await refreshYears();
|
||||
await loadMoviesForManagement();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user