From 692b19a846974e6ba2aa048ca640eeb3846ebb90 Mon Sep 17 00:00:00 2001 From: Nathan FONTEYNE Date: Wed, 8 Jul 2026 13:30:14 +0200 Subject: [PATCH] update: user can add musics, concerts and all --- README.md | 14 +++++----- public/css/style.css | 31 +++++++++++++++++++++ public/index.html | 2 +- public/js/repertoire.js | 62 ++++++++++++++++++++++++++++++++--------- public/js/setlist.js | 12 +++----- src/routes/setlists.js | 7 ----- src/routes/songs.js | 6 ---- 7 files changed, 92 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f60983e..f291c0b 100644 --- a/README.md +++ b/README.md @@ -220,19 +220,19 @@ erDiagram | Page | Accès | Description | |---|---|---| -| `/index.html` | Tous (lecture), admin (écriture) | Répertoire des morceaux travaillés, liens de tutos par morceau et par instrument | -| `/suggestions.html` | Tous | Proposer un morceau (avec lien YouTube embarqué), voter approuver/rejeter avec commentaire, attribué nominativement | -| `/setlist.html` | Tous (lecture), admin (écriture) | Setlist du prochain concert : ordre des morceaux, notes, section rappel | +| `/index.html` | Tous (lecture et écriture) | Répertoire des morceaux travaillés, liens/vignettes YouTube et Spotify, tutos embarqués par morceau et par instrument | +| `/suggestions.html` | Tous | Proposer un morceau (avec lien YouTube embarqué + note libre), voter approuver/rejeter avec commentaire, attribué nominativement | +| `/setlist.html` | Tous (lecture et écriture) | Setlist du prochain concert : choix des morceaux du répertoire, ordre, notes, section rappel | | `/history.html`, `/history-detail.html` | Tous (lecture seule) | Historique des setlists des concerts passés | -Le mode par défaut est la consultation ; seule la page **Suggestions** est interactive (chaque vote est attribué à la personne connectée). +Le mode par défaut est la consultation ; les pages Répertoire, Setlist et Suggestions sont interactives pour toute personne connectée (chaque action reste attribuée nominativement via Authentik). ## Rôles -- **Membre** : consulte tout, propose des suggestions, vote/commente. -- **Admin** : en plus, gère le répertoire, les tutos, promeut une suggestion approuvée en morceau du répertoire, crée/édite les setlists. +- **Membre** : tout le monde — consulte, ajoute/modifie/supprime des morceaux du répertoire et leurs tutos, crée/modifie des concerts et leur setlist, propose des suggestions, vote/commente. +- **Admin** : en plus, modère les suggestions (promouvoir une suggestion approuvée en morceau du répertoire, la rejeter, la supprimer). -Le rôle admin est déterminé par un claim `groups` renvoyé par Authentik (voir configuration ci-dessous), recalculé à chaque connexion — Authentik reste la seule source de vérité des rôles. +Le rôle admin n'est volontairement pas plus étendu pour l'instant : son périmètre exact (au-delà de la modération des suggestions) reste ouvert et pourra évoluer. Il n'y a pas de gestion des utilisateurs dans l'application elle-même — Authentik reste la seule source de vérité pour qui a accès et qui est admin (claim `groups`, recalculé à chaque connexion). ## Prérequis diff --git a/public/css/style.css b/public/css/style.css index 2e0bbcd..589713e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -281,6 +281,37 @@ h3 { font-size: 1rem; margin: 0 0 0.5rem; } .song-thumb:hover a.play-overlay { opacity: 1; } +/* ---------- Tutorial cards (embedded links per instrument) ---------- */ + +.tutorial-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.7rem; + margin-top: 0.6rem; +} + +.tutorial-card { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.tutorial-thumb { + width: 100%; + height: 84px; +} + +.tutorial-meta { font-size: 0.8rem; } + +.tutorial-label { + margin-top: 0.2rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tutorial-label a { color: var(--accent); text-decoration: none; } + .song-body { flex: 1 1 auto; min-width: 0; diff --git a/public/index.html b/public/index.html index 5d07b4c..cfcad03 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,7 @@

Les morceaux travaillés par le groupe, avec liens d'écoute et tutos par instrument.

-
-

Chargement…

- ${me && me.isAdmin ? ` +

Chargement…

`; @@ -101,6 +100,35 @@ function editSongCardTemplate(song) { `; } +function tutorialCardTemplate(t) { + const thumb = youtubeThumbnailUrl(t.url); + if (thumb) { + return ` +
+
+ ${escapeHtml(t.label || t.instrument_name)} + +
+
+ ${escapeHtml(t.instrument_name)} +
${escapeHtml(t.label || 'Tuto')}
+
+ +
+ `; + } + return ` +
+
🔗
+
+ ${escapeHtml(t.instrument_name)} + +
+ +
+ `; +} + function renderSongs() { const container = document.getElementById('songs-list'); renderList(container, songs, songCardTemplate, 'Aucun morceau au répertoire pour le moment.'); @@ -152,11 +180,10 @@ async function loadTutorials(songId) { container.innerHTML = '

Aucun lien pour le moment.

'; return; } - container.innerHTML = detail.tutorials - .map( - (t) => `${escapeHtml(t.instrument_name)}: ${escapeHtml(t.label || t.url)}` - ) - .join(''); + container.innerHTML = detail.tutorials.map(tutorialCardTemplate).join(''); + container.querySelectorAll('.remove-tutorial').forEach((btn) => { + btn.addEventListener('click', () => onRemoveTutorial(parseInt(btn.dataset.songId, 10), parseInt(btn.dataset.id, 10))); + }); } catch (err) { showError(err.message); } @@ -181,6 +208,18 @@ async function onAddTutorial(e) { } } +async function onRemoveTutorial(songId, tutorialId) { + try { + await api.del(`/api/songs/${songId}/tutorials/${tutorialId}`); + const song = songs.find((s) => s.id === songId); + if (song) song.tutorial_count -= 1; + await loadTutorials(songId); + renderSongs(); + } catch (err) { + showError(err.message); + } +} + async function onAddSong(e) { e.preventDefault(); const form = e.target; @@ -233,9 +272,6 @@ function showError(message) { (async function init() { me = await initNav('repertoire'); await loadInstruments(); - if (me.isAdmin) { - document.getElementById('admin-add-song').style.display = 'block'; - document.getElementById('add-song-form').addEventListener('submit', onAddSong); - } + document.getElementById('add-song-form').addEventListener('submit', onAddSong); await loadSongs(); })(); diff --git a/public/js/setlist.js b/public/js/setlist.js index 7f86e2c..d2143b2 100644 --- a/public/js/setlist.js +++ b/public/js/setlist.js @@ -30,17 +30,15 @@ function readOnlyView() { ? `
    ${encore.map((s) => `
  1. ${escapeHtml(s.title)} — ${escapeHtml(s.artist)}${s.note ? `${escapeHtml(s.note)}` : ''}
  2. `).join('')}
` : '

Aucun morceau de rappel prévu.

'} - ${me.isAdmin ? '' : ''} + `; } function renderReadOnly() { const container = document.getElementById('content'); if (!setlist) { - container.innerHTML = me.isAdmin - ? `

Aucun concert à venir.

${metaFormTemplate()}` - : '

Aucun concert à venir pour le moment.

'; - if (me.isAdmin) attachMetaFormHandler(); + container.innerHTML = `

Aucun concert à venir.

${metaFormTemplate()}`; + attachMetaFormHandler(); return; } container.innerHTML = readOnlyView(); @@ -249,9 +247,7 @@ function showError(message) { (async function init() { me = await initNav('setlist'); - if (me.isAdmin) { - allSongs = await api.get('/api/songs'); - } + allSongs = await api.get('/api/songs'); setlist = await api.get('/api/setlists/next'); renderReadOnly(); })(); diff --git a/src/routes/setlists.js b/src/routes/setlists.js index c1eab25..8a15b72 100644 --- a/src/routes/setlists.js +++ b/src/routes/setlists.js @@ -1,6 +1,5 @@ const express = require('express'); const setlistsRepo = require('../repositories/setlistsRepo'); -const { requireAdmin } = require('../auth/middleware'); const asyncHandler = require('../lib/asyncHandler'); const router = express.Router(); @@ -37,7 +36,6 @@ router.get( router.post( '/', - requireAdmin, asyncHandler(async (req, res) => { const { name, venue, concertDate } = req.body || {}; if (!concertDate) return res.status(400).json({ error: 'concert_date_required' }); @@ -48,7 +46,6 @@ router.post( router.patch( '/:id', - requireAdmin, asyncHandler(async (req, res) => { const { name, venue, concertDate } = req.body || {}; if (!concertDate) return res.status(400).json({ error: 'concert_date_required' }); @@ -60,7 +57,6 @@ router.patch( router.delete( '/:id', - requireAdmin, asyncHandler(async (req, res) => { await setlistsRepo.remove(req.params.id); res.status(204).end(); @@ -69,7 +65,6 @@ router.delete( router.put( '/:id/songs', - requireAdmin, asyncHandler(async (req, res) => { const { songs } = req.body || {}; if (!Array.isArray(songs)) return res.status(400).json({ error: 'songs_array_required' }); @@ -93,7 +88,6 @@ router.put( router.post( '/:id/songs', - requireAdmin, asyncHandler(async (req, res) => { const { songId, position, note, isEncore } = req.body || {}; if (!songId || typeof position !== 'number') { @@ -113,7 +107,6 @@ router.post( router.delete( '/:id/songs/:setlistSongId', - requireAdmin, asyncHandler(async (req, res) => { await setlistsRepo.removeSong(req.params.id, req.params.setlistSongId); res.status(204).end(); diff --git a/src/routes/songs.js b/src/routes/songs.js index 93c94e1..3746b81 100644 --- a/src/routes/songs.js +++ b/src/routes/songs.js @@ -1,6 +1,5 @@ const express = require('express'); const songsRepo = require('../repositories/songsRepo'); -const { requireAdmin } = require('../auth/middleware'); const asyncHandler = require('../lib/asyncHandler'); const { isValidYoutubeUrl } = require('../lib/youtube'); const { isValidSpotifyUrl } = require('../lib/spotify'); @@ -32,7 +31,6 @@ router.get( router.post( '/', - requireAdmin, asyncHandler(async (req, res) => { const { title, artist, notes, youtubeUrl, spotifyUrl } = req.body || {}; if (!title || !title.trim() || !artist || !artist.trim()) { @@ -54,7 +52,6 @@ router.post( router.patch( '/:id', - requireAdmin, asyncHandler(async (req, res) => { const { title, artist, notes, youtubeUrl, spotifyUrl } = req.body || {}; if (!title || !title.trim() || !artist || !artist.trim()) { @@ -76,7 +73,6 @@ router.patch( router.delete( '/:id', - requireAdmin, asyncHandler(async (req, res) => { try { await songsRepo.remove(req.params.id); @@ -94,7 +90,6 @@ router.delete( router.post( '/:id/tutorials', - requireAdmin, asyncHandler(async (req, res) => { const { instrumentId, url, label } = req.body || {}; if (!instrumentId || !url || !url.trim()) { @@ -114,7 +109,6 @@ router.post( router.delete( '/:songId/tutorials/:id', - requireAdmin, asyncHandler(async (req, res) => { await songsRepo.removeTutorial(req.params.songId, req.params.id); res.status(204).end();