mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: new view for concert history
This commit is contained in:
parent
08717c24bc
commit
0773f39763
6 changed files with 155 additions and 8 deletions
|
|
@ -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)) |
|
| `/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 |
|
| `/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 |
|
| `/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) |
|
| `/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) |
|
| `/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) |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -670,6 +670,31 @@ button:disabled {
|
||||||
border: 0;
|
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 ---------- */
|
||||||
|
|
||||||
.setlist-columns {
|
.setlist-columns {
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,17 @@
|
||||||
<nav id="main-nav"></nav>
|
<nav id="main-nav"></nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<h1>Historique des concerts</h1>
|
<div class="section-header">
|
||||||
<p class="page-subtitle">Retrouvez la setlist jouée à chaque concert précédent.</p>
|
<div>
|
||||||
|
<h1 style="margin:0">Historique des concerts</h1>
|
||||||
|
<p class="page-subtitle" style="margin:0.25rem 0 0">Du plus récent au plus ancien en défilant vers le bas.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="secondary" id="toggle-view-btn">Vue réduite</button>
|
||||||
|
</div>
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
<div id="history-list" class="card-grid"></div>
|
|
||||||
|
<div id="timeline-view"></div>
|
||||||
|
<div id="compact-view" class="card-grid" style="display:none"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,114 @@
|
||||||
|
let concerts = [];
|
||||||
|
let viewMode = 'timeline';
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
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);
|
const d = new Date(dateStr);
|
||||||
return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' });
|
return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function concertTemplate(c) {
|
function timelineSongCard(song) {
|
||||||
|
const embed = youtubeEmbedUrl(song.youtube_url);
|
||||||
|
return `
|
||||||
|
<div class="timeline-song-card">
|
||||||
|
<div class="timeline-song-title">${escapeHtml(song.title)}</div>
|
||||||
|
<div class="card-subtitle">${escapeHtml(song.artist)}</div>
|
||||||
|
${embed
|
||||||
|
? `<div class="youtube-embed timeline-embed"><iframe src="${embed}" loading="lazy" allowfullscreen></iframe></div>`
|
||||||
|
: `<p class="empty">Pas de lien YouTube</p>`}
|
||||||
|
${song.spotify_url ? `<div class="song-links"><a class="pill-link spotify" href="${escapeHtml(song.spotify_url)}" target="_blank" rel="noopener">♫ Spotify</a></div>` : ''}
|
||||||
|
${song.note ? `<p class="note">${escapeHtml(song.note)}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timelineConcertBlock(concert) {
|
||||||
|
const main = concert.songs.filter((s) => !s.is_encore);
|
||||||
|
const encore = concert.songs.filter((s) => s.is_encore);
|
||||||
|
return `
|
||||||
|
<div class="card timeline-concert">
|
||||||
|
<div class="card-title">${escapeHtml(concert.name || 'Concert')}</div>
|
||||||
|
<div class="card-subtitle">${escapeHtml(concert.venue || '')} · ${formatDate(concert.concert_date)}</div>
|
||||||
|
|
||||||
|
<div class="setlist-section">
|
||||||
|
<h3>Setlist</h3>
|
||||||
|
${main.length
|
||||||
|
? `<div class="timeline-songs">${main.map(timelineSongCard).join('')}</div>`
|
||||||
|
: '<p class="empty">Aucun morceau enregistré.</p>'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${encore.length ? `
|
||||||
|
<div class="setlist-section">
|
||||||
|
<h3>Rappel</h3>
|
||||||
|
<div class="timeline-songs">${encore.map(timelineSongCard).join('')}</div>
|
||||||
|
</div>` : ''}
|
||||||
|
|
||||||
|
<a class="back-link" href="/history-detail.html?id=${concert.id}">Voir / modifier ce concert →</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function concertCardTemplate(c) {
|
||||||
return `
|
return `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a href="/history-detail.html?id=${c.id}" style="text-decoration:none;color:inherit">
|
<a href="/history-detail.html?id=${c.id}" style="text-decoration:none;color:inherit">
|
||||||
<div class="card-title">${escapeHtml(c.name || 'Concert')}</div>
|
<div class="card-title">${escapeHtml(c.name || 'Concert')}</div>
|
||||||
<div class="card-subtitle">${escapeHtml(c.venue || '')} · ${formatDate(c.concert_date)}</div>
|
<div class="card-subtitle">${escapeHtml(c.venue || '')} · ${formatDateShort(c.concert_date)}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTimeline() {
|
||||||
|
const container = document.getElementById('timeline-view');
|
||||||
|
if (!concerts.length) {
|
||||||
|
container.innerHTML = '<p class="empty">Aucun concert passé enregistré.</p>';
|
||||||
|
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) {
|
function showError(message) {
|
||||||
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
await initNav('history');
|
await initNav('history');
|
||||||
|
|
||||||
|
document.getElementById('toggle-view-btn').addEventListener('click', () => {
|
||||||
|
viewMode = viewMode === 'timeline' ? 'compact' : 'timeline';
|
||||||
|
applyViewMode();
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const history = await api.get('/api/setlists/history');
|
concerts = await api.get('/api/setlists/history?full=1');
|
||||||
renderList(document.getElementById('history-list'), history, concertTemplate, 'Aucun concert passé enregistré.');
|
renderTimeline();
|
||||||
|
renderCompact();
|
||||||
|
applyViewMode();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err.message);
|
showError(err.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,31 @@ async function findHistory() {
|
||||||
return rows;
|
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) {
|
async function findById(id) {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT id, name, venue, concert_date, created_by, created_at, updated_at
|
`SELECT id, name, venue, concert_date, created_by, created_at, updated_at
|
||||||
|
|
@ -107,6 +132,7 @@ async function removeSong(setlistId, setlistSongId) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
findNext,
|
findNext,
|
||||||
findHistory,
|
findHistory,
|
||||||
|
findHistoryWithSongs,
|
||||||
findById,
|
findById,
|
||||||
findSongs,
|
findSongs,
|
||||||
create,
|
create,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ router.get(
|
||||||
router.get(
|
router.get(
|
||||||
'/history',
|
'/history',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
|
if (req.query.full) {
|
||||||
|
return res.json(await setlistsRepo.findHistoryWithSongs());
|
||||||
|
}
|
||||||
res.json(await setlistsRepo.findHistory());
|
res.json(await setlistsRepo.findHistory());
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue