diff --git a/README.md b/README.md
index f68d635..5c52456 100644
--- a/README.md
+++ b/README.md
@@ -274,7 +274,7 @@ 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)) |
| `/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 |
-| `/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 |
+| `/concerts.html` | Tous (lecture et écriture) | Deux onglets sur une même page. **Prochain concert** : liste de tous les concerts à venir (pas seulement le plus proche), créer autant de concerts à venir que nécessaire ; chaque ligne a des actions Modifier/Supprimer directement visibles à droite (icône seule sur mobile) ; ouvrir un concert donne accès au 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 |
| `/profile.html` | Chacun voit le sien | Profil issu d'Authentik (nom, avatar, groupes) + activité personnelle (morceaux ajoutés, suggestions, votes) ; nom affiché personnalisable (par défaut celui d'Authentik) ; gestion de ses calendriers ICS pour les disponibilités |
| `/calendar.html` | Tous (lecture et écriture) | Disponibilités du groupe pour les 3 prochaines semaines (calendrier, filtres par personne, modale par jour) ; gestion de ses propres calendriers ICS directement depuis la page ; proposer une date de répétition et l'ajouter en un clic à son calendrier (Google/Outlook/Apple) — [détails](#disponibilités-calendrier) |
| `/calendar-ics-help.html` | Tous | Page d'aide : qu'est-ce qu'un lien ICS, comment le trouver sur Google/Outlook/Apple, comment le révoquer/régénérer |
diff --git a/public/concerts.html b/public/concerts.html
index 27e7ea5..6f86ede 100644
--- a/public/concerts.html
+++ b/public/concerts.html
@@ -23,7 +23,26 @@
diff --git a/public/css/style.css b/public/css/style.css
index e0c1d83..cadf834 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -1440,3 +1440,37 @@ a.back-link:hover { color: var(--accent); }
.cal-cell.has-rehearsal.has-concert {
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 20%, var(--surface)), color-mix(in srgb, var(--accent-2) 20%, var(--surface)));
}
+
+/* ---------- Concerts: upcoming list + detail actions ---------- */
+
+.concert-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.concert-row-info { cursor: pointer; flex: 1 1 auto; }
+
+.concert-row-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex: 0 0 auto;
+}
+
+.concert-detail-card {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+/* Icon-only on narrow screens — the emoji glyph alone is enough to convey
+ "modifier"/"supprimer" once space is tight, matching how the nav collapses
+ the same way at this breakpoint. */
+@media (max-width: 640px) {
+ .btn-label { display: none; }
+}
diff --git a/public/js/concerts.js b/public/js/concerts.js
index c859ec0..42d4827 100644
--- a/public/js/concerts.js
+++ b/public/js/concerts.js
@@ -93,6 +93,105 @@ function bindConcertLinks(container) {
});
}
+// ---------- Prochain concert : liste des concerts à venir ----------
+
+let upcomingConcerts = [];
+
+function upcomingConcertRowTemplate(c) {
+ return `
+
+
+
${escapeHtml(c.name || 'Concert')}
+
${escapeHtml(c.venue || '')} · ${formatDateShort(c.concert_date)}
+
+
+ ✏️ Modifier
+ 🗑️ Supprimer
+
+
+ `;
+}
+
+async function loadUpcoming() {
+ upcomingConcerts = await api.get('/api/setlists/upcoming');
+ renderUpcomingList();
+}
+
+function renderUpcomingList() {
+ const container = document.getElementById('upcoming-concerts-list');
+ container.innerHTML = upcomingConcerts.length
+ ? upcomingConcerts.map(upcomingConcertRowTemplate).join('')
+ : '
Aucun concert à venir pour le moment.
';
+ container.querySelectorAll('.concert-row-info').forEach((el) => {
+ el.addEventListener('click', () => showNextDetail(el.dataset.concertId));
+ });
+ container.querySelectorAll('.edit-concert-btn').forEach((btn) => {
+ btn.addEventListener('click', () => showNextDetail(btn.dataset.concertId));
+ });
+ container.querySelectorAll('.delete-concert-btn').forEach((btn) => {
+ btn.addEventListener('click', () => onDeleteUpcoming(btn.dataset.concertId));
+ });
+}
+
+async function onDeleteUpcoming(id) {
+ try {
+ await api.del(`/api/setlists/${id}`);
+ await loadUpcoming();
+ } catch (err) {
+ showError(err.message);
+ }
+}
+
+async function onAddConcert(e) {
+ e.preventDefault();
+ const form = e.target;
+ const name = form.name.value.trim();
+ const venue = form.venue.value.trim();
+ const concertDate = form.concertDate.value;
+ const submitBtn = form.querySelector('button[type="submit"]');
+ submitBtn.disabled = true;
+ try {
+ await api.post('/api/setlists', { name, venue, concertDate });
+ form.reset();
+ document.getElementById('add-concert-panel').style.display = 'none';
+ document.getElementById('toggle-add-concert-btn').textContent = '+ Proposer un concert';
+ await loadUpcoming();
+ } catch (err) {
+ showError(err.message);
+ } finally {
+ submitBtn.disabled = false;
+ }
+}
+
+function showUpcomingList() {
+ document.getElementById('next-detail-view').style.display = 'none';
+ document.getElementById('next-list-view').style.display = 'block';
+}
+
+function showNextDetail(id) {
+ document.getElementById('next-list-view').style.display = 'none';
+ document.getElementById('next-detail-view').style.display = 'block';
+
+ const editor = createSetlistEditor({
+ containerId: 'next-detail-content',
+ allSongs,
+ getSetlist: async () => {
+ try {
+ return await api.get(`/api/setlists/${id}`);
+ } catch (err) {
+ return null;
+ }
+ },
+ emptyMessage: 'Concert introuvable.',
+ allowDelete: true,
+ onDeleted: async () => {
+ showUpcomingList();
+ await loadUpcoming();
+ },
+ });
+ editor.load();
+}
+
function renderTimeline() {
const container = document.getElementById('timeline-view');
if (!concerts.length) {
@@ -168,16 +267,6 @@ function showHistoryDetail(id) {
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.',
- allowDelete: true,
- onDeleted: () => nextEditor.load(),
- });
-
document.getElementById('tab-next').addEventListener('click', () => switchTab('next'));
document.getElementById('tab-history').addEventListener('click', () => switchTab('history'));
document.getElementById('toggle-history-mode-btn').addEventListener('click', () => {
@@ -188,9 +277,20 @@ function showHistoryDetail(id) {
e.preventDefault();
showHistoryList();
});
+ document.getElementById('back-to-upcoming-link').addEventListener('click', (e) => {
+ e.preventDefault();
+ showUpcomingList();
+ });
+ document.getElementById('toggle-add-concert-btn').addEventListener('click', () => {
+ const panel = document.getElementById('add-concert-panel');
+ const isOpen = panel.style.display !== 'none';
+ panel.style.display = isOpen ? 'none' : 'block';
+ document.getElementById('toggle-add-concert-btn').textContent = isOpen ? '+ Proposer un concert' : 'Annuler';
+ });
+ document.getElementById('add-concert-form').addEventListener('submit', onAddConcert);
try {
- await nextEditor.load();
+ await loadUpcoming();
await loadHistory();
const params = new URLSearchParams(window.location.search);
diff --git a/public/js/setlistEditor.js b/public/js/setlistEditor.js
index c95a671..cc3397b 100644
--- a/public/js/setlistEditor.js
+++ b/public/js/setlistEditor.js
@@ -14,7 +14,6 @@ function createSetlistEditor({
let setlist = null;
let mainRows = [];
let encoreRows = [];
- let actionsRevealed = false;
function formatDate(dateStr) {
const d = new Date(dateStr);
@@ -35,11 +34,16 @@ function createSetlistEditor({
const rowHtml = (s) => `
${escapeHtml(s.title)} — ${escapeHtml(s.artist)} ${s.note ? `${escapeHtml(s.note)} ` : ''}`;
const playlistUrl = youtubePlaylistUrl([...main, ...encore].map((s) => s.youtube_url).filter(Boolean));
return `
-
-
${escapeHtml(setlist.name || 'Concert')}
-
${escapeHtml(setlist.venue || '')} · ${formatDate(setlist.concert_date)}
- ${playlistUrl ? `
` : ''}
-
${actionsRevealed ? 'Cliquer pour masquer les actions' : 'Cliquer pour modifier ou supprimer'}
+
+
+
${escapeHtml(setlist.name || 'Concert')}
+
${escapeHtml(setlist.venue || '')} · ${formatDate(setlist.concert_date)}
+ ${playlistUrl ? `
` : ''}
+
+
+ ✏️ Modifier
+ ${allowDelete ? '🗑️ Supprimer ' : ''}
+
Setlist
@@ -49,11 +53,6 @@ function createSetlistEditor({
Rappel
${encore.length ? `
${encore.map(rowHtml).join('')} ` : '
Aucun morceau de rappel prévu.
'}
- ${actionsRevealed ? `
-
- Modifier
- ${allowDelete ? 'Supprimer ce concert ' : ''}
-
` : ''}
`;
}
@@ -66,16 +65,9 @@ function createSetlistEditor({
return;
}
container().innerHTML = readOnlyView();
- document.getElementById('concert-summary-card').addEventListener('click', () => {
- actionsRevealed = !actionsRevealed;
- renderReadOnly();
- });
- const playlistLink = document.getElementById('playlist-link');
- if (playlistLink) playlistLink.addEventListener('click', (e) => e.stopPropagation());
- const editBtn = document.getElementById('edit-btn');
- if (editBtn) editBtn.addEventListener('click', (e) => { e.stopPropagation(); enterEditMode(); });
+ document.getElementById('edit-btn').addEventListener('click', enterEditMode);
const deleteBtn = document.getElementById('delete-btn');
- if (deleteBtn) deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); onDelete(); });
+ if (deleteBtn) deleteBtn.addEventListener('click', onDelete);
}
async function onDelete() {
@@ -168,7 +160,6 @@ function createSetlistEditor({
}
function enterEditMode() {
- actionsRevealed = false;
mainRows = setlist.songs
.filter((s) => !s.is_encore)
.map((s) => ({ setlistSongId: s.id, songId: s.song_id, title: s.title, artist: s.artist, note: s.note }));
@@ -285,7 +276,6 @@ function createSetlistEditor({
async load() {
try {
setlist = await getSetlist();
- actionsRevealed = false;
renderReadOnly();
} catch (err) {
showError(err.message);
diff --git a/src/repositories/setlistsRepo.js b/src/repositories/setlistsRepo.js
index e8307ac..bfdf8b9 100644
--- a/src/repositories/setlistsRepo.js
+++ b/src/repositories/setlistsRepo.js
@@ -11,6 +11,16 @@ async function findNext() {
return rows[0] || null;
}
+async function findUpcoming() {
+ const { rows } = await pool.query(
+ `SELECT id, name, venue, concert_date, created_by, created_at, updated_at
+ FROM setlists
+ WHERE concert_date >= CURRENT_DATE
+ ORDER BY concert_date ASC`
+ );
+ return rows;
+}
+
async function findHistory() {
const { rows } = await pool.query(
`SELECT id, name, venue, concert_date, created_by, created_at, updated_at
@@ -152,6 +162,7 @@ async function getStats() {
module.exports = {
findNext,
+ findUpcoming,
findHistory,
findHistoryWithSongs,
findById,
diff --git a/src/routes/setlists.js b/src/routes/setlists.js
index 4ddf19f..55fa20e 100644
--- a/src/routes/setlists.js
+++ b/src/routes/setlists.js
@@ -18,6 +18,13 @@ router.get(
})
);
+router.get(
+ '/upcoming',
+ asyncHandler(async (req, res) => {
+ res.json(await setlistsRepo.findUpcoming());
+ })
+);
+
router.get(
'/history',
asyncHandler(async (req, res) => {