all
This commit is contained in:
+168
-175
@@ -3,239 +3,232 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Administration — Mon Cinéma</title>
|
||||
<link rel="stylesheet" href="../css/public.css">
|
||||
<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">
|
||||
|
||||
<!-- BANNIÈRE SÉCURITÉ (premier lancement) -->
|
||||
<div class="security-banner" id="security-banner" style="display:none;">
|
||||
<span><i class="ti ti-alert-triangle"></i> <strong>Attention :</strong> Aucun mot de passe n'est défini. Votre espace admin est accessible à tous !</span>
|
||||
<button onclick="openPasswordModal()">Définir un mot de passe</button>
|
||||
</div>
|
||||
|
||||
<!-- EN-TÊTE ADMIN -->
|
||||
<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>
|
||||
<h1 class="admin-title">Espace <span>Admin</span></h1>
|
||||
<p id="admin-subtitle" style="color:var(--muted); font-size:0.82rem; margin-top:0.3rem;">Gestion de vos critiques de films</p>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<button class="btn btn-outline" onclick="openConfigModal()"><i class="ti ti-key"></i> Clé TMDB</button>
|
||||
<button class="btn btn-outline" onclick="openPasswordModal()"><i class="ti ti-lock"></i> Mot de passe</button>
|
||||
<button class="btn btn-danger" onclick="logout()"><i class="ti ti-logout"></i> Quitter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ONGLETS -->
|
||||
<div class="admin-tabs">
|
||||
<button class="tab-btn active" id="tab-critiques" onclick="switchTab('critique')">
|
||||
<i class="ti ti-blockquote"></i> Mes Critiques
|
||||
<button class="tab-btn active" id="btn-tab-critique" onclick="switchAdminTab('critique')">
|
||||
<i class="ti ti-message-star"></i> Critiques
|
||||
</button>
|
||||
<button class="tab-btn" id="tab-videotheque" onclick="switchTab('videotheque')">
|
||||
<i class="ti ti-disc"></i> Ma Vidéothèque
|
||||
<button class="tab-btn" id="btn-tab-videotheque" onclick="switchAdminTab('videotheque')">
|
||||
<i class="ti ti-device-tv"></i> 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>
|
||||
<!-- IMPORT CSV (Letterboxd) -->
|
||||
<div id="import-section" style="background:var(--surface2); border:2px dashed var(--border); border-radius:8px; padding:1.8rem; text-align:center; margin-bottom:1.8rem;">
|
||||
<i class="ti ti-file-upload" style="font-size:2.2rem; color:var(--gold); display:block; margin-bottom:0.6rem;"></i>
|
||||
<p style="margin-bottom:1rem; font-size:0.9rem; color:var(--muted);">Importez vos exports <strong style="color:var(--text);">Letterboxd</strong> (.csv)</p>
|
||||
<input type="file" id="csv-file-input" accept=".csv" style="display:none;" onchange="handleCsvUpload(this)">
|
||||
<button class="btn btn-outline" onclick="document.getElementById('csv-file-input').click()">
|
||||
<i class="ti ti-file-spreadsheet"></i> Choisir un fichier .csv
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- BARRE D'ACTIONS EN MASSE -->
|
||||
<div class="bulk-actions-bar" id="bulk-actions-bar" style="display:none;">
|
||||
<span style="font-size:0.88rem; color:var(--gold); font-weight:500;">
|
||||
<i class="ti ti-checkbox"></i> <span id="bulk-count">0</span> élément(s) sélectionné(s)
|
||||
</span>
|
||||
<button class="btn btn-danger" onclick="executeBulkDelete()">
|
||||
<i class="ti ti-trash"></i> Supprimer la sélection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TOOLBAR -->
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<span class="count" id="admin-count-label">Chargement…</span>
|
||||
<button class="btn btn-gold" onclick="openAddModal()"><i class="ti ti-plus"></i> Ajouter</button>
|
||||
</div>
|
||||
|
||||
<!-- TABLEAU -->
|
||||
<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 style="width:42px; text-align:center;">
|
||||
<input type="checkbox" id="select-all-checkbox" onclick="toggleSelectAll(this)">
|
||||
</th>
|
||||
<th style="width:50px">Affiche</th>
|
||||
<th style="width:56px; text-align:center;">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>
|
||||
<th id="th-dynamic">Note</th>
|
||||
<th style="width:110px; text-align:center;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-tbody"></tbody>
|
||||
<tbody id="admin-table-body"></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>
|
||||
</div><!-- /.admin-wrap -->
|
||||
|
||||
<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" />
|
||||
<!-- ══════════════════════════════════════════
|
||||
MODALE : AJOUTER / MODIFIER UN FILM
|
||||
══════════════════════════════════════════ -->
|
||||
<div class="overlay" id="admin-modal" onclick="if(event.target===this) closeAdminModal()">
|
||||
<div class="modal" style="max-width:600px;">
|
||||
<button class="modal-close" onclick="closeAdminModal()"><i class="ti ti-x"></i></button>
|
||||
<h3 class="modal-h" id="modal-form-title">Ajouter une œuvre</h3>
|
||||
|
||||
<form id="film-form" onsubmit="saveFilmForm(event)">
|
||||
<input type="hidden" id="f-id">
|
||||
|
||||
<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>
|
||||
<label>Titre du film *</label>
|
||||
<input type="text" id="f-title" required autocomplete="off">
|
||||
</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" />
|
||||
<label>Année</label>
|
||||
<input type="number" id="f-year" min="1800" max="2100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="f-year">Année</label>
|
||||
<input id="f-year" type="text" placeholder="ex : 2001" maxlength="4" />
|
||||
<label>Réalisateur</label>
|
||||
<input type="text" id="f-director">
|
||||
</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>
|
||||
<div class="form-group">
|
||||
<label>URL de l'affiche</label>
|
||||
<input type="url" id="f-poster" placeholder="https://image.tmdb.org/…">
|
||||
</div>
|
||||
|
||||
<!-- Champs spécifiques CRITIQUE -->
|
||||
<div id="form-critique-fields">
|
||||
<div class="form-group">
|
||||
<label>Note (1 à 5 étoiles)</label>
|
||||
<select id="f-rating">
|
||||
<option value="5">★★★★★ (5/5)</option>
|
||||
<option value="4">★★★★☆ (4/5)</option>
|
||||
<option value="3" selected>★★★☆☆ (3/5)</option>
|
||||
<option value="2">★★☆☆☆ (2/5)</option>
|
||||
<option value="1">★☆☆☆☆ (1/5)</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 class="form-group">
|
||||
<label>Critique</label>
|
||||
<textarea id="f-review" rows="5"></textarea>
|
||||
</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)" />
|
||||
<label>Plateforme / Mode de visionnage</label>
|
||||
<input type="text" id="f-streaming" placeholder="Ex : Canal+, Cinéma, Blu-ray…">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="form-group-critique-fields">
|
||||
<!-- Champs spécifiques VIDÉOTHÈQUE -->
|
||||
<div id="form-videotheque-fields" style="display:none;">
|
||||
<div class="form-row">
|
||||
<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>
|
||||
<label>Format physique</label>
|
||||
<input type="text" id="f-format" placeholder="4K Ultra HD, Blu-ray, DVD…">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Durée (minutes)</label>
|
||||
<input type="number" id="f-length">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Éditeur</label>
|
||||
<input type="text" id="f-publisher">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Format image</label>
|
||||
<input type="text" id="f-aspect" placeholder="2.39:1, 1.85:1…">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Code barre (EAN)</label>
|
||||
<input type="text" id="f-ean">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nombre de disques</label>
|
||||
<input type="number" id="f-discs" value="1" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="f-review">Ma critique</label>
|
||||
<textarea id="f-review" placeholder="Écris ta critique ici…"></textarea>
|
||||
<label>Synopsis / Notes d'édition</label>
|
||||
<textarea id="f-description" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-save" onclick="saveFilm()">Enregistrer</button>
|
||||
<button type="submit" class="btn-save"><i class="ti ti-device-floppy"></i> Enregistrer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/admin.js?v=4"></script>
|
||||
|
||||
<!-- ══════════════════════════════════════════
|
||||
MODALE : CLÉ API TMDB
|
||||
══════════════════════════════════════════ -->
|
||||
<div class="overlay" id="config-modal" onclick="if(event.target===this) closeConfigModal()">
|
||||
<div class="modal" style="max-width:440px;">
|
||||
<button class="modal-close" onclick="closeConfigModal()"><i class="ti ti-x"></i></button>
|
||||
<h3 class="modal-h"><i class="ti ti-key"></i> Clé API TMDB</h3>
|
||||
<div class="api-notice">
|
||||
Obtenez votre clé gratuite sur <a href="https://www.themoviedb.org/settings/api" target="_blank">themoviedb.org</a>.
|
||||
Elle sera chiffrée en base de données.
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Clé API (v3)</label>
|
||||
<input type="password" id="tmdb-key-input" placeholder="Saisir la clé…">
|
||||
</div>
|
||||
<button class="btn-save" onclick="saveTmdbKey()">Sauvegarder</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ══════════════════════════════════════════
|
||||
MODALE : MOT DE PASSE
|
||||
══════════════════════════════════════════ -->
|
||||
<div class="overlay" id="password-modal" onclick="if(event.target===this) closePasswordModal()">
|
||||
<div class="modal" style="max-width:400px;">
|
||||
<button class="modal-close" onclick="closePasswordModal()"><i class="ti ti-x"></i></button>
|
||||
<h3 class="modal-h"><i class="ti ti-lock"></i> Changer le mot de passe</h3>
|
||||
<div class="form-group">
|
||||
<label>Nouveau mot de passe</label>
|
||||
<input type="password" id="new-password-input" placeholder="Minimum 4 caractères">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirmation</label>
|
||||
<input type="password" id="new-password-confirm" placeholder="Répétez le mot de passe">
|
||||
</div>
|
||||
<p id="pwd-error" style="color:#c0392b; font-size:0.82rem; display:none; margin-bottom:0.8rem;"></p>
|
||||
<button class="btn-save" onclick="saveNewPassword()">Mettre à jour</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="../js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+105
-6
@@ -1,16 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr"><head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
<title>Login - Backoffice</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" />
|
||||
|
||||
<div id="login-form-block">
|
||||
<input type="password" id="login-pwd" placeholder="Mot de passe" onkeydown="if(event.key==='Enter') doLogin()" />
|
||||
<p class="login-err" id="login-err">Mot de passe incorrect.</p>
|
||||
<button class="btn btn-gold" onclick="doLogin()">Accéder</button>
|
||||
</div>
|
||||
|
||||
<div id="blank-setup-block" style="display:none; text-align:center;">
|
||||
<p style="color:var(--muted, #aaa); font-size:0.85rem; margin-bottom:1.5rem; line-height:1.6;">
|
||||
Aucun mot de passe configuré sur le serveur.<br>Connectez-vous directement pour en définir un.
|
||||
</p>
|
||||
<button class="btn btn-gold" onclick="doLoginBlank()" style="width:100%;">Se connecter sans mot de passe</button>
|
||||
</div>
|
||||
<script src="../js/admin.js"></script>
|
||||
</body></html>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = '../api.php';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Si déjà connecté, rediriger vers le dashboard
|
||||
if (localStorage.getItem('token')) {
|
||||
window.location.href = 'dashboard.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const formBlock = document.getElementById('login-form-block');
|
||||
const blankBlock = document.getElementById('blank-setup-block');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=check_security_status`);
|
||||
const data = await res.json();
|
||||
if (data.is_blank) {
|
||||
formBlock.style.display = 'none';
|
||||
blankBlock.style.display = 'block';
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn("API indisponible — mode secours activé.");
|
||||
formBlock.style.display = 'none';
|
||||
blankBlock.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
async function doLoginBlank() {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: '' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
const pwd = document.getElementById('login-pwd').value;
|
||||
const errEl = document.getElementById('login-err');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: pwd })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
|
||||
// Première connexion : inviter à définir un mot de passe
|
||||
if (data.blank) {
|
||||
const newPwd = prompt("Première connexion ! Définissez votre mot de passe administrateur :");
|
||||
if (newPwd && newPwd.trim().length >= 4) {
|
||||
await fetch(`${API_URL}?action=setup_admin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': data.token
|
||||
},
|
||||
body: JSON.stringify({ password: newPwd.trim() })
|
||||
});
|
||||
}
|
||||
}
|
||||
window.location.href = 'dashboard.html';
|
||||
} else {
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
define('ENCRYPTION_KEY', 'MaCleSecreteSuperRobuste123!');
|
||||
|
||||
$host = 'localhost';
|
||||
$db = 'mon_cinema';
|
||||
$user = 'root';
|
||||
$pass = '';
|
||||
$charset = 'utf8mb4';
|
||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||
]);
|
||||
} catch (\PDOException $e) {
|
||||
echo json_encode(["error" => "Erreur BDD : " . $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Chiffrement ──────────────────────────────────────────────────
|
||||
function encryptData($data) {
|
||||
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
|
||||
$encrypted = openssl_encrypt($data, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv);
|
||||
return base64_encode($encrypted . '::' . $iv);
|
||||
}
|
||||
|
||||
function decryptData($data) {
|
||||
if (!$data) return '';
|
||||
list($encrypted_data, $iv) = explode('::', base64_decode($data), 2);
|
||||
return openssl_decrypt($encrypted_data, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv);
|
||||
}
|
||||
|
||||
// ── Génération d'ID stable et compatible bigint (PHP 32 et 64 bits) ──
|
||||
// Produit un entier sur 9 chiffres (< 2^31) → safe partout
|
||||
// crc32() PHP retourne un int signé 32 bits → on force positif avec & 0x7FFFFFFF
|
||||
// puis on module pour rester dans une plage confortable sans collision sur ~50k films
|
||||
function makeStableId($title, $year) {
|
||||
$key = strtolower(trim($title)) . '|' . trim($year);
|
||||
$hash = abs(crc32($key)); // abs() = toujours positif
|
||||
// Reste dans [100000000, 2099999999] → 9-10 chiffres, bien < BIGINT MAX
|
||||
return ($hash % 2000000000) + 100000000;
|
||||
}
|
||||
|
||||
// Pour les ajouts manuels : timestamp Unix en secondes (10 chiffres, safe)
|
||||
function makeNewId() {
|
||||
// On ajoute un suffixe aléatoire sur 3 chiffres pour éviter les doublons
|
||||
// si deux films sont ajoutés dans la même seconde
|
||||
return intval(time() . rand(100, 999));
|
||||
// time() = 10 chiffres, rand = 3 → 13 chiffres total
|
||||
// Mais BIGINT MAX = 9223372036854775807 (19 chiffres) → OK même en 64-bit
|
||||
// Et en PHP 32-bit, time() seul (10 chiffres) dépasse INT_MAX...
|
||||
// Solution : utiliser directement crc32 d'un uuid
|
||||
}
|
||||
|
||||
function makeNewIdSafe() {
|
||||
// Génère un ID unique sur 9-10 chiffres safe en 32 ET 64 bits
|
||||
$unique = uniqid('', true) . rand(1000, 9999);
|
||||
return (abs(crc32($unique)) % 2000000000) + 100000000;
|
||||
}
|
||||
|
||||
// ── Conversion note Letterboxd (/5 avec demi-points) → entier 1-5 ──
|
||||
function convertRating($rawRating) {
|
||||
$raw = floatval($rawRating);
|
||||
if ($raw <= 0) return 3; // pas de note → neutre
|
||||
// Arrondi au plus proche, clampé entre 1 et 5
|
||||
$rounded = (int) round($raw);
|
||||
return max(1, min(5, $rounded));
|
||||
}
|
||||
|
||||
// ── Authentification ─────────────────────────────────────────────
|
||||
function checkAuth($pdo) {
|
||||
$stmtCheck = $pdo->query("SELECT COUNT(*) as total FROM users");
|
||||
if ($stmtCheck->fetch()['total'] == 0) return true;
|
||||
|
||||
$token = '';
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
$token = $_SERVER['HTTP_AUTHORIZATION'];
|
||||
} elseif (function_exists('apache_request_headers')) {
|
||||
$headers = apache_request_headers();
|
||||
if (isset($headers['Authorization'])) $token = $headers['Authorization'];
|
||||
}
|
||||
|
||||
$expectedToken = md5(ENCRYPTION_KEY . 'session');
|
||||
if ($token !== $expectedToken) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Accès interdit. Session expirée."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Requête TMDB ─────────────────────────────────────────────────
|
||||
function fetchTmdb($url) {
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'MonCinemaApp/1.0');
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
$opts = [
|
||||
"http" => ["method" => "GET", "header" => "User-Agent: MonCinemaApp/1.0\r\n", "timeout" => 8],
|
||||
"ssl" => ["verify_peer" => false, "verify_peer_name" => false]
|
||||
];
|
||||
return @file_get_contents($url, false, stream_context_create($opts));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
case 'GET':
|
||||
|
||||
if ($action === 'get_films') {
|
||||
$stmtC = $pdo->query("SELECT *, 'critique' AS type FROM critiques ORDER BY created_at DESC");
|
||||
$critiques = $stmtC->fetchAll();
|
||||
$stmtV = $pdo->query("SELECT *, 'videotheque' AS type FROM videotheque ORDER BY created_at DESC");
|
||||
$videotheque = $stmtV->fetchAll();
|
||||
echo json_encode(array_merge($critiques, $videotheque), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
elseif ($action === 'get_tmdb_key_status') {
|
||||
checkAuth($pdo);
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT COUNT(*) as total FROM config
|
||||
WHERE key_name = 'tmdb_api_key'
|
||||
AND key_value IS NOT NULL AND key_value != ''"
|
||||
);
|
||||
$stmt->execute();
|
||||
echo json_encode(["exists" => ($stmt->fetch()['total'] > 0)]);
|
||||
}
|
||||
|
||||
elseif ($action === 'search_tmdb') {
|
||||
checkAuth($pdo);
|
||||
$query = $_GET['query'] ?? '';
|
||||
$stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'");
|
||||
$stmt->execute();
|
||||
$res = $stmt->fetch();
|
||||
$key = $res ? decryptData($res['key_value']) : '';
|
||||
if (!$key) { echo json_encode(["error" => "Clé TMDB non configurée."]); exit; }
|
||||
$url = "https://api.themoviedb.org/3/search/movie?api_key={$key}&query=" . urlencode($query) . "&language=fr-FR";
|
||||
$response = fetchTmdb($url);
|
||||
echo $response ?: json_encode(["results" => []]);
|
||||
}
|
||||
|
||||
elseif ($action === 'check_security_status') {
|
||||
$stmt = $pdo->query("SELECT COUNT(*) as total FROM users");
|
||||
echo json_encode(["is_blank" => ($stmt->fetch()['total'] == 0)]);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
case 'POST':
|
||||
|
||||
// ── Import CSV Letterboxd ─────────────────────────────
|
||||
if ($action === 'import_csv') {
|
||||
checkAuth($pdo);
|
||||
|
||||
if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Fichier CSV invalide ou manquant."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer la clé TMDB (optionnelle)
|
||||
$stmtK = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'");
|
||||
$stmtK->execute();
|
||||
$resK = $stmtK->fetch();
|
||||
$tmdbKey = $resK ? decryptData($resK['key_value']) : '';
|
||||
|
||||
$file = $_FILES['csv_file']['tmp_name'];
|
||||
$handle = fopen($file, 'r');
|
||||
if (!$handle) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "Impossible d'ouvrir le fichier."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Gestion BOM UTF-8 (présent ou non)
|
||||
$bom = fread($handle, 3);
|
||||
if ($bom !== "\xEF\xBB\xBF") rewind($handle);
|
||||
|
||||
// Lire l'en-tête
|
||||
$header = fgetcsv($handle, 0, ',', '"');
|
||||
if (!$header) {
|
||||
fclose($handle);
|
||||
echo json_encode(["error" => "Fichier CSV vide."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Nettoyer les noms de colonnes (espaces, BOM résiduels)
|
||||
$header = array_map(function($col) {
|
||||
return trim(preg_replace('/[\x00-\x1F\x7F\xEF\xBB\xBF]/u', '', $col));
|
||||
}, $header);
|
||||
|
||||
$colName = array_search('Name', $header);
|
||||
$colYear = array_search('Year', $header);
|
||||
$colRating = array_search('Rating', $header);
|
||||
$colReview = array_search('Review', $header); // absent dans ratings.csv → false
|
||||
|
||||
if ($colName === false || $colYear === false || $colRating === false) {
|
||||
fclose($handle);
|
||||
echo json_encode([
|
||||
"error" => "Format non reconnu. Colonnes attendues : Name, Year, Rating. Colonnes trouvées : " . implode(', ', $header)
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$importedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',', '"')) !== false) {
|
||||
// Ignorer les lignes trop courtes
|
||||
if (count($row) <= max($colName, $colYear, $colRating)) continue;
|
||||
|
||||
$title = trim($row[$colName]);
|
||||
$year = trim($row[$colYear]);
|
||||
$rating = convertRating($row[$colRating]);
|
||||
$review = ($colReview !== false && isset($row[$colReview])) ? trim($row[$colReview]) : '';
|
||||
|
||||
if (empty($title)) continue;
|
||||
|
||||
// ID stable basé sur titre+année, safe en 32 ET 64 bits
|
||||
$id = makeStableId($title, $year);
|
||||
|
||||
// Récupérer données existantes pour ne pas écraser
|
||||
$stmtCheck = $pdo->prepare("SELECT review, poster, director FROM critiques WHERE id = :id");
|
||||
$stmtCheck->execute([':id' => $id]);
|
||||
$existing = $stmtCheck->fetch();
|
||||
|
||||
$poster = $existing['poster'] ?? '';
|
||||
$director = $existing['director'] ?? '';
|
||||
|
||||
// Conserver la critique existante si le CSV n'en fournit pas
|
||||
if (empty($review) && !empty($existing['review'])) {
|
||||
$review = $existing['review'];
|
||||
}
|
||||
|
||||
// Enrichissement TMDB si données manquantes et clé disponible
|
||||
if ((empty($poster) || empty($director)) && !empty($tmdbKey)) {
|
||||
$apiUrl = "https://api.themoviedb.org/3/search/movie"
|
||||
. "?api_key={$tmdbKey}"
|
||||
. "&query=" . urlencode($title)
|
||||
. ($year ? "&year={$year}" : '')
|
||||
. "&language=fr-FR";
|
||||
$tmdbRes = fetchTmdb($apiUrl);
|
||||
|
||||
if ($tmdbRes) {
|
||||
$tmdbData = json_decode($tmdbRes, true);
|
||||
if (!empty($tmdbData['results'][0])) {
|
||||
$movieData = $tmdbData['results'][0];
|
||||
if (empty($poster) && !empty($movieData['poster_path'])) {
|
||||
$poster = "https://image.tmdb.org/t/p/w300" . $movieData['poster_path'];
|
||||
}
|
||||
if (empty($director) && !empty($movieData['id'])) {
|
||||
$creditsRes = fetchTmdb("https://api.themoviedb.org/3/movie/{$movieData['id']}/credits?api_key={$tmdbKey}");
|
||||
if ($creditsRes) {
|
||||
$creditsData = json_decode($creditsRes, true);
|
||||
foreach ($creditsData['crew'] ?? [] as $member) {
|
||||
if ($member['job'] === 'Director') {
|
||||
$director = $member['name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"REPLACE INTO critiques (id, title, year, director, rating, review, poster, streaming)
|
||||
VALUES (:id, :title, :year, :director, :rating, :review, :poster, '')"
|
||||
);
|
||||
$stmt->execute([
|
||||
':id' => $id,
|
||||
':title' => $title,
|
||||
':year' => $year,
|
||||
':director' => $director,
|
||||
':rating' => $rating,
|
||||
':review' => $review,
|
||||
':poster' => $poster,
|
||||
]);
|
||||
$importedCount++;
|
||||
} catch (\PDOException $e) {
|
||||
$errors[] = "'{$title}' : " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$result = ["success" => true, "imported" => $importedCount];
|
||||
if (!empty($errors)) $result['warnings'] = array_slice($errors, 0, 10);
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Lire le body JSON ─────────────────────────────────
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
// ── Connexion ─────────────────────────────────────────
|
||||
if ($action === 'login') {
|
||||
$stmtCheck = $pdo->query("SELECT COUNT(*) as total FROM users");
|
||||
if ($stmtCheck->fetch()['total'] == 0) {
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"token" => md5(ENCRYPTION_KEY . 'session'),
|
||||
"blank" => true
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = 'admin'");
|
||||
$stmt->execute();
|
||||
$userRow = $stmt->fetch();
|
||||
|
||||
if ($userRow && password_verify($data['password'] ?? '', $userRow['password_hash'])) {
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"token" => md5(ENCRYPTION_KEY . 'session'),
|
||||
"blank" => false
|
||||
]);
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Mot de passe incorrect."]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sauvegarde film ───────────────────────────────────
|
||||
elseif ($action === 'save_film') {
|
||||
checkAuth($pdo);
|
||||
$type = $data['type'] ?? 'critique';
|
||||
$id = (!empty($data['id'])) ? intval($data['id']) : makeNewIdSafe();
|
||||
|
||||
if ($type === 'critique') {
|
||||
$stmt = $pdo->prepare(
|
||||
"REPLACE INTO critiques (id, title, year, director, rating, review, poster, streaming)
|
||||
VALUES (:id, :title, :year, :director, :rating, :review, :poster, :streaming)"
|
||||
);
|
||||
$stmt->execute([
|
||||
':id' => $id,
|
||||
':title' => $data['title'] ?? '',
|
||||
':year' => $data['year'] ?? '',
|
||||
':director' => $data['director'] ?? '',
|
||||
':rating' => max(1, min(5, intval($data['rating'] ?? 3))),
|
||||
':review' => $data['review'] ?? '',
|
||||
':poster' => $data['poster'] ?? '',
|
||||
':streaming' => $data['streaming'] ?? '',
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"REPLACE INTO videotheque
|
||||
(id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description)
|
||||
VALUES (:id, :title, :year, :director, :poster, :format, :length, :publisher, :ean_isbn13, :number_of_discs, :aspect_ratio, :description)"
|
||||
);
|
||||
$stmt->execute([
|
||||
':id' => $id,
|
||||
':title' => $data['title'] ?? '',
|
||||
':year' => $data['year'] ?? '',
|
||||
':director' => $data['director'] ?? '',
|
||||
':poster' => $data['poster'] ?? '',
|
||||
':format' => $data['format'] ?? '',
|
||||
':length' => !empty($data['length']) ? intval($data['length']) : null,
|
||||
':publisher' => $data['publisher'] ?? '',
|
||||
':ean_isbn13' => $data['ean_isbn13'] ?? '',
|
||||
':number_of_discs' => !empty($data['number_of_discs']) ? intval($data['number_of_discs']) : 1,
|
||||
':aspect_ratio' => $data['aspect_ratio'] ?? '',
|
||||
':description' => $data['description'] ?? '',
|
||||
]);
|
||||
}
|
||||
echo json_encode(["success" => true]);
|
||||
}
|
||||
|
||||
// ── Sauvegarde clé TMDB ───────────────────────────────
|
||||
elseif ($action === 'save_tmdb_key') {
|
||||
checkAuth($pdo);
|
||||
$stmt = $pdo->prepare(
|
||||
"REPLACE INTO config (key_name, key_value) VALUES ('tmdb_api_key', :val)"
|
||||
);
|
||||
$stmt->execute([':val' => encryptData($data['tmdb_key'] ?? '')]);
|
||||
echo json_encode(["success" => true]);
|
||||
}
|
||||
|
||||
// ── Init compte admin ─────────────────────────────────
|
||||
elseif ($action === 'setup_admin') {
|
||||
$pwd = $data['password'] ?? '';
|
||||
if (strlen($pwd) < 4) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Mot de passe trop court (min. 4 caractères)."]);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
"REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)"
|
||||
);
|
||||
$stmt->execute([':pass' => password_hash($pwd, PASSWORD_BCRYPT)]);
|
||||
echo json_encode(["success" => "Compte admin initialisé."]);
|
||||
}
|
||||
|
||||
// ── Changement mot de passe ───────────────────────────
|
||||
elseif ($action === 'update_password') {
|
||||
checkAuth($pdo);
|
||||
$newPwd = $data['new_password'] ?? '';
|
||||
if (strlen($newPwd) < 4) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Mot de passe trop court (min. 4 caractères)."]);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
"REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)"
|
||||
);
|
||||
$stmt->execute([':pass' => password_hash($newPwd, PASSWORD_BCRYPT)]);
|
||||
echo json_encode(["success" => "Mot de passe mis à jour."]);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
case 'DELETE':
|
||||
|
||||
if ($action === 'delete_film') {
|
||||
checkAuth($pdo);
|
||||
$id = $_GET['id'] ?? 0;
|
||||
$type = $_GET['type'] ?? 'critique';
|
||||
$table = ($type === 'videotheque') ? 'videotheque' : 'critiques';
|
||||
$stmt = $pdo->prepare("DELETE FROM {$table} WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
echo json_encode(["success" => true]);
|
||||
}
|
||||
|
||||
elseif ($action === 'delete_multiple_films') {
|
||||
checkAuth($pdo);
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$ids = $input['ids'] ?? [];
|
||||
$type = $_GET['type'] ?? 'critique';
|
||||
$table = ($type === 'videotheque') ? 'videotheque' : 'critiques';
|
||||
|
||||
if (empty($ids) || !is_array($ids)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Aucun identifiant fourni."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ids = array_map('intval', $ids);
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmt = $pdo->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})");
|
||||
$stmt->execute($ids);
|
||||
echo json_encode(["success" => true, "deleted_count" => $stmt->rowCount()]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
+387
-112
@@ -1,145 +1,420 @@
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
admin.css — Backoffice Mon Cinéma
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
* { 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);
|
||||
--gold: #C9A84C;
|
||||
--gold-light: #E8C97A;
|
||||
--gold-dim: rgba(201, 168, 76, 0.08);
|
||||
--dark: #0E0E0E;
|
||||
--surface: #1A1A1A;
|
||||
--surface2: #252525;
|
||||
--text: #F0ECE3;
|
||||
--text-secondary: #C8C4BE;
|
||||
--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 ── */
|
||||
body {
|
||||
background: var(--dark);
|
||||
color: var(--text);
|
||||
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem 4rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Visibilité des 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; }
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
LAYOUT ADMIN
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.admin-wrap { max-width: 960px; margin: 0 auto; }
|
||||
|
||||
.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; }
|
||||
.admin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 1.8rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.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; }
|
||||
.admin-title { font-family: 'Playfair Display', serif; font-size: 1.8rem; }
|
||||
.admin-title span { color: var(--gold); font-style: italic; }
|
||||
.admin-actions { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
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; }
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
ONGLETS ADMIN
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── 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; }
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
padding: 0.55rem 1.2rem;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.03); }
|
||||
.tab-btn.active { color: var(--gold); background: rgba(201,168,76,0.08); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
BOUTONS GÉNÉRAUX
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.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: 3px;
|
||||
letter-spacing: 0.04em;
|
||||
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.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 { background: transparent; border: 1px solid rgba(201,168,76,0.3); 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 */
|
||||
.btn-primary { background: var(--gold); color: var(--dark); font-weight: 600; }
|
||||
.btn-primary:hover { background: var(--gold-light); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
TABLEAU
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.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 {
|
||||
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; }
|
||||
.thumb {
|
||||
width: 36px; height: 54px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.thumb-ph {
|
||||
width: 36px; height: 54px;
|
||||
background: var(--surface2);
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-size: 1rem;
|
||||
border: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.tbl-stars { color: var(--gold); font-size: 0.8rem; letter-spacing: 0.05em; }
|
||||
|
||||
.tbl-actions { display: flex; gap: 0.4rem; justify-content: center; }
|
||||
.tbl-actions button {
|
||||
background: none;
|
||||
border: 1px solid #444;
|
||||
cursor: pointer;
|
||||
color: #B0ACAA;
|
||||
font-size: 0.9rem;
|
||||
padding: 5px 7px;
|
||||
border-radius: 3px;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
.tbl-actions button:hover { color: var(--gold); border-color: var(--gold); }
|
||||
.tbl-actions button.del:hover { color: var(--red); border-color: var(--red); }
|
||||
|
||||
.badge-format {
|
||||
background: var(--gold-dim);
|
||||
color: var(--gold);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(201,168,76,0.25);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
BARRE D'ACTIONS EN MASSE
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.bulk-actions-bar {
|
||||
background: var(--gold-dim);
|
||||
border: 1px solid rgba(201,168,76,0.3);
|
||||
padding: 0.8rem 1.2rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
BANNIÈRE SÉCURITÉ
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.security-banner {
|
||||
background: #7f1d1d;
|
||||
border: 1px solid #f87171;
|
||||
color: #fca5a5;
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.security-banner button {
|
||||
background: #f87171;
|
||||
color: #7f1d1d;
|
||||
border: none;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-weight: 700;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.security-banner button:hover { background: #fca5a5; }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
MODALES — Overlay + panneau
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.modal {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
max-height: 92vh;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
|
||||
.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; }
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.modal-close:hover { color: var(--text); background: rgba(255,255,255,0.08); }
|
||||
|
||||
.modal-h {
|
||||
font-family: 'Playfair Display', serif;
|
||||
font-size: 1.4rem;
|
||||
color: var(--gold);
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
FORMULAIRES DANS LES MODALES
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.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 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 select,
|
||||
.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: 4px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select: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 {
|
||||
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: 600;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.8rem;
|
||||
transition: background 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.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
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.api-notice {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.9rem 1.1rem;
|
||||
margin-bottom: 1.2rem;
|
||||
font-size: 0.82rem;
|
||||
color: #C0BCB8;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.api-notice a { color: var(--gold); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
PAGE LOGIN
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.login-wrap {
|
||||
max-width: 360px;
|
||||
margin: 7rem auto;
|
||||
padding: 2rem 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.login-wrap h2 {
|
||||
font-family: 'Playfair Display', serif;
|
||||
color: var(--gold);
|
||||
margin-bottom: 1.8rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.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.7rem 0.85rem;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
margin-bottom: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.login-wrap input:focus { border-color: var(--gold); }
|
||||
|
||||
.login-err {
|
||||
color: var(--red);
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 0.8rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
RESPONSIVE
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
@media (max-width: 600px) {
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
.admin-header { flex-direction: column; align-items: flex-start; }
|
||||
.admin-actions { width: 100%; }
|
||||
.modal { padding: 1.5rem; }
|
||||
}
|
||||
+2
-1
@@ -178,7 +178,8 @@ body {
|
||||
.card-poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: cover; /* Garantit que l'affiche remplit harmonieusement la carte */
|
||||
display: block; /* Supprime les espaces blancs sous l'image */
|
||||
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// Sécurité : On s'assure que la requête vient bien de la machine locale (Gitea)
|
||||
$local_ips = array('127.0.0.1', '::1');
|
||||
if (!in_array($_SERVER['REMOTE_ADDR'], $local_ips)) {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
die('Accès refusé.');
|
||||
}
|
||||
|
||||
// 1. Définir le chemin absolu vers le dossier de votre site (adapter si nécessaire)
|
||||
$site_dir = 'C:\\wamp64\\www\\mon-petit-cinema';
|
||||
|
||||
// 2. Commande Windows pour changer de disque/dossier ET lancer le git pull
|
||||
// Le "2>&1" permet de capturer les erreurs Windows s'il y en a.
|
||||
$command = "cmd.exe /c cd /d " . escapeshellarg($site_dir) . " && git pull origin main 2>&1";
|
||||
|
||||
// 3. Exécution de la commande
|
||||
$output = shell_exec($command);
|
||||
|
||||
// Affichage du résultat dans les logs du webhook Gitea
|
||||
echo "Mise à jour Windows Git effectuée :\n";
|
||||
echo $output;
|
||||
?>
|
||||
+13
-22
@@ -22,7 +22,7 @@
|
||||
<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')">
|
||||
<button id="tab-pub-videotheques" class="tab-btn" onclick="switchPubTab('videotheque')">
|
||||
<i class="ti ti-device-tv"></i> Ma Vidéothèque
|
||||
</button>
|
||||
</div>
|
||||
@@ -31,26 +31,20 @@
|
||||
<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">
|
||||
<button class="rating-filter-btn" data-stars="5" onclick="filterByRating(5)">
|
||||
<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)">
|
||||
<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)">
|
||||
<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>
|
||||
<span class="count" id="count-label">0 élément</span>
|
||||
<a href="admin/login.html" class="btn-admin-link">
|
||||
<i class="ti ti-settings"></i> Administration
|
||||
</a>
|
||||
@@ -60,8 +54,9 @@
|
||||
|
||||
<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>
|
||||
<p>Aucun contenu trouvé dans cette catégorie.</p>
|
||||
</div>
|
||||
|
||||
<footer>Mon Cinéma — Journal personnel</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,19 +64,15 @@
|
||||
<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" />
|
||||
<img id="d-poster" src="" alt="Affiche" />
|
||||
</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 id="d-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+392
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
/**
|
||||
* =========================================================================
|
||||
* Mon Cinéma - Module d'Administration (admin.js)
|
||||
@@ -280,3 +281,394 @@ function switchTab(tabName) {
|
||||
|
||||
localStorage.setItem('mon-cinema-active-tab', tabName);
|
||||
}
|
||||
=======
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// admin.js — Backoffice Mon Cinéma
|
||||
// Chargé UNIQUEMENT par dashboard.html
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
const API_URL = '../api.php';
|
||||
let allItems = [];
|
||||
let currentAdminTab = 'critique';
|
||||
|
||||
// ── Garde de session (dashboard uniquement) ──────────────────────
|
||||
(function guardSession() {
|
||||
if (!localStorage.getItem('token')) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
})();
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 1. CHARGEMENT & RENDU
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=get_films`);
|
||||
allItems = await res.json();
|
||||
|
||||
// Vérifier si compte sécurisé
|
||||
const secRes = await fetch(`${API_URL}?action=check_security_status`);
|
||||
const secData = await secRes.json();
|
||||
const banner = document.getElementById('security-banner');
|
||||
if (banner) banner.style.display = secData.is_blank ? 'flex' : 'none';
|
||||
|
||||
renderAdminTable();
|
||||
} catch (err) {
|
||||
console.error('Erreur chargement :', err);
|
||||
}
|
||||
}
|
||||
|
||||
function switchAdminTab(tabName) {
|
||||
currentAdminTab = tabName;
|
||||
|
||||
document.getElementById('btn-tab-critique').classList.toggle('active', tabName === 'critique');
|
||||
document.getElementById('btn-tab-videotheque').classList.toggle('active', tabName === 'videotheque');
|
||||
|
||||
const importSection = document.getElementById('import-section');
|
||||
if (importSection) importSection.style.display = tabName === 'critique' ? 'block' : 'none';
|
||||
|
||||
document.getElementById('admin-subtitle').textContent =
|
||||
tabName === 'critique'
|
||||
? 'Gestion de vos critiques de films'
|
||||
: 'Gestion de votre stock physique de films';
|
||||
|
||||
document.getElementById('th-dynamic').textContent = tabName === 'critique' ? 'Note' : 'Format';
|
||||
|
||||
const selectAll = document.getElementById('select-all-checkbox');
|
||||
if (selectAll) selectAll.checked = false;
|
||||
|
||||
renderAdminTable();
|
||||
}
|
||||
|
||||
function renderAdminTable() {
|
||||
const tbody = document.getElementById('admin-table-body');
|
||||
const countLabel = document.getElementById('admin-count-label');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '';
|
||||
const filtered = allItems.filter(item => item.type === currentAdminTab);
|
||||
|
||||
if (countLabel) {
|
||||
countLabel.textContent = `${filtered.length} élément(s) dans cette catégorie`;
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; color:var(--muted); padding:3rem;">
|
||||
<i class="ti ti-folder-off" style="font-size:2rem; display:block; margin-bottom:0.5rem;"></i>
|
||||
Aucun film trouvé dans cette liste.</td></tr>`;
|
||||
updateBulkBarVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(f => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const imgHTML = f.poster
|
||||
? `<img src="${f.poster}" class="thumb" alt="${f.title}">`
|
||||
: `<div class="thumb-ph"><i class="ti ti-photo-off"></i></div>`;
|
||||
|
||||
const dynamicCell = currentAdminTab === 'critique'
|
||||
? `<span class="tbl-stars">${'★'.repeat(f.rating)}<span style="opacity:.25">${'☆'.repeat(5 - f.rating)}</span></span>`
|
||||
: `<span class="badge-format">${f.format || 'Physique'}</span>`;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="text-align:center;">
|
||||
<input type="checkbox" class="film-checkbox" value="${f.id}" onclick="updateBulkBarVisibility()">
|
||||
</td>
|
||||
<td style="text-align:center;">${imgHTML}</td>
|
||||
<td><strong style="color:var(--text);">${f.title}</strong></td>
|
||||
<td style="color:var(--text-secondary);">${f.year || '—'}</td>
|
||||
<td style="color:var(--text-secondary); font-style:italic;">${f.director || 'Inconnu'}</td>
|
||||
<td>${dynamicCell}</td>
|
||||
<td style="text-align:center;">
|
||||
<div class="tbl-actions">
|
||||
<button onclick="openEditModal('${f.id}')" title="Modifier"><i class="ti ti-edit"></i></button>
|
||||
<button class="del" onclick="deleteSingleFilm('${f.id}')" title="Supprimer"><i class="ti ti-trash"></i></button>
|
||||
</div>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
updateBulkBarVisibility();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 2. SÉLECTION EN MASSE
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
function toggleSelectAll(master) {
|
||||
document.querySelectorAll('.film-checkbox').forEach(cb => cb.checked = master.checked);
|
||||
updateBulkBarVisibility();
|
||||
}
|
||||
|
||||
function updateBulkBarVisibility() {
|
||||
const checked = document.querySelectorAll('.film-checkbox:checked');
|
||||
const bulkBar = document.getElementById('bulk-actions-bar');
|
||||
const bulkCount = document.getElementById('bulk-count');
|
||||
const selectAll = document.getElementById('select-all-checkbox');
|
||||
|
||||
if (!bulkBar || !bulkCount) return;
|
||||
|
||||
if (checked.length > 0) {
|
||||
bulkBar.style.display = 'flex';
|
||||
bulkCount.textContent = checked.length;
|
||||
} else {
|
||||
bulkBar.style.display = 'none';
|
||||
if (selectAll) selectAll.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeBulkDelete() {
|
||||
const checked = document.querySelectorAll('.film-checkbox:checked');
|
||||
const idsToDelete = Array.from(checked).map(cb => cb.value);
|
||||
if (!idsToDelete.length) return;
|
||||
|
||||
if (!confirm(`Supprimer définitivement ces ${idsToDelete.length} élément(s) ?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=delete_multiple_films&type=${currentAdminTab}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('token') },
|
||||
body: JSON.stringify({ ids: idsToDelete })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const selectAll = document.getElementById('select-all-checkbox');
|
||||
if (selectAll) selectAll.checked = false;
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert('Erreur suppression groupée : ' + data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Bulk delete :', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 3. SUPPRESSION UNITAIRE
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
async function deleteSingleFilm(id) {
|
||||
if (!confirm('Supprimer définitivement cette œuvre ?')) return;
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=delete_film&id=${id}&type=${currentAdminTab}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': localStorage.getItem('token') }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) loadDashboardData();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 4. MODALES FILM (ouvrir / fermer / remplir)
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
function openAddModal() {
|
||||
document.getElementById('film-form').reset();
|
||||
document.getElementById('f-id').value = '';
|
||||
document.getElementById('modal-form-title').textContent =
|
||||
currentAdminTab === 'critique' ? 'Rédiger une Critique' : 'Ajouter un film physique';
|
||||
|
||||
document.getElementById('form-critique-fields').style.display = currentAdminTab === 'critique' ? 'block' : 'none';
|
||||
document.getElementById('form-videotheque-fields').style.display = currentAdminTab === 'videotheque' ? 'block' : 'none';
|
||||
|
||||
document.getElementById('admin-modal').classList.add('open');
|
||||
}
|
||||
|
||||
function openEditModal(id) {
|
||||
const item = allItems.find(x => String(x.id) === String(id));
|
||||
if (!item) return;
|
||||
|
||||
openAddModal();
|
||||
document.getElementById('modal-form-title').textContent = "Modifier l'œuvre";
|
||||
|
||||
document.getElementById('f-id').value = item.id;
|
||||
document.getElementById('f-title').value = item.title || '';
|
||||
document.getElementById('f-year').value = item.year || '';
|
||||
document.getElementById('f-director').value = item.director || '';
|
||||
document.getElementById('f-poster').value = item.poster || '';
|
||||
|
||||
if (currentAdminTab === 'critique') {
|
||||
document.getElementById('f-rating').value = item.rating || 3;
|
||||
document.getElementById('f-review').value = item.review || '';
|
||||
document.getElementById('f-streaming').value = item.streaming || '';
|
||||
} else {
|
||||
document.getElementById('f-format').value = item.format || '';
|
||||
document.getElementById('f-length').value = item.length || '';
|
||||
document.getElementById('f-publisher').value = item.publisher || '';
|
||||
document.getElementById('f-aspect').value = item.aspect_ratio || '';
|
||||
document.getElementById('f-ean').value = item.ean_isbn13 || '';
|
||||
document.getElementById('f-discs').value = item.number_of_discs || 1;
|
||||
document.getElementById('f-description').value = item.description || '';
|
||||
}
|
||||
}
|
||||
|
||||
function closeAdminModal() {
|
||||
document.getElementById('admin-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 5. SAUVEGARDE FORMULAIRE FILM
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
async function saveFilmForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
type: currentAdminTab,
|
||||
id: document.getElementById('f-id').value || null,
|
||||
title: document.getElementById('f-title').value,
|
||||
year: document.getElementById('f-year').value,
|
||||
director: document.getElementById('f-director').value,
|
||||
poster: document.getElementById('f-poster').value
|
||||
};
|
||||
|
||||
if (currentAdminTab === 'critique') {
|
||||
payload.rating = parseInt(document.getElementById('f-rating').value);
|
||||
payload.review = document.getElementById('f-review').value;
|
||||
payload.streaming = document.getElementById('f-streaming').value;
|
||||
} else {
|
||||
payload.format = document.getElementById('f-format').value;
|
||||
payload.length = document.getElementById('f-length').value;
|
||||
payload.publisher = document.getElementById('f-publisher').value;
|
||||
payload.aspect_ratio = document.getElementById('f-aspect').value;
|
||||
payload.ean_isbn13 = document.getElementById('f-ean').value;
|
||||
payload.number_of_discs = document.getElementById('f-discs').value;
|
||||
payload.description = document.getElementById('f-description').value;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=save_film`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('token') },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeAdminModal();
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert('Erreur lors de l\'enregistrement.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur save :', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 6. IMPORT CSV (Letterboxd)
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
async function handleCsvUpload(input) {
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('csv_file', input.files[0]);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=import_csv`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': localStorage.getItem('token') },
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
alert(`✅ Import réussi ! ${data.imported} films ajoutés ou mis à jour.`);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert('❌ Erreur import : ' + (data.error || 'Erreur inconnue'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur Import CSV :', err);
|
||||
alert('Impossible d\'importer le fichier.');
|
||||
} finally {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 7. MODALE TMDB
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
function openConfigModal() { document.getElementById('config-modal').classList.add('open'); }
|
||||
function closeConfigModal() { document.getElementById('config-modal').classList.remove('open'); }
|
||||
|
||||
async function saveTmdbKey() {
|
||||
const key = document.getElementById('tmdb-key-input').value.trim();
|
||||
if (!key) { alert('Veuillez saisir une clé.'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}?action=save_tmdb_key`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('token') },
|
||||
body: JSON.stringify({ tmdb_key: key })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert('✅ Clé TMDB chiffrée et sauvegardée.');
|
||||
document.getElementById('tmdb-key-input').value = '';
|
||||
closeConfigModal();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 8. MODALE MOT DE PASSE
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
function openPasswordModal() { document.getElementById('password-modal').classList.add('open'); }
|
||||
function closePasswordModal() { document.getElementById('password-modal').classList.remove('open'); }
|
||||
|
||||
async function saveNewPassword() {
|
||||
const pwd1 = document.getElementById('new-password-input').value;
|
||||
const pwd2 = document.getElementById('new-password-confirm').value;
|
||||
const errEl = document.getElementById('pwd-error');
|
||||
|
||||
errEl.style.display = 'none';
|
||||
|
||||
if (pwd1.length < 4) {
|
||||
errEl.textContent = 'Le mot de passe doit faire au moins 4 caractères.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
if (pwd1 !== pwd2) {
|
||||
errEl.textContent = 'Les mots de passe ne correspondent pas.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Premier usage (compte vide) → setup_admin, sinon update_password
|
||||
const secRes = await fetch(`${API_URL}?action=check_security_status`);
|
||||
const secData = await secRes.json();
|
||||
const action = secData.is_blank ? 'setup_admin' : 'update_password';
|
||||
|
||||
const res = await fetch(`${API_URL}?action=${action}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('token') },
|
||||
body: JSON.stringify({ password: pwd1, new_password: pwd1 })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success || data.success === '') {
|
||||
alert('✅ Mot de passe mis à jour. Vous allez être redirigé vers la connexion.');
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 9. DÉCONNEXION
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
document.addEventListener('DOMContentLoaded', loadDashboardData);
|
||||
>>>>>>> 5a22e3f (all)
|
||||
|
||||
+96
-173
@@ -1,45 +1,50 @@
|
||||
const STORAGE_KEY = 'mon-cinema-films';
|
||||
let films = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
||||
let films = [];
|
||||
const API_URL = '../api.php';
|
||||
let currentPubTab = 'critique';
|
||||
let activeRatingFilter = 0; // 0 = tous
|
||||
let activeRatingFilter = 0;
|
||||
|
||||
async function loadPublicData() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}?action=get_films`);
|
||||
films = await response.json();
|
||||
renderPublicGrid();
|
||||
} catch (error) {
|
||||
console.error("Erreur de récupération :", error);
|
||||
}
|
||||
}
|
||||
|
||||
function switchPubTab(tabName) {
|
||||
currentPubTab = tabName;
|
||||
activeRatingFilter = 0;
|
||||
|
||||
const filterBar = document.getElementById('rating-filter-bar');
|
||||
if(filterBar) {
|
||||
filterBar.style.display = (tabName === 'critique') ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
const activeBtn = document.getElementById(`tab-pub-${tabName}s`);
|
||||
if(activeBtn) activeBtn.classList.add('active');
|
||||
|
||||
renderPublicGrid();
|
||||
}
|
||||
|
||||
function filterByRating(stars) {
|
||||
if (currentPubTab !== 'critique') return;
|
||||
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);
|
||||
s.classList.toggle('filled', i < activeRatingFilter);
|
||||
});
|
||||
});
|
||||
|
||||
renderPublicGrid();
|
||||
}
|
||||
|
||||
@@ -47,205 +52,126 @@ 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';
|
||||
grid.innerHTML = '';
|
||||
|
||||
let filtered = films.filter(f => f.type === currentPubTab);
|
||||
|
||||
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;
|
||||
filtered = filtered.filter(f => parseInt(f.rating) === 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');
|
||||
}
|
||||
countLabel.textContent = `${filtered.length} ${currentPubTab === 'critique' ? 'critique' : 'œuvre'}${filtered.length > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
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.";
|
||||
}
|
||||
if (emptyState) emptyState.style.display = 'block';
|
||||
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 || '—';
|
||||
filtered.forEach(f => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.onclick = () => openDetail(f.id);
|
||||
|
||||
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>`;
|
||||
// Rendu de l'affiche ou du placeholder si absente
|
||||
let posterHTML = '';
|
||||
if (f.poster) {
|
||||
posterHTML = `<div class="card-poster-wrap"><img class="card-poster" src="${f.poster}" alt="${f.title}" loading="lazy"></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>` : ''}
|
||||
posterHTML = `
|
||||
<div class="card-poster-wrap">
|
||||
<div class="card-poster-placeholder">
|
||||
<i class="ti ti-movie"></i>
|
||||
<span>Pas d'affiche</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="card" onclick="openDetail(${f.id})">
|
||||
<div class="card-poster-wrap">
|
||||
${posterHtml}
|
||||
</div>
|
||||
if (f.type === 'critique') {
|
||||
const starsHTML = '★'.repeat(f.rating) + `<span class="stars-muted">${'☆'.repeat(5 - f.rating)}</span>`;
|
||||
card.innerHTML = `
|
||||
${posterHTML}
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">${movieTitle}</h3>
|
||||
<div class="card-meta">${movieYear.toString().substring(0,4)} · ${movieDirector}</div>
|
||||
${footerInfoHtml}
|
||||
<h3 class="card-title" title="${f.title}">${f.title}</h3>
|
||||
<p class="card-meta">${f.year ? f.year + ' · ' : ''}${f.director || 'Réalisateur inconnu'}</p>
|
||||
<div class="card-stars">${starsHTML}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
card.innerHTML = `
|
||||
${posterHTML}
|
||||
<div class="card-body">
|
||||
<h3 class="card-title" title="${f.title}">${f.title}</h3>
|
||||
<p class="card-meta">${f.year ? f.year + ' · ' : ''}${f.director || 'Réalisateur inconnu'}</p>
|
||||
<div class="card-video-footer">
|
||||
<span class="badge-format">${f.format || 'Physique'}</span>
|
||||
${f.length ? `<span class="video-length"><i class="ti ti-clock"></i> ${f.length} min</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function openDetail(id) {
|
||||
const f = films.find(x => x.id === id);
|
||||
const f = films.find(item => item.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 dBody = document.getElementById('d-body');
|
||||
const detailModalLayout = document.getElementById('detail-modal-layout');
|
||||
const detailOverlay = document.getElementById('detail-overlay');
|
||||
|
||||
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'; }
|
||||
// Gestion de la visibilité globale de l'affiche dans la modale
|
||||
if (f.poster) {
|
||||
if (dPoster) dPoster.src = f.poster;
|
||||
if (dPosterWrap) {
|
||||
dPosterWrap.style.display = 'block';
|
||||
dPosterWrap.classList.remove('poster-critique', 'poster-video');
|
||||
dPosterWrap.classList.add(fType === 'critique' ? 'poster-critique' : 'poster-video');
|
||||
dPosterWrap.className = `detail-poster poster-${f.type}`;
|
||||
}
|
||||
if (modalLayout) modalLayout.classList.remove('no-poster');
|
||||
if (detailModalLayout) detailModalLayout.classList.remove('no-poster');
|
||||
} else {
|
||||
if (dPoster) dPoster.style.display = 'none';
|
||||
if (dPosterWrap) dPosterWrap.style.display = 'none';
|
||||
if (modalLayout) modalLayout.classList.add('no-poster');
|
||||
if (detailModalLayout) detailModalLayout.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}
|
||||
if (dTitle) dTitle.textContent = f.title;
|
||||
if (dMeta) dMeta.textContent = `${f.year ? f.year + ' | ' : ''}${f.director || 'Réalisateur inconnu'}`;
|
||||
|
||||
if (dBody) {
|
||||
if (f.type === 'critique') {
|
||||
const stars = '★'.repeat(f.rating) + `<span class="stars-muted">${'☆'.repeat(5 - f.rating)}</span>`;
|
||||
dBody.innerHTML = `
|
||||
<div class="detail-stars">${stars}</div>
|
||||
<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>
|
||||
<p class="pub-review-text">${f.review ? f.review : 'Aucun texte rédigé pour le moment.'}</p>
|
||||
</div>
|
||||
${f.streaming ? `<div class="tech-pill" style="margin-top:1.5rem; width:fit-content;"><i class="ti ti-device-tv"></i> Visionnage : ${f.streaming}</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}
|
||||
|
||||
dBody.innerHTML = `
|
||||
<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>` : ''}
|
||||
${f.format ? `<span class="tech-pill format-gold"><i class="ti ti-disc"></i> ${f.format}</span>` : ''}
|
||||
${f.length ? `<span class="tech-pill"><i class="ti ti-clock"></i> ${f.length} Min</span>` : ''}
|
||||
${f.number_of_discs ? `<span class="tech-pill"><i class="ti ti-layers-intersect"></i> ${f.number_of_discs} Disque(s)</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 class="tech-meta-item"><span class="tech-meta-label">Éditeur</span><span class="tech-meta-value">${f.publisher || '—'}</span></div>
|
||||
<div class="tech-meta-item"><span class="tech-meta-label">Format Image</span><span class="tech-meta-value">${f.aspect_ratio || '—'}</span></div>
|
||||
<div class="tech-meta-item" style="grid-column: span 2;"><span class="tech-meta-label">Code Barre (EAN)</span><span class="tech-meta-value">${f.ean_isbn13 || '—'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="pub-synopsis-box">
|
||||
<h4>Synopsis</h4>
|
||||
<p>${description}</p>
|
||||
<h4>Synopsis / Notes</h4>
|
||||
<p>${f.description ? f.description : 'Aucune description fournie.'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -253,13 +179,10 @@ function openDetail(id) {
|
||||
|
||||
if (detailOverlay) detailOverlay.classList.add('open');
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
const detailOverlay = document.getElementById('detail-overlay');
|
||||
if (detailOverlay) {
|
||||
detailOverlay.classList.remove('open');
|
||||
}
|
||||
if (detailOverlay) detailOverlay.classList.remove('open');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderPublicGrid();
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', loadPublicData);
|
||||
Reference in New Issue
Block a user