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
+241
View File
@@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/admin.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<title>Dashboard</title>
<style>
/* Styles pour le système d'onglets */
.admin-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border, #333);
padding-bottom: 0.5rem;
}
.tab-btn {
background: transparent;
border: none;
color: var(--muted, #888);
padding: 0.6rem 1.2rem;
font-family: 'DM Sans', sans-serif;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab-btn:hover {
color: var(--text, #fff);
background: rgba(255,255,255,0.03);
}
.tab-btn.active {
color: #000;
background: var(--gold, #C9A84C);
}
/* Style pour le badge de format dans le tableau */
.badge-format {
background: rgba(255,255,255,0.1);
color: var(--text, #fff);
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.05em;
}
</style>
</head>
<body>
<div id="page-admin" class="page active">
<div class="admin-wrap">
<div class="admin-header">
<h1 class="admin-title">Back<span>office</span></h1>
<div class="admin-actions" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<button class="btn btn-gold" onclick="openAddModal()"><i class="ti ti-plus" aria-hidden="true"></i> Ajouter un film</button>
<!-- GROUPE CRITIQUES (Affiché par défaut) -->
<button id="actions-critiques-export" class="btn btn-outline" onclick="exportToCSV()" title="Exporter mes critiques en CSV"><i class="ti ti-download" aria-hidden="true"></i> Exporter mes Critiques</button>
<button id="actions-critiques-import" class="btn btn-outline" onclick="document.getElementById('csv-import-file').click()" title="Importer un fichier CSV de critiques"><i class="ti ti-upload" aria-hidden="true"></i> Importer mes Critiques</button>
<input type="file" id="csv-import-file" accept=".csv" style="display: none;" onchange="importFromCSV(this)">
<!-- GROUPE VIDÉOTHÈQUE (Masqué par défaut au démarrage) -->
<button id="actions-video-export" class="btn btn-outline" style="display: none;" onclick="exportVideothequeToCSV()" title="Exporter ma vidéothèque en CSV"><i class="ti ti-download" aria-hidden="true"></i> Exporter ma Vidéothèque</button>
<button id="actions-video-import" class="btn btn-outline" style="display: none;" onclick="document.getElementById('csv-video-file').click()" title="Importer l'inventaire de ma vidéothèque (.csv)"><i class="ti ti-upload" aria-hidden="true"></i> Importer ma Vidéothèque</button>
<input type="file" id="csv-video-file" accept=".csv" style="display: none;" onchange="importVideothequeFromInput(this)">
<!-- BOUTON SUPPRESSION EN MASSE -->
<button id="btn-bulk-delete" class="btn" style="background: var(--red, #e54e4e); color: #fff; border: none; display: none;" onclick="deleteSelectedFilms()">
<i class="ti ti-trash-x" aria-hidden="true"></i> Supprimer la sélection (<span id="bulk-select-count">0</span>)
</button>
<button class="btn btn-outline" onclick="goPublic()"><i class="ti ti-eye" aria-hidden="true"></i> Voir le site</button>
</div>
</div>
<div class="admin-tabs">
<button class="tab-btn active" id="tab-critiques" onclick="switchTab('critique')">
<i class="ti ti-blockquote"></i> Mes Critiques
</button>
<button class="tab-btn" id="tab-videotheque" onclick="switchTab('videotheque')">
<i class="ti ti-disc"></i> Ma Vidéothèque
</button>
</div>
<div class="api-notice" id="api-notice">
<strong>Clé API TMDB requise pour la recherche d'affiches.</strong><br>
Gratuit sur <a href="https://www.themoviedb.org/settings/api" target="_blank">themoviedb.org/settings/api</a> (inscription gratuite, clé disponible immédiatement).<br>
<div style="display:flex;gap:0.6rem;margin-top:0.7rem;align-items:center;">
<input id="tmdb-key-input" type="text" placeholder="Colle ta clé TMDB ici" style="flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);font-family:'DM Sans',sans-serif;font-size:0.82rem;padding:0.5rem 0.7rem;border-radius:3px;outline:none;" />
<button class="btn btn-gold" style="white-space:nowrap;" onclick="saveTmdbKey()">Enregistrer</button>
</div>
</div>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th style="width:40px; text-align:center;">
<input type="checkbox" id="th-select-all" onclick="toggleSelectAll(this)" style="cursor:pointer; transform: scale(1.2);">
</th>
<th style="width:50px">Affiche</th>
<th>Titre</th>
<th>Année</th>
<th>Réalisateur</th>
<th id="th-dynamic-col">Note</th>
<th style="width:170px">Actions</th>
</tr>
</thead>
<tbody id="admin-tbody"></tbody>
</table>
</div>
<div class="empty-state" id="admin-empty" style="display:none">
<i class="ti ti-clapperboard-open" aria-hidden="true"></i>
<p id="empty-message">Aucun film pour l'instant. Ajoutez votre première critique !</p>
</div>
</div>
</div>
<div class="overlay" id="form-overlay" role="dialog" aria-modal="true">
<div class="modal">
<button class="modal-close" onclick="closeModal()" aria-label="Fermer"><i class="ti ti-x"></i></button>
<h2 class="modal-h" id="modal-title">Nouvelle entrée</h2>
<p style="font-size:0.75rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin-bottom:0.4rem;">Recherche automatique d'affiche</p>
<div class="search-row">
<input id="search-input" type="text" placeholder="Nom du film à rechercher…" onkeydown="if(event.key==='Enter'){event.preventDefault();searchTMDB();}" />
<button class="btn-search" onclick="searchTMDB()" aria-label="Rechercher"><i class="ti ti-search"></i></button>
</div>
<div id="search-results-wrap"></div>
<hr class="divider" />
<div class="form-group">
<label for="f-type">Type d'ajout</label>
<select id="f-type" onchange="handleTypeChange(this.value)" style="width:100%; background:var(--surface); border:1px solid var(--border); color:var(--text); padding:0.6rem; border-radius:3px; font-family:inherit;">
<option value="critique">Écrire une critique (Journal)</option>
<option value="videotheque">Ajouter à ma vidéothèque physique</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="f-title">Titre</label>
<input id="f-title" type="text" placeholder="ex : Mulholland Drive" />
</div>
<div class="form-group">
<label for="f-year">Année</label>
<input id="f-year" type="text" placeholder="ex : 2001" maxlength="4" />
</div>
</div>
<div class="form-group">
<label for="f-director">Réalisateur</label>
<input id="f-director" type="text" placeholder="ex : David Lynch" />
</div>
<div id="form-group-videotheque-fields" style="display: none;">
<div class="form-row" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<div class="form-group" style="flex: 1;">
<label for="f-format">Format physique</label>
<select id="f-format" style="width:100%; background:var(--surface); border:1px solid var(--border); color:var(--text); padding:0.6rem; border-radius:3px; font-family:inherit;">
<option value="dvd">DVD</option>
<option value="bluray">Blu-ray</option>
<option value="bluray_4k">Blu-ray 4K Ultra HD</option>
<option value="vhs">VHS</option>
<option value="collector">Édition Collector / Coffret</option>
</select>
</div>
<div class="form-group" style="flex: 1;">
<label for="f-length">Durée (minutes)</label>
<input id="f-length" type="number" placeholder="ex : 107" />
</div>
</div>
<div class="form-row" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<div class="form-group" style="flex: 1;">
<label for="f-publisher">Éditeur / Studio</label>
<input id="f-publisher" type="text" placeholder="ex : Universal Pictures" />
</div>
<div class="form-group" style="flex: 1;">
<label for="f-ean">Code-barres (EAN13)</label>
<input id="f-ean" type="text" placeholder="ex : 7321950374984" />
</div>
</div>
<div class="form-row" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<div class="form-group" style="flex: 1;">
<label for="f-discs">Nombre de disques</label>
<input id="f-discs" type="number" placeholder="ex : 2" defaultValue="1" />
</div>
<div class="form-group" style="flex: 1;">
<label for="f-aspect">Format d'image (Aspect Ratio)</label>
<input id="f-aspect" type="text" placeholder="ex : 2.35:1 ou 1.66:1" />
</div>
</div>
<div class="form-group" style="margin-bottom: 1rem;">
<label for="f-description">Description / Synopsis complet</label>
<textarea id="f-description" placeholder="Description extraite du catalogue..." style="height: 100px;"></textarea>
</div>
</div>
<div class="poster-preview" id="poster-preview-wrap">
<div class="poster-preview-none" id="poster-preview-none"><i class="ti ti-photo-off"></i></div>
<img id="poster-preview-img" src="" alt="" style="display:none" />
<div class="poster-preview-info" id="poster-preview-info">Aucune affiche sélectionnée.<br>Faites une recherche ou collez une URL.</div>
</div>
<div class="form-group">
<label for="f-poster">URL de l'affiche (optionnel)</label>
<input id="f-poster" type="url" placeholder="https://…" oninput="updatePosterPreview(this.value)" />
</div>
<div id="form-group-critique-fields">
<div class="form-group">
<label>Note</label>
<div class="star-select" id="star-select">
<button type="button" onclick="setRating(1)" aria-label="1 étoile"></button>
<button type="button" onclick="setRating(2)" aria-label="2 étoiles"></button>
<button type="button" onclick="setRating(3)" aria-label="3 étoiles"></button>
<button type="button" onclick="setRating(4)" aria-label="4 étoiles"></button>
<button type="button" onclick="setRating(5)" aria-label="5 étoiles"></button>
</div>
</div>
<div class="form-group">
<label for="f-review">Ma critique</label>
<textarea id="f-review" placeholder="Écris ta critique ici…"></textarea>
</div>
</div>
<button type="button" class="btn-save" onclick="saveFilm()">Enregistrer</button>
</div>
</div>
<script src="../js/admin.js?v=4"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/admin.css">
<title>Login</title></head>
<body>
<div id="page-login" class="page active">
<div class="login-wrap">
<h2>Backoffice</h2>
<input type="password" id="login-pwd" placeholder="Mot de passe" />
<p class="login-err" id="login-err">Mot de passe incorrect.</p>
<button class="btn btn-gold" onclick="doLogin()">Accéder</button>
</div>
</div>
<script src="../js/admin.js"></script>
</body></html>
+145
View File
@@ -0,0 +1,145 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--gold: #C9A84C; --gold-light: #E8C97A;
--dark: #0E0E0E; --surface: #1A1A1A; --surface2: #252525;
--text: #F0ECE3; --muted: #A8A4A0; --border: rgba(201,168,76,0.2);
--red: #c0392b;
}
body { background: var(--dark); color: var(--text); font-family: 'DM Sans', sans-serif; min-height: 100vh; }
/* ── PAGES ── */
.page { display: none; }
.page.active { display: block; }
/* ── SITE PUBLIC ── */
.site-wrap { max-width: 920px; margin: 0 auto; padding: 2rem 1.2rem 4rem; }
header { text-align: center; padding: 3rem 0 2rem; border-bottom: 1px solid var(--border); margin-bottom: 2.5rem; }
header::before { content: ''; display: block; width: 60px; height: 2px; background: var(--gold); margin: 0 auto 1.5rem; }
.site-title { font-family: 'Playfair Display', serif; font-size: 2.8rem; letter-spacing: 0.05em; }
.site-title span { color: var(--gold); font-style: italic; }
.site-subtitle { font-size: 0.78rem; letter-spacing: 0.25em; color: var(--muted); text-transform: uppercase; margin-top: 0.6rem; }
.pub-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.8rem; }
.count { font-size: 0.78rem; letter-spacing: 0.1em; color: #C0BCB8; text-transform: uppercase; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1.5rem; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; cursor: pointer; transition: transform 0.2s, border-color 0.2s; }
.card:hover { transform: translateY(-5px); border-color: var(--gold); }
.poster-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; background: var(--surface2); }
.poster-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.poster-ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 0.5rem; color: var(--muted); font-size: 0.75rem; }
.poster-ph i { font-size: 2.5rem; }
.rating-badge { position: absolute; top: 8px; right: 8px; background: rgba(14,14,14,0.85); border: 1px solid var(--gold); color: var(--gold); font-family: 'Playfair Display', serif; font-size: 1rem; font-weight: 700; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.card-body { padding: 0.9rem 1rem 1rem; }
.card-title { font-family: 'Playfair Display', serif; font-size: 1rem; line-height: 1.3; margin-bottom: 0.2rem; }
.card-year { font-size: 0.75rem; color: #B8B4B0; margin-bottom: 0.4rem; }
.card-stars { color: var(--gold); font-size: 0.8rem; letter-spacing: 0.1em; margin-bottom: 0.4rem; }
.card-excerpt { font-size: 0.78rem; color: #C0BCB8; line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.empty-state { text-align: center; padding: 5rem 1rem; color: #C8C4C0; }
.empty-state i { font-size: 3.5rem; display: block; margin-bottom: 1rem; opacity: 0.3; }
.empty-state p { font-size: 0.95rem; line-height: 1.7; }
footer { text-align: center; margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.75rem; color: var(--muted); letter-spacing: 0.1em; }
/* ── BACKOFFICE ── */
.admin-wrap { max-width: 960px; margin: 0 auto; padding: 2rem 1.2rem 4rem; }
.admin-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: 1.2rem; border-bottom: 1px solid var(--border); margin-bottom: 2rem; flex-wrap: wrap; gap: 1rem; }
.admin-title { font-family: 'Playfair Display', serif; font-size: 1.6rem; }
.admin-title span { color: var(--gold); }
.admin-actions { display: flex; gap: 0.7rem; align-items: center; }
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 0.55rem 1.1rem; font-family: 'DM Sans', sans-serif; font-size: 0.82rem; font-weight: 500; cursor: pointer; border-radius: 2px; letter-spacing: 0.04em; transition: background 0.2s, color 0.2s; border: none; }
.btn-gold { background: var(--gold); color: var(--dark); }
.btn-gold:hover { background: var(--gold-light); }
.btn-outline { background: transparent; border: 1px solid rgba(201,168,76,0.35); color: #C8C4BE; }
.btn-outline:hover { border-color: var(--gold); color: var(--gold); }
.btn-danger { background: transparent; border: 1px solid #555; color: #C0BBBB; }
.btn-danger:hover { border-color: var(--red); color: var(--red); }
/* Admin table */
.admin-table-wrap { overflow-x: auto; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.admin-table th { text-align: left; font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: #B8B4B0; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--border); font-weight: 600; }
.admin-table td { padding: 0.75rem 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.04); vertical-align: middle; }
.admin-table tr:hover td { background: var(--surface2); }
.thumb { width: 36px; height: 54px; object-fit: cover; border-radius: 2px; display: block; background: var(--surface2); }
.thumb-ph { width: 36px; height: 54px; background: var(--surface2); border-radius: 2px; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 1rem; }
.tbl-stars { color: var(--gold); font-size: 0.75rem; letter-spacing: 0.05em; }
.tbl-actions { display: flex; gap: 0.5rem; }
.tbl-actions button { background: none; border: none; cursor: pointer; color: #B0ACAA; font-size: 1rem; padding: 4px; border-radius: 2px; transition: color 0.15s; }
.tbl-actions button:hover { color: var(--gold); }
.tbl-actions button.del:hover { color: var(--red); }
/* ── MODAL ── */
.overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.88); z-index: 200; align-items: center; justify-content: center; padding: 1rem; }
.overlay.open { display: flex; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; width: 100%; max-width: 540px; max-height: 92vh; overflow-y: auto; padding: 2rem; position: relative; }
.modal-close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; color: var(--muted); cursor: pointer; font-size: 1.3rem; line-height: 1; }
.modal-close:hover { color: var(--text); }
.modal-h { font-family: 'Playfair Display', serif; font-size: 1.35rem; color: var(--gold); margin-bottom: 1.4rem; }
/* Film search */
.search-row { display: flex; gap: 0.6rem; margin-bottom: 1rem; }
.search-row input { flex: 1; background: var(--surface2); border: 1px solid rgba(255,255,255,0.1); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; padding: 0.65rem 0.8rem; border-radius: 3px; outline: none; }
.search-row input:focus { border-color: var(--gold); }
.btn-search { background: var(--gold); color: var(--dark); border: none; padding: 0 1rem; border-radius: 3px; cursor: pointer; font-size: 1rem; transition: background 0.2s; }
.btn-search:hover { background: var(--gold-light); }
.search-results { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 0.7rem; margin-bottom: 1.2rem; max-height: 220px; overflow-y: auto; }
.sr-item { cursor: pointer; border: 2px solid transparent; border-radius: 3px; overflow: hidden; transition: border-color 0.15s; }
.sr-item:hover, .sr-item.selected { border-color: var(--gold); }
.sr-item img { width: 100%; aspect-ratio: 2/3; object-fit: cover; display: block; }
.sr-item p { font-size: 0.65rem; color: var(--muted); padding: 3px 4px; line-height: 1.3; text-align: center; background: var(--surface2); }
.search-hint { font-size: 0.78rem; color: #B8B4B0; margin-bottom: 1rem; }
.searching { font-size: 0.8rem; color: var(--muted); text-align: center; padding: 1rem 0; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: #C0BCB8; margin-bottom: 0.35rem; }
.form-group input, .form-group textarea { width: 100%; background: var(--surface2); border: 1px solid rgba(255,255,255,0.1); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; padding: 0.65rem 0.8rem; border-radius: 3px; outline: none; transition: border-color 0.2s; }
.form-group input:focus, .form-group textarea:focus { border-color: var(--gold); }
.form-group textarea { resize: vertical; min-height: 100px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.star-select { display: flex; gap: 6px; }
.star-select button { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--muted); transition: color 0.15s, transform 0.1s; padding: 2px; line-height: 1; }
.star-select button.active { color: var(--gold); }
.star-select button:hover { transform: scale(1.15); }
.poster-preview { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.poster-preview img { width: 60px; height: 90px; object-fit: cover; border-radius: 3px; border: 1px solid var(--border); }
.poster-preview-none { width: 60px; height: 90px; background: var(--surface2); border-radius: 3px; border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 1.3rem; }
.poster-preview-info { font-size: 0.8rem; color: #B8B4B0; line-height: 1.6; }
.poster-preview-info strong { color: var(--text); font-weight: 500; }
.btn-save { width: 100%; background: var(--gold); color: var(--dark); border: none; padding: 0.8rem; font-family: 'DM Sans', sans-serif; font-size: 0.9rem; font-weight: 500; cursor: pointer; border-radius: 3px; margin-top: 0.5rem; transition: background 0.2s; }
.btn-save:hover { background: var(--gold-light); }
.divider { border: none; border-top: 1px solid var(--border); margin: 1.2rem 0; }
/* Detail overlay */
.detail-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 200; align-items: flex-start; justify-content: center; overflow-y: auto; padding: 3rem 1rem; }
.detail-overlay.open { display: flex; }
.detail-modal { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; width: 100%; max-width: 680px; padding: 2rem; position: relative; display: grid; grid-template-columns: 160px 1fr; gap: 2rem; align-items: start; }
@media(max-width:520px){ .detail-modal { grid-template-columns: 1fr; } }
.detail-poster { aspect-ratio: 2/3; background: var(--surface2); border-radius: 4px; overflow: hidden; }
.detail-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
.detail-title { font-family: 'Playfair Display', serif; font-size: 1.7rem; line-height: 1.2; margin-bottom: 0.3rem; }
.detail-meta { font-size: 0.8rem; color: #C0BCB8; margin-bottom: 0.8rem; }
.detail-stars { color: var(--gold); font-size: 1.1rem; letter-spacing: 0.15em; margin-bottom: 1.2rem; }
.detail-review { font-size: 0.95rem; line-height: 1.85; color: #D0CBC1; font-family: 'Playfair Display', serif; font-style: italic; }
.detail-review::before { content: '\201C'; font-size: 3.5rem; color: var(--gold); line-height: 0; vertical-align: -1rem; margin-right: 0.15rem; opacity: 0.6; }
/* Admin login */
.login-wrap { max-width: 360px; margin: 6rem auto; padding: 1.5rem; }
.login-wrap h2 { font-family: 'Playfair Display', serif; color: var(--gold); margin-bottom: 1.5rem; font-size: 1.4rem; }
.login-wrap input { width: 100%; background: var(--surface2); border: 1px solid rgba(255,255,255,0.1); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; padding: 0.65rem 0.8rem; border-radius: 3px; outline: none; margin-bottom: 1rem; }
.login-wrap input:focus { border-color: var(--gold); }
.login-err { color: var(--red); font-size: 0.82rem; margin-bottom: 0.8rem; display: none; }
/* TMDB badge */
.tmdb-note { font-size: 0.72rem; color: var(--muted); margin-top: 0.4rem; }
.tmdb-note a { color: var(--gold); text-decoration: none; }
/* API key notice */
.api-notice { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 1rem 1.2rem; margin-bottom: 1.5rem; font-size: 0.82rem; color: #C0BCB8; line-height: 1.6; }
.api-notice strong { color: var(--text); }
.api-notice a { color: var(--gold); }
+603
View File
@@ -0,0 +1,603 @@
/* --- VARIABLES DE DESIGN ET PALETTE SOMBRE --- */
:root {
--background: #0a0a0a;
--surface: #1a1a1a;
--surface-card: #1e1e1e;
--surface-hover: #272727;
--border: #2e2e2e;
--border-hover: #444444;
--text: #ffffff;
--text-secondary: #e4e4e7;
--muted: #aaaab8;
--gold: #C9A84C;
--gold-glow: rgba(201, 168, 76, 0.2);
--gold-dim: rgba(201, 168, 76, 0.08);
--shadow-cinematic: 0 15px 35px rgba(0, 0, 0, 0.7);
--shadow-overlay: 0 30px 70px rgba(0, 0, 0, 0.9);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* page visibility (used by index.html wrapper) */
.page { display: none; }
.page.active { display: block; }
body {
background-color: var(--background);
color: var(--text);
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
-webkit-font-smoothing: antialiased;
padding: 3rem 1rem;
}
.site-wrap {
max-width: 1040px;
margin: 0 auto;
}
/* --- EN-TÊTE CENTRÉ --- */
.site-header {
text-align: center;
margin-bottom: 3.5rem;
}
.site-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 3.2rem;
font-weight: 700;
letter-spacing: -0.03em;
margin-bottom: 0.6rem;
}
.site-title span {
color: var(--gold);
text-shadow: 0 0 20px var(--gold-glow);
}
.site-subtitle {
color: #c0c0cc;
font-size: 0.95rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* --- NAVIGATION PAR ONGLETS --- */
.pub-tabs-container {
display: flex;
justify-content: center;
margin-bottom: 3rem;
}
.pub-tabs {
background-color: var(--surface);
border: 1px solid var(--border);
padding: 0.4rem;
border-radius: 30px;
display: inline-flex;
gap: 0.4rem;
}
.tab-btn {
background: none;
border: none;
color: #d4d4de;
font-family: 'DM Sans', sans-serif;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
padding: 0.65rem 1.6rem;
border-radius: 25px;
display: flex;
align-items: center;
gap: 0.6rem;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.tab-btn i { font-size: 1.15rem; }
.tab-btn:hover {
color: var(--text);
background-color: var(--surface-hover);
}
.tab-btn.active {
color: var(--background);
background-color: var(--gold);
font-weight: 600;
box-shadow: 0 4px 15px var(--gold-glow);
}
/* --- BARRE D'OUTILS --- */
.pub-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border);
padding-bottom: 1rem;
}
.count {
font-size: 0.9rem;
color: #ccccd6;
font-weight: 500;
}
.btn-admin-link {
color: #d4b05a !important;
text-decoration: none;
font-size: 0.8rem;
letter-spacing: 0.06em;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 0.5rem;
border: 1px solid rgba(201, 168, 76, 0.2);
padding: 0.5rem 1rem;
border-radius: 4px;
transition: all 0.25s ease;
}
.btn-admin-link:hover {
background: var(--gold-dim);
border-color: var(--gold);
}
/* --- GRILLE PRINCIPALE ET CARTES --- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 2.5rem 1.8rem;
margin-top: 1rem;
}
.card {
background: transparent;
cursor: pointer;
display: flex;
flex-direction: column;
}
.card-poster-wrap {
width: 100%;
aspect-ratio: 2 / 3;
position: relative;
border-radius: 8px;
overflow: hidden;
background: #121212;
box-shadow: var(--shadow-cinematic);
border: 1px solid var(--border);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.card-poster {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.card-poster-wrap::after {
content: '';
position: absolute;
top: 0; left: -150%; width: 120%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
transform: skewX(-20deg);
}
.card:hover .card-poster-wrap {
transform: translateY(-8px);
border-color: var(--border-hover);
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.8);
}
.card:hover .card-poster-wrap::after {
left: 150%;
transition: all 0.75s ease;
}
.card:hover .card-poster { transform: scale(1.05); }
.card:hover .card-title { color: var(--gold); }
.card-poster-placeholder {
width: 100%; height: 100%;
display: flex; flex-direction: column; justify-content: center; align-items: center;
padding: 1.2rem; text-align: center;
background: linear-gradient(135deg, #181818, #0a0a0a);
}
.card-poster-placeholder i { font-size: 2.5rem; color: var(--gold); margin-bottom: 0.7rem; }
.card-body { padding: 0.9rem 0 0 0; }
.card-title {
font-size: 0.95rem; margin: 0 0 0.3rem 0; font-weight: 600; color: #ffffff;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
transition: color 0.25s ease;
}
.card-meta { font-size: 0.8rem; color: #c4c4d0; font-weight: 500; }
.card-stars {
color: #FFD700;
margin-top: 0.4rem;
font-size: 0.85rem;
letter-spacing: 1px;
}
.card-stars .stars-muted {
color: #ffffff;
opacity: 0.45;
}
.card-video-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; }
.badge-format {
background: var(--gold-dim); color: var(--gold); font-size: 0.7rem; font-weight: bold;
padding: 0.15rem 0.5rem; border-radius: 3px; border: 1px solid rgba(201, 168, 76, 0.2);
text-transform: uppercase;
}
.video-length { color: #a8a8b8; font-size: 0.75rem; display: inline-flex; align-items: center; gap: 0.25rem; }
/* ==========================================================================
REFONTE IMMERSIVE DE LA POP-UP (MODALE PREMIUM)
========================================================================== */
.detail-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(3, 3, 3, 0.85);
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
padding: 2rem 1rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.detail-overlay.open {
opacity: 1;
pointer-events: all;
}
.detail-modal {
background: #0f0f10;
border: 1px solid var(--border);
border-radius: 16px;
max-width: 840px;
width: 100%;
max-height: 85vh;
overflow-y: auto;
position: relative;
display: flex;
gap: 3.5rem;
padding: 3.5rem;
box-shadow: var(--shadow-overlay);
transform: scale(0.96) translateY(20px);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.detail-overlay.open .detail-modal {
transform: scale(1) translateY(0);
}
.detail-modal.no-poster {
max-width: 620px;
display: block;
}
/* Bouton Fermer */
.modal-close {
position: absolute;
top: 1.5rem; right: 1.5rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
color: #d0d0da;
width: 38px; height: 38px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem; cursor: pointer;
transition: all 0.25s ease;
z-index: 10;
}
.modal-close:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.08);
border-color: #444;
transform: scale(1.05);
}
/* Colonne Image (Affiche) */
.detail-poster {
flex: 0 0 240px;
width: 240px;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.8);
background: #141414;
align-self: flex-start;
}
.detail-poster.poster-critique { aspect-ratio: unset; }
.detail-poster.poster-critique img { width: 100%; height: auto; display: block; }
.detail-poster.poster-video { aspect-ratio: 2 / 3; display: flex; align-items: center; justify-content: center; }
.detail-poster.poster-video img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* Colonne Contenu */
.detail-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.detail-header-block {
margin-bottom: 1.8rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
padding-bottom: 1.2rem;
}
.detail-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 2.4rem;
font-weight: 700;
margin-bottom: 0.5rem;
line-height: 1.2;
color: var(--text);
}
.detail-meta {
font-size: 0.95rem;
color: #d0d0da;
margin-bottom: 0.8rem;
font-weight: 500;
letter-spacing: 0.02em;
}
/* --- VISIBILITÉ DES ÉTOILES DANS LA POP-UP --- */
.detail-stars {
color: #FFD700; /* Un or plus lumineux et chaud (YellowGold) */
font-size: 1.6rem; /* Taille augmentée pour une meilleure accroche visuelle */
letter-spacing: 0.12em; /* Espace subtil entre les étoiles */
margin-bottom: 1.2rem;
/* Effet de néon cinématographique discret sur les étoiles pleines */
text-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 20px rgba(255, 215, 0, 0.2);
display: inline-flex;
align-items: center;
}
/* Style appliqué aux étoiles vides (masquées en arrière-plan) */
.detail-stars .stars-muted {
color: #ffffff;
opacity: 0.35; /* Très estompé dans le noir pour faire ressortir la note réelle */
text-shadow: none; /* Pas d'effet de lueur pour les étoiles vides */
}
/* --- DESIGN SUBLIMÉ DE LA CRITIQUE --- */
.pub-review-card {
position: relative;
background: linear-gradient(145deg, #151516, #111112);
border: 1px solid var(--border);
border-radius: 12px;
padding: 2.2rem 2rem 2rem 2rem;
margin-top: 0.5rem;
box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.03);
}
.pub-review-quote-icon {
position: absolute;
top: -14px; left: 24px;
background: #181819;
border: 1px solid var(--border);
color: var(--gold);
width: 30px; height: 30px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.95rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
.pub-review-text {
font-family: 'DM Sans', sans-serif;
color: var(--text-secondary);
font-size: 1.05rem;
line-height: 1.8;
white-space: pre-wrap;
}
/* --- DESIGN INTERNE : VIDEOTHEQUE --- */
.pub-tech-badges {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-bottom: 1.5rem;
}
.tech-pill {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
padding: 0.38rem 0.85rem;
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
color: #dddddd;
letter-spacing: 0.02em;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.tech-pill.format-gold {
background: var(--gold-dim);
border-color: rgba(201, 168, 76, 0.3);
color: var(--gold);
}
.pub-tech-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
background: #141415;
border: 1px solid var(--border);
padding: 1.1rem 1.3rem;
border-radius: 8px;
margin-bottom: 1.8rem;
}
.tech-meta-item { display: flex; flex-direction: column; gap: 0.25rem; }
.tech-meta-label { font-size: 0.75rem; text-transform: uppercase; color: #c0c0cc; letter-spacing: 0.05em; font-weight: 500; }
.tech-meta-value { font-size: 0.9rem; color: var(--text); font-weight: 500; }
.pub-synopsis-box h4 {
color: var(--text); margin-bottom: 0.6rem; font-size: 0.85rem;
font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
}
.pub-synopsis-box p { color: #e0e0e0; font-size: 0.95rem; line-height: 1.7; white-space: pre-wrap; }
/* --- FIN DE PAGE & ACCESSOIRES --- */
footer {
margin-top: 6rem; border-top: 1px solid var(--border);
padding-top: 2rem; text-align: center; color: var(--muted);
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em;
}
.empty-state { text-align: center; padding: 6rem 1rem; color: var(--muted); }
.empty-state i { font-size: 3.5rem; color: #444450; margin-bottom: 1.2rem; display: block; }
/* ==========================================================================
COMPORTEMENT RESPONSIVE MOBILE (APPLICATION NATIVE APP-LIKE)
========================================================================== */
@media (max-width: 720px) {
body { padding: 2rem 1rem; }
.site-title { font-size: 2.6rem; }
.detail-overlay { padding: 0; }
.detail-modal {
flex-direction: column;
gap: 1.5rem;
padding: 4.5rem 1.5rem 2.5rem 1.5rem;
max-height: 100vh;
height: 100vh;
border-radius: 0;
border: none;
}
.detail-poster {
margin: 0 auto;
width: 170px;
flex: 0 0 auto;
}
.detail-header-block {
text-align: center;
padding-bottom: 1.5rem;
}
.detail-title { font-size: 2rem; }
.detail-stars { justify-content: center; display: flex; }
.pub-review-card { padding: 1.8rem 1.3rem; }
.pub-tech-grid { grid-template-columns: 1fr; gap: 0.9rem; }
.pub-tabs { width: 100%; justify-content: center; }
.tab-btn { padding: 0.6rem 1.1rem; font-size: 0.85rem; }
}
/* ==========================================================================
FILTRE PAR NOTES (ÉTOILES)
========================================================================== */
.rating-filter-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.rf-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #ccccda;
font-weight: 500;
}
.rf-buttons {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
justify-content: center;
}
.rating-filter-btn {
background: #1e1e1e;
border: 1px solid #3a3a3a;
border-radius: 8px;
padding: 0.45rem 0.85rem;
cursor: pointer;
display: flex;
gap: 1px;
align-items: center;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.rating-filter-btn:hover {
border-color: var(--gold);
background: var(--gold-dim);
}
.rating-filter-btn.active {
border-color: var(--gold);
background: var(--gold-dim);
box-shadow: 0 0 12px var(--gold-glow);
}
.rf-star {
font-size: 0.85rem;
color: #7a7a88;
transition: color 0.2s ease, text-shadow 0.2s ease;
line-height: 1;
}
.rating-filter-btn:hover .rf-star,
.rating-filter-btn.active .rf-star {
color: #FFD700;
text-shadow: 0 0 6px rgba(255, 215, 0, 0.5);
}
/* On hover: light up stars progressively via sibling trick */
.rating-filter-btn:not(.active):hover .rf-star:not(.filled) {
color: rgba(255, 215, 0, 0.35);
}
/* Active state: filled stars glow, empty ones stay dim */
.rating-filter-btn.active .rf-star.filled {
color: #FFD700;
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
}
.rating-filter-btn.active .rf-star:not(.filled) {
color: #555560;
text-shadow: none;
}
@media (max-width: 720px) {
.rating-filter-bar { gap: 0.7rem; }
.rf-label { display: none; }
.rf-star { font-size: 0.8rem; }
.rating-filter-btn { padding: 0.4rem 0.65rem; }
}
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Cinéma</title>
<link rel="stylesheet" href="css/public.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
</head>
<body>
<div id="page-public" class="page active">
<div class="site-wrap">
<header class="site-header">
<h1 class="site-title">Mon <span>Ciné</span>ma</h1>
<p class="site-subtitle">Journal de critiques &middot; Mes films vus</p>
</header>
<div class="pub-tabs-container">
<div class="pub-tabs">
<button id="tab-pub-critiques" class="tab-btn active" onclick="switchPubTab('critique')">
<i class="ti ti-message-star"></i> Mes Critiques
</button>
<button id="tab-pub-videotheque" class="tab-btn" onclick="switchPubTab('videotheque')">
<i class="ti ti-device-tv"></i> Ma Vidéothèque
</button>
</div>
</div>
<div class="rating-filter-bar" id="rating-filter-bar">
<span class="rf-label">Filtrer :</span>
<div class="rf-buttons">
<button class="rating-filter-btn" data-stars="1" onclick="filterByRating(1)" title="1 étoile">
<span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span>
</button>
<button class="rating-filter-btn" data-stars="2" onclick="filterByRating(2)" title="2 étoiles">
<span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span>
</button>
<button class="rating-filter-btn" data-stars="3" onclick="filterByRating(3)" title="3 étoiles">
<span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span>
</button>
<button class="rating-filter-btn" data-stars="4" onclick="filterByRating(4)" title="4 étoiles">
<span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span>
</button>
<button class="rating-filter-btn" data-stars="5" onclick="filterByRating(5)" title="5 étoiles">
<span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span><span class="rf-star"></span>
</button>
</div>
</div>
<div class="pub-toolbar">
<span class="count" id="count-label">0 critique</span>
<a href="admin/login.html" class="btn-admin-link">
<i class="ti ti-settings"></i> Administration
</a>
</div>
<div class="grid" id="grid"></div>
<div class="empty-state" id="empty-state" style="display:none">
<i class="ti ti-movie" aria-hidden="true"></i>
<p>Aucune critique pour l'instant.</p>
</div>
<footer>Mon Cinéma &mdash; Journal personnel</footer>
</div>
</div>
<div class="detail-overlay" id="detail-overlay" role="dialog" aria-modal="true" onclick="if(event.target===this) closeDetail()">
<div class="detail-modal" id="detail-modal-layout">
<button class="modal-close" onclick="closeDetail()" aria-label="Fermer"><i class="ti ti-x"></i></button>
<div class="detail-poster" id="d-poster-wrap">
<img id="d-poster" src="" alt="Affiche du film" />
</div>
<div class="detail-content">
<header class="detail-header-block">
<h2 class="detail-title" id="d-title"></h2>
<p class="detail-meta" id="d-meta"></p>
<div class="detail-stars" id="d-stars"></div>
</header>
<div class="detail-dynamic-body" id="d-review"></div>
</div>
</div>
</div>
<script src="js/public.js"></script>
</body>
</html>
+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();
});