v2.3 - redesign admin panel, redesign stats, fix cache and more #61
+456
-138
@@ -199,59 +199,125 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-version" role="tabpanel" aria-labelledby="admin-version-tab">
|
<div class="tab-pane fade" id="admin-version" role="tabpanel" aria-labelledby="admin-version-tab">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12 col-xxl-6">
|
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<h4 class="mb-1">Version active (footer)</h4>
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2">
|
||||||
<div class="text-muted mb-3">
|
<div>
|
||||||
Remplace complètement le checker GitHub. La version affichée dans le footer vient de la DB et peut être changée ici.
|
<h4 class="mb-1">Version</h4>
|
||||||
|
<div class="text-muted">Contrôle la version affichée dans le footer (stockée en DB).</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="app_version_alert" class="alert d-none" role="alert"></div>
|
<div id="app_version_alert" class="alert d-none mt-3 mb-0" role="alert"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<ul class="nav nav-pills admin-subtabs mt-3" id="adminVersionSubTabs" role="tablist">
|
||||||
<label for="app_version_active_select" class="form-label">Choisir la version active</label>
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="admin-version-subtab-active" data-bs-toggle="tab" data-bs-target="#admin-version-pane-active" type="button" role="tab" aria-controls="admin-version-pane-active" aria-selected="true">
|
||||||
|
Active
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-version-subtab-history" data-bs-toggle="tab" data-bs-target="#admin-version-pane-history" type="button" role="tab" aria-controls="admin-version-pane-history" aria-selected="false">
|
||||||
|
Historique
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-version-subtab-create" data-bs-toggle="tab" data-bs-target="#admin-version-pane-create" type="button" role="tab" aria-controls="admin-version-pane-create" aria-selected="false">
|
||||||
|
Nouvelle version
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
<!-- Active -->
|
||||||
|
<div class="tab-pane fade show active" id="admin-version-pane-active" role="tabpanel" aria-labelledby="admin-version-subtab-active" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-7">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-1">Version active</div>
|
||||||
|
<div class="text-muted small mb-3">
|
||||||
|
Met à jour ce que les utilisateurs voient dans le footer.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="app_version_active_select" class="form-label">Choisir une version</label>
|
||||||
|
<div class="row g-2 align-items-end">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
<select id="app_version_active_select" class="form-select"></select>
|
<select id="app_version_active_select" class="form-select"></select>
|
||||||
<div class="form-text">Cette sélection impacte ce que les utilisateurs voient dans le footer.</div>
|
<div class="form-text">La liste vient de l’historique stocké en DB.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-4 d-flex gap-2">
|
||||||
|
<button type="button" id="app_version_reload" class="btn btn-outline-secondary w-100">Recharger</button>
|
||||||
|
<button type="button" id="app_version_set_active" class="btn btn-warning w-100">Activer</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap justify-content-end gap-2">
|
|
||||||
<button type="button" id="app_version_reload" class="btn btn-outline-primary">Recharger</button>
|
|
||||||
<button type="button" id="app_version_set_active" class="btn btn-warning">Définir active</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="text-muted small mb-1">Aperçu (format footer)</div>
|
<div class="text-muted small mb-1">Aperçu (footer)</div>
|
||||||
<div id="app_version_preview" class="border rounded p-2 bg-light">—</div>
|
<div id="app_version_preview" class="border rounded p-2 bg-white app-version-preview">—</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xxl-6">
|
<div class="col-12 col-xxl-5">
|
||||||
<div class="card p-3">
|
<div class="border rounded p-3 bg-white">
|
||||||
<h4 class="mb-1">Créer une nouvelle version</h4>
|
<div class="fw-semibold mb-2">Bon à savoir</div>
|
||||||
<div class="text-muted mb-3">
|
<ul class="mb-0 text-muted">
|
||||||
Ajoute une entrée dans l’historique, puis tu peux la rendre active.
|
<li>“Recharger” relit l’historique depuis l’API.</li>
|
||||||
|
<li>“Activer” change la version active en DB.</li>
|
||||||
|
<li>Le footer lit ensuite la version via <code>/api/version</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History -->
|
||||||
|
<div class="tab-pane fade" id="admin-version-pane-history" role="tabpanel" aria-labelledby="admin-version-subtab-history" tabindex="0">
|
||||||
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2 mb-2">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Historique</div>
|
||||||
|
<div class="text-muted small">Supprime les entrées obsolètes si besoin.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="app_version_list" class="border rounded bg-white app-version-list">—</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create -->
|
||||||
|
<div class="tab-pane fade" id="admin-version-pane-create" role="tabpanel" aria-labelledby="admin-version-subtab-create" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-7">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-1">Créer une nouvelle version</div>
|
||||||
|
<div class="text-muted small mb-3">
|
||||||
|
Ajoute une entrée dans l’historique (tu peux l’activer immédiatement).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<label for="app_version_new_version" class="form-label">Version</label>
|
<label for="app_version_new_version" class="form-label">Version</label>
|
||||||
<input id="app_version_new_version" type="text" class="form-control" placeholder="ex: 1.0.1" maxlength="80">
|
<input id="app_version_new_version" type="text" class="form-control" placeholder="ex: 1.0.1" maxlength="80">
|
||||||
|
<div class="form-text">Ex: <code>1.2.3</code></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
<label for="app_version_new_message" class="form-label">Message (optionnel)</label>
|
<label for="app_version_new_message" class="form-label">Message (optionnel)</label>
|
||||||
<input id="app_version_new_message" type="text" class="form-control" placeholder="ex: Hotfix login" maxlength="500">
|
<input id="app_version_new_message" type="text" class="form-control" placeholder="ex: Hotfix login" maxlength="500">
|
||||||
|
<div class="form-text">Court & lisible (affiché dans le footer).</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 d-flex flex-wrap justify-content-end gap-2">
|
<div class="col-12 d-flex flex-wrap justify-content-end gap-2">
|
||||||
<button type="button" id="app_version_create" class="btn btn-primary">Créer</button>
|
<button type="button" id="app_version_create" class="btn btn-primary">Créer</button>
|
||||||
<button type="button" id="app_version_create_and_activate" class="btn btn-warning">Créer & activer</button>
|
<button type="button" id="app_version_create_and_activate" class="btn btn-warning">Créer & activer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="col-12 col-xxl-5">
|
||||||
<div class="text-muted small mb-2">Historique</div>
|
<div class="border rounded p-3 bg-white">
|
||||||
<div id="app_version_list" class="border rounded bg-white app-version-list">—</div>
|
<div class="fw-semibold mb-2">Astuce</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Après création, va dans l’onglet “Active” pour vérifier l’aperçu du footer.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,13 +325,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-modal-100" role="tabpanel" aria-labelledby="admin-modal-100-tab">
|
<div class="tab-pane fade" id="admin-modal-100" role="tabpanel" aria-labelledby="admin-modal-100-tab">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12 col-xxl-6">
|
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<h4 class="mb-1">Contenu “Modal 100%”</h4>
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2">
|
||||||
<p class="text-muted mb-3">
|
<div>
|
||||||
Contenu affiché quand un utilisateur a vu 100% des films dans la checklist.
|
<h4 class="mb-1">Modal 100%</h4>
|
||||||
</p>
|
<div class="text-muted">Contenu affiché quand un utilisateur a vu 100% des films.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav nav-pills admin-subtabs mt-3" id="adminCompletionModalSubTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="admin-modal-100-subtab-edit" data-bs-toggle="tab" data-bs-target="#admin-modal-100-pane-edit" type="button" role="tab" aria-controls="admin-modal-100-pane-edit" aria-selected="true">
|
||||||
|
Éditer
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-modal-100-subtab-help" data-bs-toggle="tab" data-bs-target="#admin-modal-100-pane-help" type="button" role="tab" aria-controls="admin-modal-100-pane-help" aria-selected="false">
|
||||||
|
Aide
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
<!-- Edit + Live preview side-by-side -->
|
||||||
|
<div class="tab-pane fade show active" id="admin-modal-100-pane-edit" role="tabpanel" aria-labelledby="admin-modal-100-subtab-edit" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-7">
|
||||||
|
<div class="border rounded p-3 bg-light h-100">
|
||||||
|
<div class="fw-semibold mb-1">Contenu</div>
|
||||||
|
<div class="text-muted small mb-3">
|
||||||
|
Modifie le contenu puis enregistre. L’aperçu est live (non sauvegardé tant que tu n’enregistres pas).
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="completion_modal_title" class="form-label">Titre</label>
|
<label for="completion_modal_title" class="form-label">Titre</label>
|
||||||
@@ -299,15 +389,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xxl-6">
|
<div class="col-12 col-xxl-5">
|
||||||
<div class="card p-3">
|
<div class="border rounded p-3 bg-white h-100">
|
||||||
<h4 class="mb-1">Aperçu</h4>
|
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
||||||
<div class="text-muted mb-3">Aperçu live (non sauvegardé tant que tu n’enregistres pas).</div>
|
<div class="fw-semibold">Aperçu</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mb-3">Aperçu live (non sauvegardé).</div>
|
||||||
|
|
||||||
<div class="border rounded p-3 bg-white">
|
<div class="border rounded p-3 bg-light">
|
||||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
||||||
<div class="fw-semibold" id="completion_modal_preview_title">—</div>
|
<div class="fw-semibold" id="completion_modal_preview_title">—</div>
|
||||||
<span class="badge bg-secondary">Preview</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="completion_modal_preview_custom" class="text-start d-none"></div>
|
<div id="completion_modal_preview_custom" class="text-start d-none"></div>
|
||||||
<div id="completion_modal_preview_text" class="text-start" style="white-space: pre-wrap;"></div>
|
<div id="completion_modal_preview_text" class="text-start" style="white-space: pre-wrap;"></div>
|
||||||
@@ -324,6 +415,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Help -->
|
||||||
|
<div class="tab-pane fade" id="admin-modal-100-pane-help" role="tabpanel" aria-labelledby="admin-modal-100-subtab-help" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-8">
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="fw-semibold mb-2">Règles</div>
|
||||||
|
<ul class="mb-0 text-muted">
|
||||||
|
<li><strong>Texte</strong>: affiché tel quel (safe), retours à la ligne conservés.</li>
|
||||||
|
<li><strong>HTML</strong>: le serveur retire les <code><script></code> et les attributs <code>on*</code>.</li>
|
||||||
|
<li><strong>Vidéo</strong>: accepte un chemin relatif (ex: <code>video/reward.mp4</code>) ou une URL complète.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xxl-4">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-2">Astuce</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Si tu utilises du HTML, préfère des tags simples (<code><p></code>, <code><a></code>, <code><strong></code>) pour éviter des surprises d’affichage.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-users" role="tabpanel" aria-labelledby="admin-users-tab">
|
<div class="tab-pane fade" id="admin-users" role="tabpanel" aria-labelledby="admin-users-tab">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -362,92 +480,67 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-movies" role="tabpanel" aria-labelledby="admin-movies-tab">
|
<div class="tab-pane fade" id="admin-movies" role="tabpanel" aria-labelledby="admin-movies-tab">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12 col-xxl-4">
|
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<h4 class="mb-1">Ajouter un film</h4>
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2">
|
||||||
<p class="text-muted mb-3">
|
<div>
|
||||||
Ajoute un film via l’API (<code>/api/movies/add</code>). Le serveur récupère automatiquement le titre/plot/affiche depuis OMDb avec l’<code>imdb_id</code>.
|
<h4 class="mb-1">Films</h4>
|
||||||
</p>
|
<div class="text-muted">Gestion et ajout des films (par année).</div>
|
||||||
|
|
||||||
<form id="add-movie-form">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="oscar_year" class="form-label">Oscar year</label>
|
|
||||||
<input type="number" class="form-control" id="oscar_year" placeholder="2026" min="1900" max="3000" required>
|
|
||||||
<div class="form-text">Permet de séparer les films et les stats par année.</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="imdb_id" class="form-label">IMDB ID (ex: tt1234567)</label>
|
|
||||||
<input type="text" class="form-control" id="imdb_id" placeholder="tt..." required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="category" class="form-label">Catégorie</label>
|
|
||||||
<input type="text" class="form-control" id="category" placeholder="Best Picture, etc." required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Lecteur (source)</label>
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-12 col-lg-4">
|
|
||||||
<label for="player_mode" class="form-label">Mode</label>
|
|
||||||
<select id="player_mode" class="form-select">
|
|
||||||
<option value="auto" selected>Auto</option>
|
|
||||||
<option value="video">Video</option>
|
|
||||||
<option value="embed">Embed</option>
|
|
||||||
</select>
|
|
||||||
<div class="form-text">Auto = video_src → embed_src → vod_link.</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-8">
|
|
||||||
<label for="video_src" class="form-label">Video URL (mp4/webm/m3u8)</label>
|
|
||||||
<input type="text" class="form-control" id="video_src" placeholder="https://.../movie.mp4 or .../playlist.m3u8">
|
|
||||||
<div class="form-text">If you use a server file (below) and leave this empty, it will auto-use <code>/api/video/<movieId></code>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label for="video_file" class="form-label">Server video file (optional)</label>
|
|
||||||
<input type="text" class="form-control font-monospace" id="video_file" placeholder="my_movie.mp4 or 2026/my_movie.mp4">
|
|
||||||
<div class="form-text">
|
|
||||||
Relative path under <code>VIDEO_FILES_DIR</code> (default: <code>public/video</code>). Supports seeking (Range).
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label for="embed_src" class="form-label">Embed URL or iframe code</label>
|
|
||||||
<textarea class="form-control font-monospace" id="embed_src" rows="3" placeholder="https://www.youtube.com/watch?v=... OR <iframe src='...'></iframe>"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label for="vod_link" class="form-label">Legacy VOD link (optional)</label>
|
|
||||||
<input type="text" class="form-control" id="vod_link" placeholder="https://...">
|
|
||||||
<div class="form-text">Backward-compat. Prefer video_src / embed_src above.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
|
||||||
<button type="button" id="reset-form" class="btn btn-outline-secondary">Reset</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xxl-8">
|
<ul class="nav nav-pills admin-subtabs mt-3" id="adminMoviesSubTabs" role="tablist">
|
||||||
<div class="card p-3">
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="admin-movies-subtab-manage" data-bs-toggle="tab" data-bs-target="#admin-movies-pane-manage" type="button" role="tab" aria-controls="admin-movies-pane-manage" aria-selected="true">
|
||||||
|
Gérer
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-movies-subtab-add" data-bs-toggle="tab" data-bs-target="#admin-movies-pane-add" type="button" role="tab" aria-controls="admin-movies-pane-add" aria-selected="false">
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-movies-subtab-help" data-bs-toggle="tab" data-bs-target="#admin-movies-pane-help" type="button" role="tab" aria-controls="admin-movies-pane-help" aria-selected="false">
|
||||||
|
Aide
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
<!-- Manage -->
|
||||||
|
<div class="tab-pane fade show active" id="admin-movies-pane-manage" role="tabpanel" aria-labelledby="admin-movies-subtab-manage" tabindex="0">
|
||||||
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2 mb-2">
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2 mb-2">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">Gérer les films</h4>
|
<h5 class="mb-1">Gérer les films</h5>
|
||||||
<div class="text-muted">Sélectionne une année, puis coche les films à supprimer.</div>
|
<div class="text-muted">Filtre, sélection et suppression en lot.</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span id="admin-movies-table-count">—</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-2 align-items-end">
|
<div class="row g-2 align-items-end">
|
||||||
<div class="col-md-6 col-xl-4">
|
<div class="col-12 col-md-4 col-xl-3">
|
||||||
<label for="manage_year" class="form-label">Oscar year</label>
|
<label for="manage_year" class="form-label">Oscar year</label>
|
||||||
<select id="manage_year" class="form-select">
|
<select id="manage_year" class="form-select">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-xl-4 d-flex gap-2">
|
|
||||||
|
<div class="col-12 col-md-8 col-xl-5">
|
||||||
|
<label for="admin_movies_search" class="form-label">Recherche</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="admin_movies_search" placeholder="Titre, IMDB ID, catégorie…" autocomplete="off">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="admin_movies_clear_search">Effacer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-4 d-flex gap-2">
|
||||||
<button type="button" id="refresh-movies" class="btn btn-outline-primary w-100">Rafraîchir</button>
|
<button type="button" id="refresh-movies" class="btn btn-outline-primary w-100">Rafraîchir</button>
|
||||||
<button type="button" id="delete-selected" class="btn btn-danger w-100" disabled>Supprimer</button>
|
<button type="button" id="delete-selected" class="btn btn-danger w-100" disabled>Supprimer</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-4">
|
|
||||||
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="select-all-movies">
|
<input class="form-check-input" type="checkbox" id="select-all-movies">
|
||||||
@@ -459,7 +552,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive mt-2">
|
<div class="table-responsive mt-2">
|
||||||
<table class="table table-sm align-middle mb-0">
|
<table class="table table-sm align-middle mb-0 admin-movies-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 1%;">Sel.</th>
|
<th style="width: 1%;">Sel.</th>
|
||||||
@@ -477,25 +570,153 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add -->
|
||||||
|
<div class="tab-pane fade" id="admin-movies-pane-add" role="tabpanel" aria-labelledby="admin-movies-subtab-add" tabindex="0">
|
||||||
|
<h5 class="mb-2">Ajouter un film</h5>
|
||||||
|
<div class="text-muted mb-3">
|
||||||
|
Le serveur récupère automatiquement titre/plot/poster via OMDb avec l’<code>imdb_id</code>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="add-movie-form">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="oscar_year" class="form-label">Oscar year</label>
|
||||||
|
<input type="number" class="form-control" id="oscar_year" placeholder="2026" min="1900" max="3000" required>
|
||||||
|
<div class="form-text">Sépare films & stats par année.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="imdb_id" class="form-label">IMDB ID</label>
|
||||||
|
<input type="text" class="form-control" id="imdb_id" placeholder="tt1234567" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="category" class="form-label">Catégorie</label>
|
||||||
|
<input type="text" class="form-control" id="category" placeholder="Best Picture, etc." required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="accordion" id="adminAddMovieAccordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="adminAddMovieHeadingPlayer">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#adminAddMovieCollapsePlayer" aria-expanded="true" aria-controls="adminAddMovieCollapsePlayer">
|
||||||
|
Source vidéo / Player
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="adminAddMovieCollapsePlayer" class="accordion-collapse collapse show" aria-labelledby="adminAddMovieHeadingPlayer" data-bs-parent="#adminAddMovieAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-lg-4">
|
||||||
|
<label for="player_mode" class="form-label">Mode</label>
|
||||||
|
<select id="player_mode" class="form-select">
|
||||||
|
<option value="auto" selected>Auto</option>
|
||||||
|
<option value="video">Video</option>
|
||||||
|
<option value="embed">Embed</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">Auto = video_src → embed_src → vod_link.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<label for="video_src" class="form-label">Video URL (mp4/webm/m3u8)</label>
|
||||||
|
<input type="text" class="form-control" id="video_src" placeholder="https://.../movie.mp4 or .../playlist.m3u8">
|
||||||
|
<div class="form-text">Si <code>video_file</code> est fourni et que ce champ est vide, la lecture utilisera <code>/api/video/<movieId></code>.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="video_file" class="form-label">Server video file (optionnel)</label>
|
||||||
|
<input type="text" class="form-control font-monospace" id="video_file" placeholder="my_movie.mp4 or 2026/my_movie.mp4">
|
||||||
|
<div class="form-text">Chemin relatif sous <code>VIDEO_FILES_DIR</code> (défaut: <code>public/video</code>).</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="embed_src" class="form-label">Embed URL ou iframe</label>
|
||||||
|
<textarea class="form-control font-monospace" id="embed_src" rows="3" placeholder="https://... OR <iframe src='...'></iframe>"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="vod_link" class="form-label">Legacy VOD link (optionnel)</label>
|
||||||
|
<input type="text" class="form-control" id="vod_link" placeholder="https://...">
|
||||||
|
<div class="form-text">Compat. Préfère <code>video_src</code> / <code>embed_src</code>.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex flex-wrap justify-content-end gap-2">
|
||||||
|
<button type="button" id="reset-form" class="btn btn-outline-secondary">Reset</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help -->
|
||||||
|
<div class="tab-pane fade" id="admin-movies-pane-help" role="tabpanel" aria-labelledby="admin-movies-subtab-help" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-8">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-2">Notes rapides</div>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li><strong>Auto</strong>: utilise <code>video_src</code> → <code>embed_src</code> → <code>vod_link</code>.</li>
|
||||||
|
<li><strong>Server file</strong>: mets <code>video_file</code> (chemin relatif sous <code>public/video</code>).</li>
|
||||||
|
<li><strong>Édition</strong>: le bouton “Éditer” ouvre une modale avec des onglets Info / Player.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xxl-4">
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="fw-semibold mb-2">Astuce</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Dans “Gérer”, la recherche filtre la table localement (sans requête API). Le “Tout sélectionner” ne coche que les lignes visibles.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-backup" role="tabpanel" aria-labelledby="admin-backup-tab">
|
<div class="tab-pane fade" id="admin-backup" role="tabpanel" aria-labelledby="admin-backup-tab">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12 col-xxl-6">
|
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<h4 class="mb-1">Backup</h4>
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2">
|
||||||
<p class="text-muted mb-3">
|
<div>
|
||||||
Crée une sauvegarde de la base MongoDB (format <code>.ndjson.gz</code>) et conserve-la sur le serveur.
|
<h4 class="mb-1">Backup / Restore</h4>
|
||||||
</p>
|
<div class="text-muted">Sauvegarde et restauration de la base MongoDB.</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
<button type="button" id="db-backup-create" class="btn btn-warning">Créer une sauvegarde</button>
|
|
||||||
<button type="button" id="db-backup-refresh" class="btn btn-outline-primary">Rafraîchir la liste</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive mt-3">
|
<ul class="nav nav-pills admin-subtabs mt-3" id="adminBackupSubTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="admin-backup-subtab-backup" data-bs-toggle="tab" data-bs-target="#admin-backup-pane-backup" type="button" role="tab" aria-controls="admin-backup-pane-backup" aria-selected="true">
|
||||||
|
Backup
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-backup-subtab-restore" data-bs-toggle="tab" data-bs-target="#admin-backup-pane-restore" type="button" role="tab" aria-controls="admin-backup-pane-restore" aria-selected="false">
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-backup-subtab-help" data-bs-toggle="tab" data-bs-target="#admin-backup-pane-help" type="button" role="tab" aria-controls="admin-backup-pane-help" aria-selected="false">
|
||||||
|
Aide
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
<!-- Backup -->
|
||||||
|
<div class="tab-pane fade show active" id="admin-backup-pane-backup" role="tabpanel" aria-labelledby="admin-backup-subtab-backup" tabindex="0">
|
||||||
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2 mb-2">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Sauvegardes</div>
|
||||||
|
<div class="text-muted small">Format <code>.ndjson.gz</code>, stocké sur le serveur.</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<button type="button" id="db-backup-create" class="btn btn-warning">Créer une sauvegarde</button>
|
||||||
|
<button type="button" id="db-backup-refresh" class="btn btn-outline-secondary">Rafraîchir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
<table class="table table-sm align-middle mb-0">
|
<table class="table table-sm align-middle mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -511,16 +732,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Restore -->
|
||||||
|
<div class="tab-pane fade" id="admin-backup-pane-restore" role="tabpanel" aria-labelledby="admin-backup-subtab-restore" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-7">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold text-danger mb-1">Restaurer une sauvegarde</div>
|
||||||
|
<div class="text-muted small mb-3">
|
||||||
|
Restaure un fichier <code>.ndjson.gz</code>. L’option <strong>drop</strong> est recommandée.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xxl-6">
|
<div class="alert alert-warning mb-3">
|
||||||
<div class="card p-3">
|
|
||||||
<h4 class="mb-1 text-danger">Restore</h4>
|
|
||||||
<p class="text-muted mb-3">
|
|
||||||
Restaure une sauvegarde (fichier <code>.ndjson.gz</code>). Option recommandée: <strong>drop</strong> pour supprimer les collections avant import.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
La restauration peut <strong>écraser</strong> les données. Fais un backup juste avant si besoin.
|
La restauration peut <strong>écraser</strong> les données. Fais un backup juste avant si besoin.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -543,51 +766,146 @@
|
|||||||
<div id="db-restore-result" class="small text-muted mt-3"></div>
|
<div id="db-restore-result" class="small text-muted mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xxl-5">
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="fw-semibold mb-2">Conseil</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Utilise <strong>drop</strong> sauf si tu sais exactement ce que tu fais (sinon risque de doublons/erreurs).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help -->
|
||||||
|
<div class="tab-pane fade" id="admin-backup-pane-help" role="tabpanel" aria-labelledby="admin-backup-subtab-help" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-8">
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="fw-semibold mb-2">Fonctionnement</div>
|
||||||
|
<ul class="mb-0 text-muted">
|
||||||
|
<li><strong>Backup</strong>: crée un fichier <code>.ndjson.gz</code> (export MongoDB) et le garde sur le serveur.</li>
|
||||||
|
<li><strong>Télécharger</strong>: récupère le fichier localement (utile avant une manipulation risquée).</li>
|
||||||
|
<li><strong>Restore</strong>: importe le fichier. Avec <strong>drop</strong>, les collections restaurées sont d’abord supprimées.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xxl-4">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-2">Check rapide</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Après restore, recharge la page et vérifie “Films”, “Checklist” et “Statistiques”.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-winners" role="tabpanel" aria-labelledby="admin-winners-tab">
|
<div class="tab-pane fade" id="admin-winners" role="tabpanel" aria-labelledby="admin-winners-tab">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12 col-xxl-8">
|
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<h4 class="mb-1">Définir les gagnants par année (égalité)</h4>
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2">
|
||||||
<div class="text-muted mb-3">
|
<div>
|
||||||
Tu peux ajouter plusieurs gagnants pour la même année (ex: égalité au même nombre de points).
|
<h4 class="mb-1">Gagnants</h4>
|
||||||
Choisis un utilisateur + des points (optionnel), puis clique <strong>Ajouter</strong>.
|
<div class="text-muted">Définis un ou plusieurs gagnants par année (égalité possible).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav nav-pills admin-subtabs mt-3" id="adminWinnersSubTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="admin-winners-subtab-set" data-bs-toggle="tab" data-bs-target="#admin-winners-pane-set" type="button" role="tab" aria-controls="admin-winners-pane-set" aria-selected="true">
|
||||||
|
Gérer
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-winners-subtab-help" data-bs-toggle="tab" data-bs-target="#admin-winners-pane-help" type="button" role="tab" aria-controls="admin-winners-pane-help" aria-selected="false">
|
||||||
|
Aide
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
<!-- Set -->
|
||||||
|
<div class="tab-pane fade show active" id="admin-winners-pane-set" role="tabpanel" aria-labelledby="admin-winners-subtab-set" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-7">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-1">Ajouter un gagnant</div>
|
||||||
|
<div class="text-muted small mb-3">
|
||||||
|
Tu peux ajouter plusieurs gagnants pour une même année (égalité).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<label for="winner_year" class="form-label">Année</label>
|
<label for="winner_year" class="form-label">Année</label>
|
||||||
<select id="winner_year" class="form-select"></select>
|
<select id="winner_year" class="form-select"></select>
|
||||||
<div class="form-text">Basé sur les années présentes dans les films.</div>
|
<div class="form-text">Basé sur les années présentes dans les films.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="col-12 col-md-5">
|
||||||
<label for="winner_user" class="form-label">Utilisateur</label>
|
<label for="winner_user" class="form-label">Utilisateur</label>
|
||||||
<select id="winner_user" class="form-select">
|
<select id="winner_user" class="form-select">
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">Pour retirer un gagnant, utilise la liste “Gagnants actuels” ci-dessous.</div>
|
<div class="form-text">Pour retirer: utilise la liste ci-dessous.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<label for="winner_points" class="form-label">Points (optionnel)</label>
|
<label for="winner_points" class="form-label">Points (optionnel)</label>
|
||||||
<input id="winner_points" type="number" class="form-control" placeholder="ex: 42" min="0" step="1">
|
<input id="winner_points" type="number" class="form-control" placeholder="ex: 42" min="0" step="1">
|
||||||
|
<div class="form-text">Laisse vide si non applicable.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap justify-content-end gap-2 mt-3">
|
<div class="d-flex flex-wrap justify-content-end gap-2 mt-3">
|
||||||
<button type="button" id="winner_reload" class="btn btn-outline-primary">Recharger</button>
|
<button type="button" id="winner_reload" class="btn btn-outline-secondary">Recharger</button>
|
||||||
<button type="button" id="winner_clear" class="btn btn-outline-danger">Tout supprimer</button>
|
<button type="button" id="winner_clear" class="btn btn-outline-danger">Tout supprimer</button>
|
||||||
<button type="button" id="winner_save" class="btn btn-warning">Ajouter</button>
|
<button type="button" id="winner_save" class="btn btn-warning">Ajouter</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xxl-5">
|
||||||
|
<div class="border rounded p-3 bg-white h-100">
|
||||||
|
<div class="d-flex flex-wrap align-items-end justify-content-between gap-2 mb-2">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Gagnants actuels</div>
|
||||||
|
<div class="text-muted small">Vue globale (toutes les années).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="text-muted small mb-1">Gagnants actuels</div>
|
|
||||||
<div id="winner_current" class="border rounded p-2 bg-light">—</div>
|
<div id="winner_current" class="border rounded p-2 bg-light">—</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Help -->
|
||||||
|
<div class="tab-pane fade" id="admin-winners-pane-help" role="tabpanel" aria-labelledby="admin-winners-subtab-help" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-xxl-8">
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="fw-semibold mb-2">Comment ça marche</div>
|
||||||
|
<ul class="mb-0 text-muted">
|
||||||
|
<li>Une année peut avoir <strong>plusieurs gagnants</strong> (égalité).</li>
|
||||||
|
<li>Les points sont <strong>optionnels</strong> (affichage uniquement).</li>
|
||||||
|
<li>Pour retirer un gagnant, utilise le bouton “Retirer” dans la liste “Gagnants actuels”.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xxl-4">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-2">Astuce</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Si tu changes l’année active du site, vérifie aussi les gagnants de cette année.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -166,6 +166,41 @@ footer {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Admin: compact "sub tabs" used inside panels */
|
||||||
|
.admin-subtabs {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subtabs .nav-item {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subtabs.nav-pills .nav-link {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.45rem 0.8rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subtabs.nav-pills .nav-link:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subtabs.nav-pills .nav-link.active {
|
||||||
|
background: #111;
|
||||||
|
border-color: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin → Version preview: keep it compact */
|
||||||
|
.app-version-preview {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Movies page helpers (Bootstrap-first, minimal custom) */
|
/* Movies page helpers (Bootstrap-first, minimal custom) */
|
||||||
.movie-poster {
|
.movie-poster {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+45
-25
@@ -986,22 +986,10 @@ window.onload = async function () {
|
|||||||
return winnersByYear.get(String(y)) || null;
|
return winnersByYear.get(String(y)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWinnerCurrent() {
|
function sortWinnersForDisplay(list) {
|
||||||
if (!winnerCurrentEl || !winnerYearSelect) return;
|
|
||||||
const y = parseYear(winnerYearSelect.value);
|
|
||||||
if (!y) {
|
|
||||||
winnerCurrentEl.textContent = '—';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = getWinnerForYear(y);
|
|
||||||
const winners = Array.isArray(list) ? list : [];
|
const winners = Array.isArray(list) ? list : [];
|
||||||
if (!winners.length) {
|
|
||||||
winnerCurrentEl.textContent = `${y}: (aucun gagnant)`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort for stable display: points desc (null last), then name
|
// Sort for stable display: points desc (null last), then name
|
||||||
const sorted = winners.slice().sort((a, b) => {
|
return winners.slice().sort((a, b) => {
|
||||||
const ap = a?.points === null || a?.points === undefined ? null : Number(a.points);
|
const ap = a?.points === null || a?.points === undefined ? null : Number(a.points);
|
||||||
const bp = b?.points === null || b?.points === undefined ? null : Number(b.points);
|
const bp = b?.points === null || b?.points === undefined ? null : Number(b.points);
|
||||||
if (ap === null && bp !== null) return 1;
|
if (ap === null && bp !== null) return 1;
|
||||||
@@ -1009,25 +997,57 @@ window.onload = async function () {
|
|||||||
if (ap !== null && bp !== null && ap !== bp) return bp - ap;
|
if (ap !== null && bp !== null && ap !== bp) return bp - ap;
|
||||||
return String(a?.name || '').localeCompare(String(b?.name || ''), 'fr', { sensitivity: 'base' });
|
return String(a?.name || '').localeCompare(String(b?.name || ''), 'fr', { sensitivity: 'base' });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWinnerCurrent() {
|
||||||
|
if (!winnerCurrentEl) return;
|
||||||
|
|
||||||
|
const years = Array.from(winnersByYear.keys())
|
||||||
|
.map((k) => Number(k))
|
||||||
|
.filter((n) => Number.isInteger(n))
|
||||||
|
.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
if (!years.length) {
|
||||||
|
winnerCurrentEl.textContent = '—';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = years.map((y) => {
|
||||||
|
const list = winnersByYear.get(String(y)) || [];
|
||||||
|
const sorted = sortWinnersForDisplay(list);
|
||||||
|
|
||||||
|
if (!sorted.length) {
|
||||||
|
return `
|
||||||
|
<div class="border rounded p-2 bg-white mb-2">
|
||||||
|
<div class="fw-semibold">${escapeHtml(String(y))}</div>
|
||||||
|
<div class="text-muted small">(aucun gagnant)</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const rows = sorted.map((w) => {
|
const rows = sorted.map((w) => {
|
||||||
const name = w?.name || '(utilisateur supprimé)';
|
const name = escapeHtml(w?.name || '(utilisateur supprimé)');
|
||||||
const pts = w?.points === null || w?.points === undefined ? null : Number(w.points);
|
const pts = w?.points === null || w?.points === undefined ? null : Number(w.points);
|
||||||
const ptsLabel = pts === null || Number.isNaN(pts) ? '' : ` <span class="text-muted">— ${pts} pts</span>`;
|
const ptsLabel = pts === null || Number.isNaN(pts) ? '' : ` <span class="text-muted">— ${escapeHtml(String(Math.round(pts)))} pts</span>`;
|
||||||
const uid = String(w?.userId || '');
|
const uid = escapeHtml(String(w?.userId || '').trim());
|
||||||
|
return (
|
||||||
|
`<div class="d-flex justify-content-between align-items-center border rounded px-2 py-1 mb-2 bg-white">` +
|
||||||
|
`<div class="me-2 min-width-0"><strong class="text-break">${name}</strong>${ptsLabel}</div>` +
|
||||||
|
`<button type="button" class="btn btn-sm btn-outline-danger flex-shrink-0" data-winner-remove-year="${escapeHtml(String(y))}" data-winner-remove-user="${uid}">Retirer</button>` +
|
||||||
|
`</div>`
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const tieLabel = sorted.length > 1 ? ' <span class="text-muted fw-normal">(égalité)</span>' : '';
|
||||||
return `
|
return `
|
||||||
<div class="d-flex justify-content-between align-items-center border rounded px-2 py-1 mb-2">
|
<div class="border rounded p-2 bg-light mb-2">
|
||||||
<div class="me-2"><strong>${name}</strong>${ptsLabel}</div>
|
<div class="fw-semibold mb-2">${escapeHtml(String(y))}${tieLabel}</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" data-winner-remove-year="${y}" data-winner-remove-user="${uid}">Retirer</button>
|
${rows}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const tieLabel = sorted.length > 1 ? ' <span class="text-muted fw-normal">(égalité)</span>' : '';
|
winnerCurrentEl.innerHTML = blocks;
|
||||||
winnerCurrentEl.innerHTML = `
|
|
||||||
<div class="fw-semibold mb-2">${y}${tieLabel}</div>
|
|
||||||
${rows}
|
|
||||||
`;
|
|
||||||
|
|
||||||
winnerCurrentEl.querySelectorAll('button[data-winner-remove-year][data-winner-remove-user]').forEach((btn) => {
|
winnerCurrentEl.querySelectorAll('button[data-winner-remove-year][data-winner-remove-user]').forEach((btn) => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user