Premier import de mon site Mon Cinéma
This commit is contained in:
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 · 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 — 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
@@ -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