diff --git a/public/control.html b/public/control.html index a72c8b5..e455012 100644 --- a/public/control.html +++ b/public/control.html @@ -199,59 +199,125 @@
-
-
-
-

Version active (footer)

-
- Remplace complètement le checker GitHub. La version affichée dans le footer vient de la DB et peut être changée ici. -
- - - -
- - -
Cette sélection impacte ce que les utilisateurs voient dans le footer.
-
- -
- - -
- -
-
Aperçu (format footer)
-
—
-
+
+
+
+

Version

+
Contrôle la version affichée dans le footer (stockée en DB).
-
-
-

Créer une nouvelle version

-
- Ajoute une entrée dans l’historique, puis tu peux la rendre active. -
+ + + +
+ +
-
- - +
+
+
Version active
+
+ Met à jour ce que les utilisateurs voient dans le footer. +
+ + +
+
+ +
La liste vient de l’historique stocké en DB.
+
+
+ + +
+
+ +
+
Aperçu (footer)
+
—
+
+
-
- - -
-
- - + +
+
+
Bon à savoir
+
    +
  • “Recharger” relit l’historique depuis l’API.
  • +
  • “Activer” change la version active en DB.
  • +
  • Le footer lit ensuite la version via /api/version.
  • +
+
+
-
-
Historique
-
—
+ +
+
+
+
Historique
+
Supprime les entrées obsolètes si besoin.
+
+
+
—
+
+ + +
+
+
+
+
Créer une nouvelle version
+
+ Ajoute une entrée dans l’historique (tu peux l’activer immédiatement). +
+ +
+
+ + +
Ex: 1.2.3
+
+
+ + +
Court & lisible (affiché dans le footer).
+
+
+ + +
+
+
+
+ +
+
+
Astuce
+
+ Après création, va dans l’onglet “Active” pour vérifier l’aperçu du footer. +
+
+
@@ -259,64 +325,116 @@
-
-
-
-

Contenu “Modal 100%”

-

- Contenu affiché quand un utilisateur a vu 100% des films dans la checklist. -

- -
- - -
- -
- - -
Affiché en texte brut (safe). Laisse vide si tu n’en veux pas.
-
- -
- - -
- Affiché comme HTML (le serveur retire les <script> et les handlers on*). -
-
- -
- - -
Chemin relatif (ex: video/reward.mp4) ou URL complète. Vide = pas de vidéo.
-
- -
- - -
+
+
+
+

Modal 100%

+
Contenu affiché quand un utilisateur a vu 100% des films.
-
-
-

Aperçu

-
Aperçu live (non sauvegardé tant que tu n’enregistres pas).
+ -
-
-
—
- Preview +
+ +
+
+
+
+
Contenu
+
+ Modifie le contenu puis enregistre. L’aperçu est live (non sauvegardé tant que tu n’enregistres pas). +
+ +
+ + +
+ +
+ + +
Affiché en texte brut (safe). Laisse vide si tu n’en veux pas.
+
+ +
+ + +
+ Affiché comme HTML (le serveur retire les <script> et les handlers on*). +
+
+ +
+ + +
Chemin relatif (ex: video/reward.mp4) ou URL complète. Vide = pas de vidéo.
+
+ +
+ + +
+
-
-
-
- +
+
+
+
Aperçu
+
+
Aperçu live (non sauvegardé).
+ +
+
+
—
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+
+
+
+
Règles
+
    +
  • Texte: affiché tel quel (safe), retours à la ligne conservés.
  • +
  • HTML: le serveur retire les <script> et les attributs on*.
  • +
  • Vidéo: accepte un chemin relatif (ex: video/reward.mp4) ou une URL complète.
  • +
+
+
+
+
+
Astuce
+
+ Si tu utilises du HTML, préfère des tags simples (<p>, <a>, <strong>) pour éviter des surprises d’affichage. +
+
@@ -362,92 +480,67 @@
-
-
-
-

Ajouter un film

-

- Ajoute un film via l’API (/api/movies/add). Le serveur récupère automatiquement le titre/plot/affiche depuis OMDb avec l’imdb_id. -

- -
-
- - -
Permet de séparer les films et les stats par année.
-
-
- - -
-
- - -
-
- -
-
- - -
Auto = video_src → embed_src → vod_link.
-
-
- - -
If you use a server file (below) and leave this empty, it will auto-use /api/video/<movieId>.
-
-
- - -
- Relative path under VIDEO_FILES_DIR (default: public/video). Supports seeking (Range). -
-
-
- - -
-
- - -
Backward-compat. Prefer video_src / embed_src above.
-
-
-
-
- - -
-
+
+
+
+

Films

+
Gestion et ajout des films (par année).
-
-
+ + +
+ +
-

Gérer les films

-
Sélectionne une année, puis coche les films à supprimer.
+
Gérer les films
+
Filtre, sélection et suppression en lot.
+
+
+ —
-
+
-
+ +
+ +
+ + +
+
+ +
-
+ +
@@ -459,7 +552,7 @@
- +
@@ -477,25 +570,153 @@
Sel.
+ + +
+
Ajouter un film
+
+ Le serveur récupère automatiquement titre/plot/poster via OMDb avec l’imdb_id. +
+ +
+
+
+ + +
Sépare films & stats par année.
+
+
+ + +
+
+ + +
+ +
+
+
+

+ +

+
+
+
+
+ + +
Auto = video_src → embed_src → vod_link.
+
+
+ + +
Si video_file est fourni et que ce champ est vide, la lecture utilisera /api/video/<movieId>.
+
+
+ + +
Chemin relatif sous VIDEO_FILES_DIR (défaut: public/video).
+
+
+ + +
+
+ + +
Compat. Préfère video_src / embed_src.
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+ + +
+
+
+
+
Notes rapides
+
    +
  • Auto: utilise video_src → embed_src → vod_link.
  • +
  • Server file: mets video_file (chemin relatif sous public/video).
  • +
  • Édition: le bouton “Éditer” ouvre une modale avec des onglets Info / Player.
  • +
+
+
+
+
+
Astuce
+
+ Dans “Gérer”, la recherche filtre la table localement (sans requête API). Le “Tout sélectionner” ne coche que les lignes visibles. +
+
+
+
+
-
-
-
-

Backup

-

- Crée une sauvegarde de la base MongoDB (format .ndjson.gz) et conserve-la sur le serveur. -

+
+
+
+

Backup / Restore

+
Sauvegarde et restauration de la base MongoDB.
+
+
-
- - + + +
+ +
+
+
+
Sauvegardes
+
Format .ndjson.gz, stocké sur le serveur.
+
+
+ + +
-
+
@@ -511,79 +732,176 @@
-
-
-
-

Restore

-

- Restaure une sauvegarde (fichier .ndjson.gz). Option recommandée: drop pour supprimer les collections avant import. -

+ +
+
+
+
+
Restaurer une sauvegarde
+
+ Restaure un fichier .ndjson.gz. L’option drop est recommandée. +
-
- La restauration peut écraser les données. Fais un backup juste avant si besoin. +
+ La restauration peut écraser les données. Fais un backup juste avant si besoin. +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+
+
Conseil
+
+ Utilise drop sauf si tu sais exactement ce que tu fais (sinon risque de doublons/erreurs). +
+
+
+
-
- - + +
+
+
+
+
Fonctionnement
+
    +
  • Backup: crée un fichier .ndjson.gz (export MongoDB) et le garde sur le serveur.
  • +
  • Télécharger: récupère le fichier localement (utile avant une manipulation risquée).
  • +
  • Restore: importe le fichier. Avec drop, les collections restaurées sont d’abord supprimées.
  • +
+
+
+
+
+
Check rapide
+
+ Après restore, recharge la page et vérifie “Films”, “Checklist” et “Statistiques”. +
+
+
- -
- - -
- -
- -
- -
-
-
-
-

Définir les gagnants par année (égalité)

-
- Tu peux ajouter plusieurs gagnants pour la même année (ex: égalité au même nombre de points). - Choisis un utilisateur + des points (optionnel), puis clique Ajouter. -
+
+
+
+

Gagnants

+
Définis un ou plusieurs gagnants par année (égalité possible).
+
+
+ + +
+ +
-
- - -
Basé sur les années présentes dans les films.
+
+
+
Ajouter un gagnant
+
+ Tu peux ajouter plusieurs gagnants pour une même année (égalité). +
+ +
+
+ + +
Basé sur les années présentes dans les films.
+
+
+ + +
Pour retirer: utilise la liste ci-dessous.
+
+
+ + +
Laisse vide si non applicable.
+
+
+ +
+ + + +
+
-
- - -
Pour retirer un gagnant, utilise la liste “Gagnants actuels” ci-dessous.
-
-
- - + +
+
+
+
+
Gagnants actuels
+
Vue globale (toutes les années).
+
+
+ +
—
+
+
-
- - - -
- -
-
Gagnants actuels
-
—
+ +
+
+
+
+
Comment ça marche
+
    +
  • Une année peut avoir plusieurs gagnants (égalité).
  • +
  • Les points sont optionnels (affichage uniquement).
  • +
  • Pour retirer un gagnant, utilise le bouton “Retirer” dans la liste “Gagnants actuels”.
  • +
+
+
+
+
+
Astuce
+
+ Si tu changes l’année active du site, vérifie aussi les gagnants de cette année. +
+
+
diff --git a/public/css/app.css b/public/css/app.css index 3d60bd2..b5e236c 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -166,6 +166,41 @@ footer { 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) */ .movie-poster { width: 100%; diff --git a/public/js/app_control.js b/public/js/app_control.js index b5ade3a..4839be4 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -986,22 +986,10 @@ window.onload = async function () { return winnersByYear.get(String(y)) || null; } - function renderWinnerCurrent() { - if (!winnerCurrentEl || !winnerYearSelect) return; - const y = parseYear(winnerYearSelect.value); - if (!y) { - winnerCurrentEl.textContent = '—'; - return; - } - const list = getWinnerForYear(y); + function sortWinnersForDisplay(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 - 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 bp = b?.points === null || b?.points === undefined ? null : Number(b.points); 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; return String(a?.name || '').localeCompare(String(b?.name || ''), 'fr', { sensitivity: 'base' }); }); + } - const rows = sorted.map((w) => { - const name = w?.name || '(utilisateur supprimé)'; - const pts = w?.points === null || w?.points === undefined ? null : Number(w.points); - const ptsLabel = pts === null || Number.isNaN(pts) ? '' : ` — ${pts} pts`; - const uid = String(w?.userId || ''); + 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 ` +
+
${escapeHtml(String(y))}
+
(aucun gagnant)
+
+ `; + } + + const rows = sorted.map((w) => { + const name = escapeHtml(w?.name || '(utilisateur supprimé)'); + const pts = w?.points === null || w?.points === undefined ? null : Number(w.points); + const ptsLabel = pts === null || Number.isNaN(pts) ? '' : ` — ${escapeHtml(String(Math.round(pts)))} pts`; + const uid = escapeHtml(String(w?.userId || '').trim()); + return ( + `
` + + `
${name}${ptsLabel}
` + + `` + + `
` + ); + }).join(''); + + const tieLabel = sorted.length > 1 ? ' (égalité)' : ''; return ` -
-
${name}${ptsLabel}
- +
+
${escapeHtml(String(y))}${tieLabel}
+ ${rows}
`; }).join(''); - const tieLabel = sorted.length > 1 ? ' (égalité)' : ''; - winnerCurrentEl.innerHTML = ` -
${y}${tieLabel}
- ${rows} - `; + winnerCurrentEl.innerHTML = blocks; winnerCurrentEl.querySelectorAll('button[data-winner-remove-year][data-winner-remove-user]').forEach((btn) => { btn.addEventListener('click', async () => {