From 0773f39763456f198622775802f43fc357ea7fcc Mon Sep 17 00:00:00 2001 From: Nathan FONTEYNE Date: Wed, 8 Jul 2026 16:04:22 +0200 Subject: [PATCH] update: new view for concert history --- README.md | 2 +- public/css/style.css | 25 +++++++++ public/history.html | 13 ++++- public/js/history.js | 94 ++++++++++++++++++++++++++++++-- src/repositories/setlistsRepo.js | 26 +++++++++ src/routes/setlists.js | 3 + 6 files changed, 155 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 82c9bab..11e2c3f 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ erDiagram | `/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. Ajout avec autocomplete titre/artiste + liens auto-trouvés ([détails](#recherche-automatique-de-morceaux)) | | `/suggestions.html` | Tous | Proposer un morceau (liens YouTube et Spotify + note libre), voter approuver/rejeter avec commentaire (attribué nominativement), ajouter une suggestion au répertoire | | `/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 et écriture) | Historique des setlists des concerts passés — modifiable (date, morceaux, ordre, rappel) et supprimable en cas d'erreur | +| `/history.html`, `/history-detail.html` | Tous (lecture et écriture) | Historique des concerts passés, modifiable (date, morceaux, ordre, rappel) et supprimable. `/history.html` propose deux vues : chronologique (par défaut, tous les concerts détaillés avec YouTube embarqué par morceau, du plus récent au plus ancien) et réduite (liste compacte, comme avant) | | `/profile.html` | Chacun voit le sien | Profil issu d'Authentik (nom, avatar, groupes) + votre activité (morceaux ajoutés, suggestions, votes) | | `/calendar.html` | Tous (lecture et écriture) | Disponibilités du groupe pour les 3 prochaines semaines (calendrier, filtres par personne, modale par jour) — [détails](#disponibilités-calendrier) | diff --git a/public/css/style.css b/public/css/style.css index a0c940f..53a0875 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -670,6 +670,31 @@ button:disabled { border: 0; } +/* ---------- History timeline ---------- */ + +.timeline-concert { margin-bottom: 1.5rem; } + +.timeline-songs { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.9rem; + margin-top: 0.5rem; +} + +.timeline-song-card { + background: var(--surface-alt); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.7rem; +} + +.timeline-song-title { font-weight: 600; } + +.timeline-embed { + max-width: none; + margin-top: 0.5rem; +} + /* ---------- Setlist ---------- */ .setlist-columns { diff --git a/public/history.html b/public/history.html index f468a85..b10ae4d 100644 --- a/public/history.html +++ b/public/history.html @@ -12,10 +12,17 @@
-

Historique des concerts

-

Retrouvez la setlist jouée à chaque concert précédent.

+
+
+

Historique des concerts

+

Du plus récent au plus ancien en défilant vers le bas.

+
+ +
-
+ +
+
diff --git a/public/js/history.js b/public/js/history.js index 17d1b3c..c0bcf64 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -1,28 +1,114 @@ +let concerts = []; +let viewMode = 'timeline'; + function formatDate(dateStr) { + const d = new Date(dateStr); + return d.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); +} + +function formatDateShort(dateStr) { const d = new Date(dateStr); return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }); } -function concertTemplate(c) { +function timelineSongCard(song) { + const embed = youtubeEmbedUrl(song.youtube_url); + return ` +
+
${escapeHtml(song.title)}
+
${escapeHtml(song.artist)}
+ ${embed + ? `
` + : `

Pas de lien YouTube

`} + ${song.spotify_url ? `` : ''} + ${song.note ? `

${escapeHtml(song.note)}

` : ''} +
+ `; +} + +function timelineConcertBlock(concert) { + const main = concert.songs.filter((s) => !s.is_encore); + const encore = concert.songs.filter((s) => s.is_encore); + return ` +
+
${escapeHtml(concert.name || 'Concert')}
+
${escapeHtml(concert.venue || '')} · ${formatDate(concert.concert_date)}
+ +
+

Setlist

+ ${main.length + ? `
${main.map(timelineSongCard).join('')}
` + : '

Aucun morceau enregistré.

'} +
+ + ${encore.length ? ` +
+

Rappel

+
${encore.map(timelineSongCard).join('')}
+
` : ''} + + Voir / modifier ce concert → +
+ `; +} + +function concertCardTemplate(c) { return `
${escapeHtml(c.name || 'Concert')}
-
${escapeHtml(c.venue || '')} · ${formatDate(c.concert_date)}
+
${escapeHtml(c.venue || '')} · ${formatDateShort(c.concert_date)}
`; } +function renderTimeline() { + const container = document.getElementById('timeline-view'); + if (!concerts.length) { + container.innerHTML = '

Aucun concert passé enregistré.

'; + return; + } + container.innerHTML = concerts.map(timelineConcertBlock).join(''); +} + +function renderCompact() { + const container = document.getElementById('compact-view'); + renderList(container, concerts, concertCardTemplate, 'Aucun concert passé enregistré.'); +} + +function applyViewMode() { + const timelineEl = document.getElementById('timeline-view'); + const compactEl = document.getElementById('compact-view'); + const btn = document.getElementById('toggle-view-btn'); + if (viewMode === 'timeline') { + timelineEl.style.display = 'block'; + compactEl.style.display = 'none'; + btn.textContent = 'Vue réduite'; + } else { + timelineEl.style.display = 'none'; + compactEl.style.display = 'grid'; + btn.textContent = 'Vue chronologique'; + } +} + function showError(message) { document.getElementById('error').innerHTML = `
${escapeHtml(message)}
`; } (async function init() { await initNav('history'); + + document.getElementById('toggle-view-btn').addEventListener('click', () => { + viewMode = viewMode === 'timeline' ? 'compact' : 'timeline'; + applyViewMode(); + }); + try { - const history = await api.get('/api/setlists/history'); - renderList(document.getElementById('history-list'), history, concertTemplate, 'Aucun concert passé enregistré.'); + concerts = await api.get('/api/setlists/history?full=1'); + renderTimeline(); + renderCompact(); + applyViewMode(); } catch (err) { showError(err.message); } diff --git a/src/repositories/setlistsRepo.js b/src/repositories/setlistsRepo.js index 23ec04f..be1bd35 100644 --- a/src/repositories/setlistsRepo.js +++ b/src/repositories/setlistsRepo.js @@ -21,6 +21,31 @@ async function findHistory() { return rows; } +async function findHistoryWithSongs() { + const { rows } = await pool.query( + `SELECT sl.id, sl.name, sl.venue, sl.concert_date, sl.created_by, sl.created_at, sl.updated_at, + COALESCE( + ( + SELECT json_agg( + json_build_object( + 'id', ss.id, 'song_id', ss.song_id, 'title', s.title, 'artist', s.artist, + 'youtube_url', s.youtube_url, 'spotify_url', s.spotify_url, + 'position', ss.position, 'note', ss.note, 'is_encore', ss.is_encore + ) + ORDER BY ss.is_encore, ss.position + ) + FROM setlist_songs ss + JOIN songs s ON s.id = ss.song_id + WHERE ss.setlist_id = sl.id + ), '[]' + ) AS songs + FROM setlists sl + WHERE sl.concert_date < CURRENT_DATE + ORDER BY sl.concert_date DESC` + ); + return rows; +} + async function findById(id) { const { rows } = await pool.query( `SELECT id, name, venue, concert_date, created_by, created_at, updated_at @@ -107,6 +132,7 @@ async function removeSong(setlistId, setlistSongId) { module.exports = { findNext, findHistory, + findHistoryWithSongs, findById, findSongs, create, diff --git a/src/routes/setlists.js b/src/routes/setlists.js index 8a15b72..4ddf19f 100644 --- a/src/routes/setlists.js +++ b/src/routes/setlists.js @@ -21,6 +21,9 @@ router.get( router.get( '/history', asyncHandler(async (req, res) => { + if (req.query.full) { + return res.json(await setlistsRepo.findHistoryWithSongs()); + } res.json(await setlistsRepo.findHistory()); }) );