This commit is contained in:
2026-06-18 07:38:41 +02:00
parent ec815787ed
commit 2d4d139f9d
9 changed files with 1696 additions and 530 deletions
+197 -204
View File
@@ -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">
<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)">
<div class="admin-wrap">
<!-- 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)">
<!-- 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>
<!-- 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>
<!-- EN-TÊTE ADMIN -->
<div class="admin-header">
<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-tabs">
<button class="tab-btn active" id="tab-critiques" onclick="switchTab('critique')">
<i class="ti ti-blockquote"></i> Mes Critiques
</button>
<button class="tab-btn" id="tab-videotheque" onclick="switchTab('videotheque')">
<i class="ti ti-disc"></i> Ma Vidéothèque
</button>
</div>
<div class="api-notice" id="api-notice">
<strong>Clé API TMDB requise pour la recherche d'affiches.</strong><br>
Gratuit sur <a href="https://www.themoviedb.org/settings/api" target="_blank">themoviedb.org/settings/api</a> (inscription gratuite, clé disponible immédiatement).<br>
<div style="display:flex;gap:0.6rem;margin-top:0.7rem;align-items:center;">
<input id="tmdb-key-input" type="text" placeholder="Colle ta clé TMDB ici" style="flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);font-family:'DM Sans',sans-serif;font-size:0.82rem;padding:0.5rem 0.7rem;border-radius:3px;outline:none;" />
<button class="btn btn-gold" style="white-space:nowrap;" onclick="saveTmdbKey()">Enregistrer</button>
</div>
</div>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th style="width:40px; text-align:center;">
<input type="checkbox" id="th-select-all" onclick="toggleSelectAll(this)" style="cursor:pointer; transform: scale(1.2);">
</th>
<th style="width:50px">Affiche</th>
<th>Titre</th>
<th>Année</th>
<th>Réalisateur</th>
<th id="th-dynamic-col">Note</th>
<th style="width:170px">Actions</th>
</tr>
</thead>
<tbody id="admin-tbody"></tbody>
</table>
</div>
<div class="empty-state" id="admin-empty" style="display:none">
<i class="ti ti-clapperboard-open" aria-hidden="true"></i>
<p id="empty-message">Aucun film pour l'instant. Ajoutez votre première critique !</p>
<div 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>
</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>
<!-- ONGLETS -->
<div class="admin-tabs">
<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="btn-tab-videotheque" onclick="switchAdminTab('videotheque')">
<i class="ti ti-device-tv"></i> Vidéothèque
</button>
</div>
<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>
<!-- 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>
<hr class="divider" />
<!-- 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>
<div class="form-group">
<label for="f-type">Type d'ajout</label>
<select id="f-type" onchange="handleTypeChange(this.value)" style="width:100%; background:var(--surface); border:1px solid var(--border); color:var(--text); padding:0.6rem; border-radius:3px; font-family:inherit;">
<option value="critique">Écrire une critique (Journal)</option>
<option value="videotheque">Ajouter à ma vidéothèque physique</option>
</select>
</div>
<!-- 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:42px; text-align:center;">
<input type="checkbox" id="select-all-checkbox" onclick="toggleSelectAll(this)">
</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">Note</th>
<th style="width:110px; text-align:center;">Actions</th>
</tr>
</thead>
<tbody id="admin-table-body"></tbody>
</table>
</div>
</div><!-- /.admin-wrap -->
<!-- ══════════════════════════════════════════
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-row">
<div class="form-group">
<label for="f-title">Titre</label>
<input id="f-title" type="text" placeholder="ex : Mulholland Drive" />
<label>Titre du film *</label>
<input type="text" id="f-title" required autocomplete="off">
</div>
<div class="form-group">
<label for="f-year">Année</label>
<input id="f-year" type="text" placeholder="ex : 2001" maxlength="4" />
</div>
</div>
<div class="form-group">
<label for="f-director">Réalisateur</label>
<input id="f-director" type="text" placeholder="ex : David Lynch" />
</div>
<div id="form-group-videotheque-fields" style="display: none;">
<div class="form-row" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<div class="form-group" style="flex: 1;">
<label for="f-format">Format physique</label>
<select id="f-format" style="width:100%; background:var(--surface); border:1px solid var(--border); color:var(--text); padding:0.6rem; border-radius:3px; font-family:inherit;">
<option value="dvd">DVD</option>
<option value="bluray">Blu-ray</option>
<option value="bluray_4k">Blu-ray 4K Ultra HD</option>
<option value="vhs">VHS</option>
<option value="collector">Édition Collector / Coffret</option>
<div class="form-row">
<div class="form-group">
<label>Année</label>
<input type="number" id="f-year" min="1800" max="2100">
</div>
<div class="form-group">
<label>Réalisateur</label>
<input type="text" id="f-director">
</div>
</div>
<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 class="form-group">
<label>Critique</label>
<textarea id="f-review" rows="5"></textarea>
</div>
<div class="form-group">
<label>Plateforme / Mode de visionnage</label>
<input type="text" id="f-streaming" placeholder="Ex : Canal+, Cinéma, Blu-ray…">
</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" />
<!-- Champs spécifiques VIDÉOTHÈQUE -->
<div id="form-videotheque-fields" style="display:none;">
<div class="form-row">
<div class="form-group">
<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-group" style="flex: 1;">
<label for="f-ean">Code-barres (EAN13)</label>
<input id="f-ean" type="text" placeholder="ex : 7321950374984" />
<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>Synopsis / Notes d'édition</label>
<textarea id="f-description" rows="4"></textarea>
</div>
</div>
<div class="form-row" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<div class="form-group" style="flex: 1;">
<label for="f-discs">Nombre de disques</label>
<input id="f-discs" type="number" placeholder="ex : 2" defaultValue="1" />
</div>
<div class="form-group" style="flex: 1;">
<label for="f-aspect">Format d'image (Aspect Ratio)</label>
<input id="f-aspect" type="text" placeholder="ex : 2.35:1 ou 1.66:1" />
</div>
</div>
<div class="form-group" style="margin-bottom: 1rem;">
<label for="f-description">Description / Synopsis complet</label>
<textarea id="f-description" placeholder="Description extraite du catalogue..." style="height: 100px;"></textarea>
</div>
</div>
<div class="poster-preview" id="poster-preview-wrap">
<div class="poster-preview-none" id="poster-preview-none"><i class="ti ti-photo-off"></i></div>
<img id="poster-preview-img" src="" alt="" style="display:none" />
<div class="poster-preview-info" id="poster-preview-info">Aucune affiche sélectionnée.<br>Faites une recherche ou collez une URL.</div>
</div>
<div class="form-group">
<label for="f-poster">URL de l'affiche (optionnel)</label>
<input id="f-poster" type="url" placeholder="https://…" oninput="updatePosterPreview(this.value)" />
</div>
<div id="form-group-critique-fields">
<div class="form-group">
<label>Note</label>
<div class="star-select" id="star-select">
<button type="button" onclick="setRating(1)" aria-label="1 étoile"></button>
<button type="button" onclick="setRating(2)" aria-label="2 étoiles"></button>
<button type="button" onclick="setRating(3)" aria-label="3 étoiles"></button>
<button type="button" onclick="setRating(4)" aria-label="4 étoiles"></button>
<button type="button" onclick="setRating(5)" aria-label="5 étoiles"></button>
</div>
</div>
<div class="form-group">
<label for="f-review">Ma critique</label>
<textarea id="f-review" placeholder="Écris ta critique ici…"></textarea>
</div>
</div>
<button type="button" class="btn-save" onclick="saveFilm()">Enregistrer</button>
<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>
+108 -9
View File
@@ -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">
<link rel="stylesheet" href="../css/admin.css">
<title>Login</title></head>
<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 - 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" />
<p class="login-err" id="login-err">Mot de passe incorrect.</p>
<button class="btn btn-gold" onclick="doLogin()">Accéder</button>
<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>
</div>
</div>
<script src="../js/admin.js"></script>
</body></html>
<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>