Premier import de mon site Mon Cinéma

This commit is contained in:
2026-06-15 22:26:57 +02:00
commit c80aa67c50
7 changed files with 2223 additions and 0 deletions
+862
View File
@@ -0,0 +1,862 @@
const ADMIN_PASSWORD = 'cinema2024';
const STORAGE_KEY = 'mon-cinema-films';
let films = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
let currentRating = 0;
let editingFilmId = null;
let currentTab = 'critique';
function persist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(films));
}
if (location.pathname.includes('dashboard.html') && sessionStorage.getItem('admin-auth') !== '1') {
window.location.href = 'login.html';
}
function doLogin() {
const val = document.getElementById('login-pwd').value;
if (val === ADMIN_PASSWORD) {
sessionStorage.setItem('admin-auth', '1');
window.location.href = 'dashboard.html';
} else {
document.getElementById('login-err').style.display = 'block';
}
}
function switchTab(tabName) {
currentTab = tabName;
document.getElementById('tab-critiques').classList.toggle('active', tabName === 'critique');
document.getElementById('tab-videotheque').classList.toggle('active', tabName === 'videotheque');
const thDynamic = document.getElementById('th-dynamic-col');
if (thDynamic) {
thDynamic.textContent = tabName === 'critique' ? 'Note' : 'Format / Éditeur';
}
const btnCritiqueExport = document.getElementById('actions-critiques-export');
const btnCritiqueImport = document.getElementById('actions-critiques-import');
const btnVideoExport = document.getElementById('actions-video-export');
const btnVideoImport = document.getElementById('actions-video-import');
if (tabName === 'videotheque') {
if (btnCritiqueExport) btnCritiqueExport.style.display = 'none';
if (btnCritiqueImport) btnCritiqueImport.style.display = 'none';
if (btnVideoExport) btnVideoExport.style.display = 'inline-flex';
if (btnVideoImport) btnVideoImport.style.display = 'inline-flex';
} else {
if (btnCritiqueExport) btnCritiqueExport.style.display = 'inline-flex';
if (btnCritiqueImport) btnCritiqueImport.style.display = 'inline-flex';
if (btnVideoExport) btnVideoExport.style.display = 'none';
if (btnVideoImport) btnVideoImport.style.display = 'none';
}
renderAdminTable();
}
function handleTypeChange(typeValue) {
const videoFields = document.getElementById('form-group-videotheque-fields');
const critiqueFields = document.getElementById('form-group-critique-fields');
if (typeValue === 'videotheque') {
if (videoFields) videoFields.style.display = 'block';
if (critiqueFields) critiqueFields.style.display = 'none';
} else {
if (videoFields) videoFields.style.display = 'none';
if (critiqueFields) critiqueFields.style.display = 'block';
}
}
function setRating(n) {
currentRating = n;
const buttons = document.querySelectorAll('#star-select button');
buttons.forEach((btn, idx) => {
btn.style.color = idx < n ? 'var(--gold)' : 'var(--muted)';
});
}
function resetStars() {
currentRating = 0;
const buttons = document.querySelectorAll('#star-select button');
buttons.forEach(btn => btn.style.color = 'var(--muted)');
}
function saveTmdbKey() {
const key = document.getElementById('tmdb-key-input').value.trim();
localStorage.setItem('tmdb-api-key', key);
alert('Clé TMDB enregistrée avec succès !');
}
async function searchTMDB() {
const query = document.getElementById('search-input').value.trim();
const key = localStorage.getItem('tmdb-api-key') || document.getElementById('tmdb-key-input').value.trim();
const resultsWrap = document.getElementById('search-results-wrap');
if (!key) {
alert("Veuillez configurer votre clé API TMDB d'abord.");
return;
}
if (!query) return;
resultsWrap.innerHTML = '<div style="color:var(--muted); font-size:0.85rem; padding:0.5rem 0;">Recherche en cours...</div>';
try {
const res = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(query)}&language=fr-FR`);
const data = await res.json();
if (!data.results || data.results.length === 0) {
resultsWrap.innerHTML = '<div style="color:var(--muted); font-size:0.85rem; padding:0.5rem 0;">Aucun résultat trouvé.</div>';
return;
}
resultsWrap.innerHTML = `
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; max-height:200px; overflow-y:auto; background:rgba(255,255,255,0.05); padding:0.5rem; border-radius:4px;">
${data.results.slice(0, 5).map(movie => {
const posterUrl = movie.poster_path ? `https://image.tmdb.org/t/p/w300${movie.poster_path}` : '';
const year = movie.release_date ? movie.release_date.substring(0, 4) : '';
const safeTitle = movie.title.replace(/'/g, "\\'").replace(/"/g, "&quot;");
return `
<div onclick="selectTmdbMovie('${safeTitle}', '${year}', '${posterUrl}')"
style="display:flex; align-items:center; gap:0.7rem; padding:0.4rem; cursor:pointer; border-radius:3px; transition:background 0.2s;"
onmouseover="this.style.background='rgba(255,255,255,0.08)'"
onmouseout="this.style.background='transparent'">
${posterUrl ? `<img src="${posterUrl}" style="width:30px; height:42px; object-fit:cover; border-radius:2px;">` : `<div style="width:30px; height:42px; background:#111; display:flex; align-items:center; justify-content:center; font-size:0.5rem; color:var(--muted);">—</div>`}
<span style="font-size:0.85rem; color:var(--text);">${movie.title} <span style="color:var(--muted)">(${year})</span></span>
</div>
`;
}).join('')}
</div>
`;
} catch (err) {
resultsWrap.innerHTML = '<div style="color:var(--red); font-size:0.85rem; padding:0.5rem 0;">Erreur lors de la recherche.</div>';
}
}
function selectTmdbMovie(title, year, poster) {
document.getElementById('f-title').value = title;
document.getElementById('f-year').value = year;
if (poster) {
document.getElementById('f-poster').value = poster;
updatePosterPreview(poster);
}
document.getElementById('search-results-wrap').innerHTML = '';
}
function updatePosterPreview(url) {
const img = document.getElementById('poster-preview-img');
const none = document.getElementById('poster-preview-none');
const info = document.getElementById('poster-preview-info');
if (url) {
img.src = url;
img.style.display = 'block';
none.style.display = 'none';
info.innerHTML = "<strong>Affiche sélectionnée</strong>";
} else {
img.style.display = 'none';
none.style.display = 'flex';
info.innerHTML = "Aucune affiche sélectionnée.<br>Faites une recherche ou collez une URL.";
}
}
function openAddModal() {
editingFilmId = null;
document.getElementById('modal-title').textContent = "Nouvelle entrée";
document.getElementById('f-type').value = currentTab;
handleTypeChange(currentTab);
resetStars();
document.getElementById('f-title').value = '';
document.getElementById('f-year').value = '';
document.getElementById('f-director').value = '';
document.getElementById('f-poster').value = '';
document.getElementById('f-review').value = '';
document.getElementById('f-format').value = 'dvd';
document.getElementById('f-length').value = '';
document.getElementById('f-publisher').value = '';
document.getElementById('f-ean').value = '';
document.getElementById('f-discs').value = '1';
document.getElementById('f-aspect').value = '';
document.getElementById('f-description').value = '';
document.getElementById('search-input').value = '';
document.getElementById('search-results-wrap').innerHTML = '';
updatePosterPreview('');
document.getElementById('form-overlay').classList.add('open');
}
function openEditModal(id) {
const f = films.find(x => x.id === id);
if (!f) return;
editingFilmId = id;
document.getElementById('modal-title').textContent = "Modifier l'entrée";
const type = f.type || 'critique';
document.getElementById('f-type').value = type;
handleTypeChange(type);
document.getElementById('f-title').value = f.title || '';
document.getElementById('f-year').value = f.year || '';
document.getElementById('f-director').value = f.director || '';
document.getElementById('f-poster').value = f.poster || '';
document.getElementById('f-review').value = f.review || '';
document.getElementById('f-format').value = f.format || 'dvd';
document.getElementById('f-length').value = f.length || '';
document.getElementById('f-publisher').value = f.publisher || '';
document.getElementById('f-ean').value = f.ean_isbn13 || '';
document.getElementById('f-discs').value = f.number_of_discs || '1';
document.getElementById('f-aspect').value = f.aspect_ratio || '';
document.getElementById('f-description').value = f.description || '';
document.getElementById('search-input').value = '';
document.getElementById('search-results-wrap').innerHTML = '';
setRating(f.rating || 0);
updatePosterPreview(f.poster || '');
document.getElementById('form-overlay').classList.add('open');
}
function closeModal() {
document.getElementById('form-overlay').classList.remove('open');
}
function saveFilm() {
const title = document.getElementById('f-title').value.trim();
if (!title) {
alert("Le titre du film est obligatoire !");
return;
}
const type = document.getElementById('f-type').value;
const metadataVideotheque = type === 'videotheque' ? {
format: document.getElementById('f-format').value,
length: document.getElementById('f-length').value.trim(),
publisher: document.getElementById('f-publisher').value.trim(),
ean_isbn13: document.getElementById('f-ean').value.trim(),
number_of_discs: document.getElementById('f-discs').value.trim(),
aspect_ratio: document.getElementById('f-aspect').value.trim(),
description: document.getElementById('f-description').value.trim()
} : {
format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '', aspect_ratio: '', description: ''
};
if (editingFilmId !== null) {
const index = films.findIndex(f => f.id === editingFilmId);
if (index !== -1) {
films[index] = {
...films[index],
title: title,
type: type,
year: document.getElementById('f-year').value.trim(),
director: document.getElementById('f-director').value.trim(),
poster: document.getElementById('f-poster').value.trim(),
rating: type === 'critique' ? (currentRating || 1) : 0,
review: type === 'critique' ? document.getElementById('f-review').value.trim() : '',
streaming: films[index].streaming || "Disponible au cinéma ou support physique", // Conserve ou ajoute par défaut
...metadataVideotheque
};
}
} else {
const film = {
id: Date.now(),
title: title,
type: type,
year: document.getElementById('f-year').value.trim(),
director: document.getElementById('f-director').value.trim(),
poster: document.getElementById('f-poster').value.trim(),
rating: type === 'critique' ? (currentRating || 1) : 0,
review: type === 'critique' ? document.getElementById('f-review').value.trim() : '',
streaming: "Disponible au cinéma ou support physique", // Valeur par défaut à la création
...metadataVideotheque
};
films.unshift(film);
}
persist();
renderAdminTable();
closeModal();
}
function deleteFilm(id) {
if (confirm("Voulez-vous vraiment supprimer cet élément ?")) {
films = films.filter(f => f.id !== id);
persist();
renderAdminTable();
}
}
function renderAdminTable() {
const tbody = document.getElementById('admin-tbody');
const emptyState = document.getElementById('admin-empty');
const emptyMessage = document.getElementById('empty-message');
if (!tbody) return;
// Reset select-all + bulk-delete button BEFORE rebuilding DOM
const selectAll = document.getElementById('th-select-all');
if (selectAll) selectAll.checked = false;
const btnBulkReset = document.getElementById('btn-bulk-delete');
if (btnBulkReset) btnBulkReset.style.display = 'none';
const countSpanReset = document.getElementById('bulk-select-count');
if (countSpanReset) countSpanReset.textContent = '0';
const filteredFilms = films.filter(f => {
const fType = f.type || 'critique';
return fType === currentTab;
});
if (filteredFilms.length === 0) {
tbody.innerHTML = '';
if (emptyState) {
emptyMessage.textContent = currentTab === 'critique'
? "Aucun film pour l'instant. Ajoutez votre première critique !"
: "Votre vidéothèque est vide. Ajoutez vos premiers DVD ou Blu-ray !";
emptyState.style.display = 'block';
}
return;
}
if (emptyState) emptyState.style.display = 'none';
// Update film count display
const countEl = document.getElementById('films-count');
if (countEl) countEl.textContent = filteredFilms.length;
tbody.innerHTML = filteredFilms.map(f => {
const posterHtml = f.poster
? `<img class="thumb" src="${f.poster}" alt="" style="width:40px; height:55px; object-fit:cover; border-radius:3px;">`
: '<div style="color:var(--muted); text-align:center;">—</div>';
let dynamicCellHtml = '';
if (currentTab === 'critique') {
dynamicCellHtml = `<span style="color:var(--gold); letter-spacing:0.05em;">${'★'.repeat(f.rating || 1)}${'☆'.repeat(5 - (f.rating || 1))}</span>`;
} else {
let formatLabel = (f.format || 'DVD').toUpperCase().replace('_4K', ' 4K');
let publisherLabel = f.publisher ? `<div style="font-size:0.75rem; color:var(--muted); margin-top:2px;">${f.publisher}</div>` : '';
dynamicCellHtml = `<span class="badge-format" style="background:rgba(255,255,255,0.1); padding:0.2rem 0.5rem; border-radius:3px; font-size:0.8rem; font-weight:bold;">${formatLabel}</span>${publisherLabel}`;
}
return `
<tr>
<td style="text-align:center;">
<input type="checkbox" class="row-select" data-id="${f.id}" onclick="updateBulkDeleteButton()" style="cursor:pointer; transform: scale(1.2);">
</td>
<td>${posterHtml}</td>
<td style="font-weight:500; color:var(--text);">${f.title}</td>
<td>${f.year || '—'}</td>
<td>${f.director || '—'}</td>
<td>${dynamicCellHtml}</td>
<td style="display: flex; gap: 0.4rem; border: none;">
<button onclick="openEditModal(${f.id})" style="background: var(--gold); color: #000; border: none; padding: 0.4rem 0.6rem; border-radius: 3px; cursor: pointer; font-size: 0.8rem; font-weight: 500; display: inline-flex; align-items: center; gap: 0.2rem;">
Modifier
</button>
<button onclick="deleteFilm(${f.id})" style="background:var(--red); color:#fff; border:none; padding:0.4rem 0.6rem; border-radius:3px; cursor:pointer; font-size:0.8rem; display: inline-flex; align-items: center; gap: 0.2rem;">
Supprimer
</button>
</td>
</tr>
`;
}).join('');
}
function toggleSelectAll(masterCheckbox) {
const checkboxes = document.querySelectorAll('.row-select');
checkboxes.forEach(cb => cb.checked = masterCheckbox.checked);
updateBulkDeleteButton();
}
function updateBulkDeleteButton() {
const checkedBoxes = document.querySelectorAll('.row-select:checked');
const btnBulk = document.getElementById('btn-bulk-delete');
const countSpan = document.getElementById('bulk-select-count');
if (btnBulk && countSpan) {
if (checkedBoxes && checkedBoxes.length > 0) {
countSpan.textContent = checkedBoxes.length;
btnBulk.style.display = 'inline-flex';
} else {
btnBulk.style.display = 'none';
}
}
}
function deleteSelectedFilms() {
const checkedBoxes = document.querySelectorAll('.row-select:checked');
if (!checkedBoxes || checkedBoxes.length === 0) return;
if (confirm(`Êtes-vous sûr de vouloir supprimer définitivement ces ${checkedBoxes.length} éléments ?`)) {
const idsToDelete = Array.from(checkedBoxes).map(cb => parseInt(cb.getAttribute('data-id')));
films = films.filter(f => !idsToDelete.includes(f.id));
persist();
renderAdminTable();
}
}
// ── Letterboxd CSV parser (handles quoted commas correctly) ──────────────
function parseLetterboxdCSV(text) {
const rows = [];
const lines = text.split(/\r\n|\n/);
for (const line of lines) {
if (!line.trim()) continue;
const cols = [];
let cur = '', inQ = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (c === '"') {
if (inQ && line[i+1] === '"') { cur += '"'; i++; }
else inQ = !inQ;
} else if (c === ',' && !inQ) {
cols.push(cur); cur = '';
} else {
cur += c;
}
}
cols.push(cur);
rows.push(cols);
}
return rows;
}
// ── Show import progress overlay ─────────────────────────────────────────
function showImportProgress(total) {
let overlay = document.getElementById('import-progress-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'import-progress-overlay';
overlay.style.cssText = `
position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:999;
display:flex;align-items:center;justify-content:center;
`;
overlay.innerHTML = `
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;
padding:2rem 2.5rem;min-width:320px;text-align:center;">
<div style="font-family:'Playfair Display',serif;color:var(--gold);font-size:1.2rem;margin-bottom:1.2rem;">
Import
</div>
<div id="import-progress-label" style="font-size:0.82rem;color:var(--muted);margin-bottom:0.8rem;">
Initialisation…
</div>
<div style="background:var(--surface2);border-radius:99px;height:6px;overflow:hidden;margin-bottom:0.8rem;">
<div id="import-progress-bar" style="height:100%;background:var(--gold);width:0%;transition:width 0.3s;border-radius:99px;"></div>
</div>
<div id="import-progress-count" style="font-size:0.75rem;color:var(--muted);">0 / ${total}</div>
</div>
`;
document.body.appendChild(overlay);
}
return overlay;
}
function updateImportProgress(done, total, label) {
const bar = document.getElementById('import-progress-bar');
const lbl = document.getElementById('import-progress-label');
const cnt = document.getElementById('import-progress-count');
if (bar) bar.style.width = Math.round((done / total) * 100) + '%';
if (lbl) lbl.textContent = label;
if (cnt) cnt.textContent = `${done} / ${total}`;
}
function hideImportProgress() {
const el = document.getElementById('import-progress-overlay');
if (el) el.remove();
}
// ── TMDB poster lookup (single film) ─────────────────────────────────────
async function fetchTmdbPoster(title, year) {
const key = localStorage.getItem('tmdb-api-key');
if (!key) return '';
try {
const yearParam = year ? `&year=${year}` : '';
const res = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR${yearParam}`
);
const data = await res.json();
if (data.results && data.results.length > 0 && data.results[0].poster_path) {
return `https://image.tmdb.org/t/p/w500${data.results[0].poster_path}`;
}
// Retry without year if no result
if (year) {
const res2 = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR`
);
const data2 = await res2.json();
if (data2.results && data2.results.length > 0 && data2.results[0].poster_path) {
return `https://image.tmdb.org/t/p/w500${data2.results[0].poster_path}`;
}
}
} catch (e) {}
return '';
}
// ── DVD/Blu-ray cover: Open Library (EAN) → TMDB fallback ────────────────
async function fetchDvdCover(ean, title, year) {
// Step 1 — Open Library ISBN/EAN lookup (no key needed)
if (ean) {
try {
const olRes = await fetch(
`https://openlibrary.org/api/books?bibkeys=ISBN:${encodeURIComponent(ean)}&format=json&jscmd=data`
);
const olData = await olRes.json();
const key = `ISBN:${ean}`;
if (olData[key]) {
const book = olData[key];
// Prefer large cover, fall back to medium/small
if (book.cover) {
const coverUrl = book.cover.large || book.cover.medium || book.cover.small || '';
if (coverUrl) return coverUrl;
}
}
} catch (e) {}
// Step 1b — Open Library cover API directly by ISBN (faster path)
try {
const testUrl = `https://covers.openlibrary.org/b/isbn/${ean}-L.jpg?default=false`;
const testRes = await fetch(testUrl);
if (testRes.ok && testRes.headers.get('content-type')?.includes('image')) {
return testUrl;
}
} catch (e) {}
}
// Step 2 — TMDB fallback (uses existing key)
return await fetchTmdbPoster(title, year);
}
// ── Main Letterboxd import (reviews.csv OR ratings.csv) ──────────────────
function importFromCSV(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async function (e) {
const text = e.target.result;
const rows = parseLetterboxdCSV(text);
if (rows.length <= 1) {
alert("Le fichier semble vide ou invalide.");
input.value = '';
return;
}
const headers = rows[0].map(h => h.trim().toLowerCase());
// Detect file type: reviews.csv has a "Review" column, ratings.csv doesn't
const isReviews = headers.includes('review');
const iCol = { // column indices by header name
name: headers.indexOf('name'),
year: headers.indexOf('year'),
rating: headers.indexOf('rating'),
review: isReviews ? headers.indexOf('review') : -1,
watchedDate: headers.indexOf('watched date'),
};
// Collect rows to import (skip duplicates first pass)
const toImport = [];
let duplicateCount = 0;
for (let i = 1; i < rows.length; i++) {
const cols = rows[i];
const title = iCol.name >= 0 ? cols[iCol.name]?.trim() : '';
if (!title) continue;
const year = iCol.year >= 0 ? (cols[iCol.year]?.trim() || '') : '';
const ratingRaw = iCol.rating >= 0 ? parseFloat(cols[iCol.rating]) : 0;
// Letterboxd uses 0.55 scale; convert to our 15 integer scale
const rating = Math.round(Math.min(5, Math.max(1, ratingRaw)));
const review = (isReviews && iCol.review >= 0) ? (cols[iCol.review]?.trim() || '') : '';
const alreadyExists = films.some(
f => f.title.toLowerCase() === title.toLowerCase() && f.year === year
);
if (alreadyExists) { duplicateCount++; continue; }
toImport.push({ title, year, rating, review });
}
if (toImport.length === 0) {
alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`);
input.value = '';
return;
}
const hasTmdb = !!localStorage.getItem('tmdb-api-key');
showImportProgress(toImport.length);
let importedCount = 0;
for (let i = 0; i < toImport.length; i++) {
const { title, year, rating, review } = toImport[i];
updateImportProgress(i, toImport.length, `🎬 ${title}`);
let poster = '';
let streaming = "Disponible au cinéma ou support physique";
if (hasTmdb) {
// On récupère les deux infos en un seul traitement
const details = await fetchTmdbDetails(title, year);
poster = details.poster;
streaming = details.streaming;
// Augmentation légère du délai pour respecter les limites de l'API avec le double appel
await new Promise(r => setTimeout(r, 350));
}
films.push({
id: Date.now() + Math.floor(Math.random() * 100000) + i,
title,
type: 'critique',
year,
director: '',
rating,
review,
poster,
streaming, // Ajout du nouveau champ
format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '1', aspect_ratio: '', description: ''
});
importedCount++;
}
updateImportProgress(toImport.length, toImport.length, '✅ Terminé !');
await new Promise(r => setTimeout(r, 600));
hideImportProgress();
films.sort((a, b) => b.id - a.id);
persist();
renderAdminTable();
const typeLabel = isReviews ? 'reviews.csv (avec critiques)' : 'ratings.csv (notes seules)';
const posterInfo = hasTmdb ? ' · Affiches récupérées via TMDB' : ' · ⚠️ Clé TMDB absente, affiches non récupérées';
alert(
`Import Letterboxd — ${typeLabel}\n` +
`${importedCount} film(s) importé(s)${posterInfo}\n` +
`⚠️ ${duplicateCount} doublon(s) ignoré(s).`
);
input.value = '';
};
reader.readAsText(file, 'UTF-8');
}
function exportToCSV() {
if (films.length === 0) { alert("Aucune donnée à exporter."); return; }
let csvContent = "ID;Titre;Annee;Realisateur;Note;Critique;URL_Affiche;Type;Format\r\n";
films.filter(f => (f.type || 'critique') === 'critique').forEach(f => {
const cleanTitle = (f.title || '').replace(/"/g, '""');
const cleanDirector = (f.director || '').replace(/"/g, '""');
const cleanReview = (f.review || '').replace(/\r?\n|\r/g, ' ').replace(/"/g, '""');
const cleanPoster = (f.poster || '').replace(/"/g, '""');
csvContent += `${f.id};"${cleanTitle}";${f.year || ''};"${cleanDirector}";${f.rating || 0};"${cleanReview}";"${cleanPoster}";"critique";""\r\n`;
});
const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `export_critiques_${Date.now()}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function importVideothequeFromInput(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async function(e) {
await importVideothequeCSV(e.target.result);
input.value = '';
};
reader.readAsText(file, 'UTF-8');
}
async function importVideothequeCSV(text) {
// ── Parse CSV (handles quoted commas) ──
const lines = [];
let row = [""];
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
let c = text[i], next = text[i+1];
if (c === '"') {
if (inQuotes && next === '"') { row[row.length - 1] += '"'; i++; }
else { inQuotes = !inQuotes; }
} else if (c === ',' && !inQuotes) {
row.push('');
} else if ((c === '\r' || c === '\n') && !inQuotes) {
if (c === '\r' && next === '\n') { i++; }
lines.push(row); row = [''];
} else {
row[row.length - 1] += c;
}
}
if (row.length > 1 || row[0] !== '') lines.push(row);
if (lines.length <= 1) return;
// ── Collect rows to import ──
const toImport = [];
let duplicateCount = 0;
for (let i = 1; i < lines.length; i++) {
const data = lines[i];
if (data.length < 2 || !data[1]) continue;
const title = data[1].trim();
const creators = data[2] ? data[2].trim() : '';
const ean = data[6] ? data[6].trim() : '';
const description = data[8] ? data[8].trim() : '';
const publisher = data[9] ? data[9].trim() : '';
const publishDate = data[10] ? data[10].trim() : '';
const length = data[15] ? data[15].trim() : '';
const discs = data[16] ? data[16].trim() : '1';
const aspect = data[20] ? data[20].trim() : '';
let year = publishDate ? publishDate.split('-')[0] : '';
const lowerTitle = title.toLowerCase();
const lowerDesc = description.toLowerCase();
let format = 'dvd';
if (lowerTitle.includes('blu-ray') || lowerTitle.includes('bluray') ||
lowerDesc.includes('blu-ray') || lowerTitle.includes('4k')) {
format = (lowerTitle.includes('4k') || lowerDesc.includes('4k')) ? 'bluray_4k' : 'bluray';
}
const duplicate = films.some(
f => f.type === 'videotheque' &&
f.title.toLowerCase() === title.toLowerCase() &&
f.year === year
);
if (duplicate) { duplicateCount++; continue; }
toImport.push({ title, creators, ean, description, publisher, year, length, discs, aspect, format });
}
if (toImport.length === 0) {
alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`);
return;
}
// ── Progress bar ──
showImportProgress(toImport.length);
let importedCount = 0;
let coversFound = 0;
for (let i = 0; i < toImport.length; i++) {
const { title, creators, ean, description, publisher, year, length, discs, aspect, format } = toImport[i];
const sourceLabel = ean ? `EAN ${ean}` : title;
updateImportProgress(i, toImport.length, `📀 ${sourceLabel}`);
// ── Fetch cover: Open Library (EAN) → TMDB ──
let poster = '';
try {
poster = await fetchDvdCover(ean, title, year);
if (poster) coversFound++;
} catch(e) {}
// Respectful delay
await new Promise(r => setTimeout(r, 300));
films.push({
id: Date.now() + Math.floor(Math.random() * 100000) + i,
title, type: 'videotheque',
director: creators, year, poster, format, length,
publisher, ean_isbn13: ean,
number_of_discs: discs,
aspect_ratio: aspect,
description, rating: 0, review: ''
});
importedCount++;
}
updateImportProgress(toImport.length, toImport.length, '✅ Terminé !');
await new Promise(r => setTimeout(r, 600));
hideImportProgress();
persist();
renderAdminTable();
alert(
`Import Vidéothèque\n` +
`${importedCount} film(s) importé(s)\n` +
`🖼️ ${coversFound} jaquette(s) trouvée(s) (Open Library + TMDB)\n` +
`⚠️ ${duplicateCount} doublon(s) ignoré(s).`
);
}
function exportVideothequeToCSV() {
const filmsVideo = films.filter(f => (f.type || 'critique') === 'videotheque');
if (filmsVideo.length === 0) { alert("Aucune donnée de vidéothèque à exporter."); return; }
let csvContent = "item_type,title,creators,ean_isbn13,description,publisher,publish_date,length,number_of_discs,aspect_ratio\r\n";
filmsVideo.forEach(f => {
const cleanTitle = (f.title || '').replace(/"/g, '""');
const cleanDirector = (f.director || '').replace(/"/g, '""');
const cleanEan = (f.ean_isbn13 || '');
const cleanDesc = (f.description || '').replace(/\r?\n|\r/g, ' ').replace(/"/g, '""');
const cleanPublisher = (f.publisher || '').replace(/"/g, '""');
const fakePublishDate = f.year ? `${f.year}-01-01` : '';
csvContent += `movie,"${cleanTitle}","${cleanDirector}","${cleanEan}","${cleanDesc}","${cleanPublisher}","${fakePublishDate}","${f.length || ''}","${f.number_of_discs || '1'}","${f.aspect_ratio || ''}"\r\n`;
});
const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `export_videotheque_${Date.now()}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function goPublic() { window.location.href = '../index.html'; }
document.addEventListener('DOMContentLoaded', () => {
const savedKey = localStorage.getItem('tmdb-api-key');
if (savedKey && document.getElementById('tmdb-key-input')) {
document.getElementById('tmdb-key-input').value = savedKey;
}
renderAdminTable();
});
// Récupère l'affiche ET les plateformes de streaming en France
async function fetchTmdbDetails(title, year) {
const key = localStorage.getItem('tmdb-api-key');
let streamingDefault = "Disponible au cinéma ou support physique";
if (!key) return { poster: '', streaming: streamingDefault };
try {
const yearParam = year ? `&year=${year}` : '';
const res = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR${yearParam}`);
const data = await res.json();
let movie = data.results && data.results.length > 0 ? data.results[0] : null;
if (!movie && year) {
const res2 = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR`);
const data2 = await res2.json();
if (data2.results && data2.results.length > 0) movie = data2.results[0];
}
if (movie) {
const poster = movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : '';
let streaming = streamingDefault;
try {
const providerRes = await fetch(`https://api.themoviedb.org/3/movie/${movie.id}/watch/providers?api_key=${key}`);
const providerData = await providerRes.json();
// On cherche les plateformes d'abonnement (flatrate) disponibles en France (FR)
if (providerData.results && providerData.results.FR && providerData.results.FR.flatrate) {
const providers = providerData.results.FR.flatrate.map(p => p.provider_name);
if (providers.length > 0) {
streaming = providers.join(', ');
}
}
} catch (e) {
console.error("Erreur fournisseurs streaming:", e);
}
return { poster, streaming };
}
} catch (e) {
console.error("Erreur recherche TMDB:", e);
}
return { poster: '', streaming: streamingDefault };
}
+265
View File
@@ -0,0 +1,265 @@
const STORAGE_KEY = 'mon-cinema-films';
let films = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
let currentPubTab = 'critique';
let activeRatingFilter = 0; // 0 = tous
function switchPubTab(tabName) {
currentPubTab = tabName;
activeRatingFilter = 0;
document.querySelectorAll('.rating-filter-btn').forEach(btn => {
btn.classList.remove('active');
btn.querySelectorAll('.rf-star').forEach(s => s.classList.remove('filled'));
});
const tabCritiques = document.getElementById('tab-pub-critiques');
const tabVideotheque = document.getElementById('tab-pub-videotheque');
if (tabCritiques && tabVideotheque) {
if (tabName === 'critique') {
tabCritiques.classList.add('active');
tabVideotheque.classList.remove('active');
} else {
tabVideotheque.classList.add('active');
tabCritiques.classList.remove('active');
}
}
renderPublicGrid();
}
function filterByRating(stars) {
activeRatingFilter = (activeRatingFilter === stars) ? 0 : stars;
// Update button states
document.querySelectorAll('.rating-filter-btn').forEach(btn => {
const n = parseInt(btn.dataset.stars);
btn.classList.toggle('active', n === activeRatingFilter);
// Highlight filled stars up to active filter
btn.querySelectorAll('.rf-star').forEach((s, i) => {
s.classList.toggle('filled', activeRatingFilter > 0 && i < activeRatingFilter);
});
});
renderPublicGrid();
}
function renderPublicGrid() {
const grid = document.getElementById('grid');
const emptyState = document.getElementById('empty-state');
const countLabel = document.getElementById('count-label');
if (!grid) return;
// Show rating filter only on critique tab
const filterBar = document.getElementById('rating-filter-bar');
if (filterBar) filterBar.style.display = currentPubTab === 'critique' ? 'flex' : 'none';
const filtered = films.filter(f => {
const fType = f.type || 'critique';
if (fType !== currentPubTab) return false;
if (currentPubTab === 'critique' && activeRatingFilter > 0) {
return (f.rating || f.note || 1) === activeRatingFilter;
}
return true;
});
if (countLabel) {
if (currentPubTab === 'critique') {
const suffix = activeRatingFilter > 0 ? ` · filtrées ${activeRatingFilter}` : '';
countLabel.textContent = filtered.length + (filtered.length > 1 ? ' critiques' : ' critique') + suffix;
} else {
countLabel.textContent = filtered.length + (filtered.length > 1 ? ' films physiques' : ' film physique');
}
}
if (filtered.length === 0) {
grid.innerHTML = '';
if (emptyState) {
emptyState.style.display = 'block';
emptyState.querySelector('p').textContent = currentPubTab === 'critique'
? "Aucune critique pour l'instant."
: "Aucun film dans la vidéothèque pour l'instant.";
}
return;
}
if (emptyState) emptyState.style.display = 'none';
grid.innerHTML = filtered.map(f => {
const posterUrl = f.poster || f.image || f.affiche || '';
const movieTitle = f.title || f.titre || 'Sans titre';
const movieYear = f.year || f.annee || f.publish_date || '—';
const movieDirector = f.director || f.creators || f.realisateur || '—';
const posterHtml = posterUrl
? `<img src="${posterUrl}" alt="Affiche ${movieTitle}" class="card-poster">`
: `<div class="card-poster-placeholder">
<i class="ti ti-movie"></i>
<span>${movieTitle}</span>
</div>`;
let footerInfoHtml = '';
if (currentPubTab === 'critique') {
const rating = f.rating || f.note || 1;
footerInfoHtml = `<div class="card-stars">${"★".repeat(rating)}<span class="stars-muted">${"☆".repeat(5 - rating)}</span></div>`;
} else {
const format = (f.format || f.support || 'DVD').toUpperCase().replace('_4K', ' 4K');
const length = f.length || f.duree || '';
footerInfoHtml = `
<div class="card-video-footer">
<span class="badge-format">${format}</span>
${length ? `<span class="video-length"><i class="ti ti-clock"></i> ${length} min</span>` : ''}
</div>`;
}
return `
<div class="card" onclick="openDetail(${f.id})">
<div class="card-poster-wrap">
${posterHtml}
</div>
<div class="card-body">
<h3 class="card-title">${movieTitle}</h3>
<div class="card-meta">${movieYear.toString().substring(0,4)} &middot; ${movieDirector}</div>
${footerInfoHtml}
</div>
</div>
`;
}).join('');
}
function openDetail(id) {
const f = films.find(x => x.id === id);
if (!f) return;
const detailOverlay = document.getElementById('detail-overlay');
const modalLayout = document.getElementById('detail-modal-layout');
const dPoster = document.getElementById('d-poster');
const dPosterWrap = document.getElementById('d-poster-wrap');
const dTitle = document.getElementById('d-title');
const dMeta = document.getElementById('d-meta');
const dStars = document.getElementById('d-stars');
const dReview = document.getElementById('d-review');
const movieTitle = f.title || f.titre || 'Sans titre';
const movieYear = f.year || f.annee || f.publish_date || '—';
const movieDirector = f.director || f.creators || f.realisateur || '—';
const posterUrl = f.poster || f.image || f.affiche || '';
if (dTitle) dTitle.textContent = movieTitle;
if (dMeta) dMeta.textContent = [movieYear.toString().substring(0,4), movieDirector].filter(Boolean).join(' · ');
const fType = f.type || 'critique';
// Gestion de l'affichage de la colonne image
if (posterUrl) {
if (dPoster) { dPoster.src = posterUrl; dPoster.style.display = 'block'; }
if (dPosterWrap) {
dPosterWrap.style.display = 'block';
dPosterWrap.classList.remove('poster-critique', 'poster-video');
dPosterWrap.classList.add(fType === 'critique' ? 'poster-critique' : 'poster-video');
}
if (modalLayout) modalLayout.classList.remove('no-poster');
} else {
if (dPoster) dPoster.style.display = 'none';
if (dPosterWrap) dPosterWrap.style.display = 'none';
if (modalLayout) modalLayout.classList.add('no-poster');
}
// Préparation du badge de visionnage/streaming (commun aux deux types de fiches)
const streamingInfo = f.streaming || "Disponible au cinéma ou support physique";
const isPlatform = streamingInfo !== "Disponible au cinéma ou support physique";
const streamIcon = isPlatform ? "ti-device-tv" : "ti-movie";
const streamingBadgeHtml = `
<div style="margin-bottom: 1.2rem; display: inline-flex; align-items: center; gap: 0.5rem; background: ${isPlatform ? 'rgba(201, 168, 76, 0.1)' : 'rgba(255,255,255,0.03)'}; border: 1px solid ${isPlatform ? 'var(--gold)' : 'var(--border)'}; padding: 0.4rem 0.8rem; border-radius: 6px; font-size: 0.85rem; color: ${isPlatform ? 'var(--gold)' : '#c0c0cc'}; font-weight: 500;">
<i class="ti ${streamIcon}" style="font-size: 1.1rem;"></i>
<span>${streamingInfo}</span>
</div>
`;
if (fType === 'critique') {
// ----------------------------------------
// POP-UP MODE : CRITIQUE JOURNAL
// ----------------------------------------
if (dStars) {
dStars.style.display = 'block';
const rating = f.rating || f.note || 1;
dStars.innerHTML = "★".repeat(rating) + `<span class="stars-muted">${"☆".repeat(5 - rating)}</span>`;
}
if (dReview) {
const reviewText = f.review || f.critique || "Aucune critique rédigée pour ce film.";
dReview.innerHTML = `
${streamingBadgeHtml}
<div class="pub-review-card">
<div class="pub-review-quote-icon"><i class="ti ti-quote"></i></div>
<p class="pub-review-text">${reviewText}</p>
</div>
`;
}
} else {
// ----------------------------------------
// POP-UP MODE : VIDEOTHEQUE PREMIUM
// ----------------------------------------
if (dStars) dStars.style.display = 'none';
const format = (f.format || f.support || 'DVD').toUpperCase().replace('_4K', ' 4K');
const publisher = f.publisher || f.editeur || '';
const length = f.length || f.duree || '';
const discs = f.number_of_discs || f.disques || f.nb_disques || '';
const ratio = f.aspect_ratio || f.format_image || '';
const ean = f.ean_isbn13 || f.ean || '';
const description = f.description || f.synopsis || "Aucun synopsis disponible dans le catalogue.";
if (dReview) {
dReview.innerHTML = `
${streamingBadgeHtml}
<div class="pub-tech-badges">
<span class="tech-pill format-gold"><i class="ti ti-disc"></i> ${format}</span>
${length ? `<span class="tech-pill"><i class="ti ti-clock"></i> ${length} Min</span>` : ''}
${discs ? `<span class="tech-pill"><i class="ti ti-layers-intersect"></i> ${discs} ${discs > 1 ? 'Disques' : 'Disque'}</span>` : ''}
</div>
<div class="pub-tech-grid">
${publisher ? `
<div class="tech-meta-item">
<span class="tech-meta-label">Éditeur / Studio</span>
<span class="tech-meta-value">${publisher}</span>
</div>` : ''}
${ratio ? `
<div class="tech-meta-item">
<span class="tech-meta-label">Format d'image</span>
<span class="tech-meta-value" style="font-family: monospace;">${ratio}</span>
</div>` : ''}
${ean ? `
<div class="tech-meta-item" style="grid-column: span 2;">
<span class="tech-meta-label">Code-barres (EAN)</span>
<span class="tech-meta-value" style="font-family: monospace; color: var(--muted);">${ean}</span>
</div>` : ''}
</div>
<div class="pub-synopsis-box">
<h4>Synopsis</h4>
<p>${description}</p>
</div>
`;
}
}
if (detailOverlay) detailOverlay.classList.add('open');
}
function closeDetail() {
const detailOverlay = document.getElementById('detail-overlay');
if (detailOverlay) {
detailOverlay.classList.remove('open');
}
}
document.addEventListener('DOMContentLoaded', () => {
renderPublicGrid();
});