Premier import de mon site Mon Cinéma
This commit is contained in:
+862
@@ -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, """);
|
||||
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.5–5 scale; convert to our 1–5 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
@@ -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)} · ${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();
|
||||
});
|
||||
Reference in New Issue
Block a user