update: one concert tab instead of 2

This commit is contained in:
Nathan FONTEYNE 2026-07-09 18:38:22 +02:00
parent 23fc039ac3
commit 8ef9d1db42
11 changed files with 161 additions and 153 deletions

View file

@ -239,12 +239,11 @@ erDiagram
|---|---|---| |---|---|---|
| `/index.html` | Tous (lecture et écriture) | Répertoire des morceaux travaillés, avec recherche instantanée (titre/artiste), 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, avec recherche instantanée (titre/artiste), 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, lien "Écouter la setlist sur YouTube" | | `/concerts.html` | Tous (lecture et écriture) | Deux onglets sur une même page. **Prochain concert** : choix des morceaux du répertoire, ordre, notes, section rappel, lien "Écouter la setlist sur YouTube". **Historique** : concerts passés, modifiables (date, morceaux, ordre, rappel) et supprimables, avec le même lien playlist ; 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) ; ouvrir un concert passé (`?tab=history&id=...`) reste partageable en lien direct |
| `/history.html`, `/history-detail.html` | Tous (lecture et écriture) | Historique des concerts passés, modifiable (date, morceaux, ordre, rappel) et supprimable, avec le même lien playlist YouTube. `/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) + activité personnelle (morceaux ajoutés, suggestions, votes) | | `/profile.html` | Chacun voit le sien | Profil issu d'Authentik (nom, avatar, groupes) + activité personnelle (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) |
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). Le mode par défaut est la consultation ; les pages Répertoire, Concerts et Suggestions sont interactives pour toute personne connectée (chaque action reste attribuée nominativement via Authentik).
Un bouton clair/sombre dans la barre de navigation permet de forcer un thème (mémorisé par navigateur) ; sans préférence explicite, l'app suit le thème du système. Un bouton clair/sombre dans la barre de navigation permet de forcer un thème (mémorisé par navigateur) ; sans préférence explicite, l'app suit le thème du système.

51
public/concerts.html Normal file
View file

@ -0,0 +1,51 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Concerts — Octane</title>
<script src="/js/theme-init.js"></script>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="app-header">
<nav id="main-nav"></nav>
</header>
<main>
<div class="section-header">
<h1 style="margin:0">Concerts</h1>
<div class="view-toggle">
<button type="button" class="view-toggle-btn active" id="tab-next">Prochain concert</button>
<button type="button" class="view-toggle-btn" id="tab-history">Historique</button>
</div>
</div>
<div id="error"></div>
<div id="next-view">
<div id="next-content">Chargement…</div>
</div>
<div id="history-view" style="display:none">
<div id="history-list-view">
<div class="section-header" style="margin:0 0 0.75rem">
<p class="page-subtitle" style="margin:0">Du plus récent au plus ancien en défilant vers le bas.</p>
<button type="button" class="secondary" id="toggle-history-mode-btn">Vue réduite</button>
</div>
<div id="timeline-view"></div>
<div id="compact-view" class="card-grid" style="display:none"></div>
</div>
<div id="history-detail-view" style="display:none">
<a href="#" class="back-link" id="back-to-history-link">&larr; Retour à l'historique</a>
<div id="history-detail-content"></div>
</div>
</div>
</main>
<script src="/js/api.js"></script>
<script src="/js/render.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/setlistEditor.js"></script>
<script src="/js/concerts.js"></script>
</body>
</html>

View file

@ -1,26 +0,0 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Détail du concert — Octane</title>
<script src="/js/theme-init.js"></script>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="app-header">
<nav id="main-nav"></nav>
</header>
<main>
<a href="/history.html" class="back-link">&larr; Retour à l'historique</a>
<div id="error"></div>
<div id="content">Chargement…</div>
</main>
<script src="/js/api.js"></script>
<script src="/js/render.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/setlistEditor.js"></script>
<script src="/js/history-detail.js"></script>
</body>
</html>

View file

@ -1,33 +0,0 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Historique des concerts — Octane</title>
<script src="/js/theme-init.js"></script>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="app-header">
<nav id="main-nav"></nav>
</header>
<main>
<div class="section-header">
<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="timeline-view"></div>
<div id="compact-view" class="card-grid" style="display:none"></div>
</main>
<script src="/js/api.js"></script>
<script src="/js/render.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/history.js"></script>
</body>
</html>

View file

@ -176,7 +176,7 @@ async function onSaveSlotSettings(e) {
</div> </div>
<h2>Créneaux de répétition</h2> <h2>Créneaux de répétition</h2>
<p class="note">Horaires utilisés pour calculer les disponibilités sur `/calendar.html`.</p> <p class="note">Horaires utilisés pour calculer les disponibilités sur <code>/calendar.html</code>.</p>
<div class="panel"> <div class="panel">
<form id="slot-settings-form" class="stacked-form"> <form id="slot-settings-form" class="stacked-form">
<label>Semaine début <input type="time" name="weekdayStart" required></label> <label>Semaine début <input type="time" name="weekdayStart" required></label>
@ -198,7 +198,7 @@ async function onSaveSlotSettings(e) {
<h2>Calendriers des membres</h2> <h2>Calendriers des membres</h2>
<p class="note"> <p class="note">
Chaque utilisateur de l'application peut avoir plusieurs calendriers (Google, Outlook, Apple...). Chaque utilisateur de l'application peut avoir plusieurs calendriers (Google, Outlook, Apple...).
Seuls les utilisateurs avec au moins un calendrier configuré apparaissent sur `/calendar.html`. Seuls les utilisateurs avec au moins un calendrier configuré apparaissent sur <code>/calendar.html</code>.
L'application ne conserve jamais le contenu de ces calendriers seul un statut disponible/occupé L'application ne conserve jamais le contenu de ces calendriers seul un statut disponible/occupé
par créneau est déduit et enregistré. par créneau est déduit et enregistré.
</p> </p>

View file

@ -1,5 +1,6 @@
let allSongs = [];
let concerts = []; let concerts = [];
let viewMode = 'timeline'; let historyViewMode = 'timeline';
function formatDate(dateStr) { function formatDate(dateStr) {
const d = new Date(dateStr); const d = new Date(dateStr);
@ -11,6 +12,21 @@ function formatDateShort(dateStr) {
return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }); return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' });
} }
function showError(message) {
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
}
// ---------- Tabs ----------
function switchTab(tab) {
document.getElementById('next-view').style.display = tab === 'next' ? '' : 'none';
document.getElementById('history-view').style.display = tab === 'history' ? '' : 'none';
document.getElementById('tab-next').classList.toggle('active', tab === 'next');
document.getElementById('tab-history').classList.toggle('active', tab === 'history');
}
// ---------- Historique: liste (timeline / compact) ----------
function timelineSongRow(song) { function timelineSongRow(song) {
const embed = youtubeEmbedUrl(song.youtube_url); const embed = youtubeEmbedUrl(song.youtube_url);
return ` return `
@ -52,7 +68,7 @@ function timelineConcertBlock(concert) {
<div class="timeline-songs">${encore.map(timelineSongRow).join('')}</div> <div class="timeline-songs">${encore.map(timelineSongRow).join('')}</div>
</div>` : ''} </div>` : ''}
<a class="back-link" href="/history-detail.html?id=${concert.id}">Voir / modifier ce concert &rarr;</a> <a class="back-link" href="#" data-concert-id="${concert.id}">Voir / modifier ce concert &rarr;</a>
</div> </div>
`; `;
} }
@ -60,7 +76,7 @@ function timelineConcertBlock(concert) {
function concertCardTemplate(c) { 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="#" data-concert-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 || '')} · ${formatDateShort(c.concert_date)}</div> <div class="card-subtitle">${escapeHtml(c.venue || '')} · ${formatDateShort(c.concert_date)}</div>
</a> </a>
@ -68,6 +84,15 @@ function concertCardTemplate(c) {
`; `;
} }
function bindConcertLinks(container) {
container.querySelectorAll('[data-concert-id]').forEach((el) => {
el.addEventListener('click', (e) => {
e.preventDefault();
showHistoryDetail(el.dataset.concertId);
});
});
}
function renderTimeline() { function renderTimeline() {
const container = document.getElementById('timeline-view'); const container = document.getElementById('timeline-view');
if (!concerts.length) { if (!concerts.length) {
@ -75,18 +100,20 @@ function renderTimeline() {
return; return;
} }
container.innerHTML = concerts.map(timelineConcertBlock).join(''); container.innerHTML = concerts.map(timelineConcertBlock).join('');
bindConcertLinks(container);
} }
function renderCompact() { function renderCompact() {
const container = document.getElementById('compact-view'); const container = document.getElementById('compact-view');
renderList(container, concerts, concertCardTemplate, 'Aucun concert passé enregistré.'); renderList(container, concerts, concertCardTemplate, 'Aucun concert passé enregistré.');
bindConcertLinks(container);
} }
function applyViewMode() { function applyHistoryViewMode() {
const timelineEl = document.getElementById('timeline-view'); const timelineEl = document.getElementById('timeline-view');
const compactEl = document.getElementById('compact-view'); const compactEl = document.getElementById('compact-view');
const btn = document.getElementById('toggle-view-btn'); const btn = document.getElementById('toggle-history-mode-btn');
if (viewMode === 'timeline') { if (historyViewMode === 'timeline') {
timelineEl.style.display = 'block'; timelineEl.style.display = 'block';
compactEl.style.display = 'none'; compactEl.style.display = 'none';
btn.textContent = 'Vue réduite'; btn.textContent = 'Vue réduite';
@ -97,23 +124,81 @@ function applyViewMode() {
} }
} }
function showError(message) { async function loadHistory() {
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
}
(async function init() {
await initNav('history');
document.getElementById('toggle-view-btn').addEventListener('click', () => {
viewMode = viewMode === 'timeline' ? 'compact' : 'timeline';
applyViewMode();
});
try {
concerts = await api.get('/api/setlists/history?full=1'); concerts = await api.get('/api/setlists/history?full=1');
renderTimeline(); renderTimeline();
renderCompact(); renderCompact();
applyViewMode(); applyHistoryViewMode();
}
// ---------- Historique: détail d'un concert ----------
function showHistoryList() {
document.getElementById('history-detail-view').style.display = 'none';
document.getElementById('history-list-view').style.display = 'block';
history.pushState({}, '', '/concerts.html?tab=history');
}
function showHistoryDetail(id) {
document.getElementById('history-list-view').style.display = 'none';
document.getElementById('history-detail-view').style.display = 'block';
history.pushState({}, '', `/concerts.html?tab=history&id=${id}`);
const editor = createSetlistEditor({
containerId: 'history-detail-content',
allSongs,
getSetlist: async () => {
try {
return await api.get(`/api/setlists/${id}`);
} catch (err) {
return null;
}
},
emptyMessage: 'Concert introuvable.',
allowDelete: true,
onDeleted: async () => {
showHistoryList();
await loadHistory();
},
});
editor.load();
}
(async function init() {
await initNav('concerts');
allSongs = await api.get('/api/songs');
const nextEditor = createSetlistEditor({
containerId: 'next-content',
allSongs,
getSetlist: () => api.get('/api/setlists/next'),
createSetlist: (data) => api.post('/api/setlists', data),
emptyMessage: 'Aucun concert à venir pour le moment.',
});
document.getElementById('tab-next').addEventListener('click', () => switchTab('next'));
document.getElementById('tab-history').addEventListener('click', () => switchTab('history'));
document.getElementById('toggle-history-mode-btn').addEventListener('click', () => {
historyViewMode = historyViewMode === 'timeline' ? 'compact' : 'timeline';
applyHistoryViewMode();
});
document.getElementById('back-to-history-link').addEventListener('click', (e) => {
e.preventDefault();
showHistoryList();
});
try {
await nextEditor.load();
await loadHistory();
const params = new URLSearchParams(window.location.search);
const deepId = params.get('id');
if (deepId) {
switchTab('history');
showHistoryDetail(deepId);
} else if (params.get('tab') === 'history') {
switchTab('history');
}
} catch (err) { } catch (err) {
showError(err.message); showError(err.message);
} }

View file

@ -1,29 +0,0 @@
(async function init() {
await initNav('history');
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
const container = document.getElementById('content');
if (!id) {
container.innerHTML = '<p class="empty">Concert introuvable.</p>';
return;
}
const allSongs = await api.get('/api/songs');
const editor = createSetlistEditor({
allSongs,
getSetlist: async () => {
try {
return await api.get(`/api/setlists/${id}`);
} catch (err) {
return null;
}
},
emptyMessage: 'Concert introuvable.',
allowDelete: true,
onDeleted: () => {
window.location.href = '/history.html';
},
});
await editor.load();
})();

View file

@ -28,9 +28,8 @@ async function initNav(activePage) {
<div class="nav-links" id="nav-links"> <div class="nav-links" id="nav-links">
<a href="/index.html" class="${activePage === 'repertoire' ? 'active' : ''}">Répertoire</a> <a href="/index.html" class="${activePage === 'repertoire' ? 'active' : ''}">Répertoire</a>
<a href="/suggestions.html" class="${activePage === 'suggestions' ? 'active' : ''}">Suggestions</a> <a href="/suggestions.html" class="${activePage === 'suggestions' ? 'active' : ''}">Suggestions</a>
<a href="/setlist.html" class="${activePage === 'setlist' ? 'active' : ''}">Prochain concert</a> <a href="/concerts.html" class="${activePage === 'concerts' ? 'active' : ''}">Concerts</a>
<a href="/calendar.html" class="${activePage === 'calendar' ? 'active' : ''}">Disponibilités</a> <a href="/calendar.html" class="${activePage === 'calendar' ? 'active' : ''}">Disponibilités</a>
<a href="/history.html" class="${activePage === 'history' ? 'active' : ''}">Historique</a>
</div> </div>
<div class="nav-user"> <div class="nav-user">
<div class="nav-profile" id="nav-profile"> <div class="nav-profile" id="nav-profile">

View file

@ -1,12 +0,0 @@
(async function init() {
await initNav('setlist');
const allSongs = await api.get('/api/songs');
const editor = createSetlistEditor({
allSongs,
getSetlist: () => api.get('/api/setlists/next'),
createSetlist: (data) => api.post('/api/setlists', data),
emptyMessage: 'Aucun concert à venir pour le moment.',
});
await editor.load();
})();

View file

@ -1,7 +1,7 @@
// Shared setlist editor used by both "prochain concert" (setlist.js) and // Shared setlist editor used by concerts.js for both the "prochain concert"
// "historique" (history-detail.js): choose songs from the repertoire, order // tab and a past concert's detail view: choose songs from the repertoire,
// them, add per-song notes, mark encore songs, save. Parameterized so the // order them, add per-song notes, mark encore songs, save. Parameterized so
// caller decides how to load/create/delete the underlying setlist. // the caller decides how to load/create/delete the underlying setlist.
function createSetlistEditor({ function createSetlistEditor({
containerId = 'content', containerId = 'content',
allSongs, allSongs,

View file

@ -1,26 +0,0 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prochain concert — Octane</title>
<script src="/js/theme-init.js"></script>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="app-header">
<nav id="main-nav"></nav>
</header>
<main>
<h1>Prochain concert</h1>
<div id="error"></div>
<div id="content">Chargement…</div>
</main>
<script src="/js/api.js"></script>
<script src="/js/render.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/setlistEditor.js"></script>
<script src="/js/setlist.js"></script>
</body>
</html>