mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: one concert tab instead of 2
This commit is contained in:
parent
23fc039ac3
commit
8ef9d1db42
11 changed files with 161 additions and 153 deletions
|
|
@ -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)) |
|
||||
| `/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" |
|
||||
| `/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) |
|
||||
| `/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 |
|
||||
| `/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) |
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
51
public/concerts.html
Normal file
51
public/concerts.html
Normal 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">← 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>
|
||||
|
|
@ -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">← 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -176,7 +176,7 @@ async function onSaveSlotSettings(e) {
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<form id="slot-settings-form" class="stacked-form">
|
||||
<label>Semaine — début <input type="time" name="weekdayStart" required></label>
|
||||
|
|
@ -198,7 +198,7 @@ async function onSaveSlotSettings(e) {
|
|||
<h2>Calendriers des membres</h2>
|
||||
<p class="note">
|
||||
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é
|
||||
par créneau est déduit et enregistré.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
let allSongs = [];
|
||||
let concerts = [];
|
||||
let viewMode = 'timeline';
|
||||
let historyViewMode = 'timeline';
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
|
|
@ -11,6 +12,21 @@ function formatDateShort(dateStr) {
|
|||
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) {
|
||||
const embed = youtubeEmbedUrl(song.youtube_url);
|
||||
return `
|
||||
|
|
@ -52,7 +68,7 @@ function timelineConcertBlock(concert) {
|
|||
<div class="timeline-songs">${encore.map(timelineSongRow).join('')}</div>
|
||||
</div>` : ''}
|
||||
|
||||
<a class="back-link" href="/history-detail.html?id=${concert.id}">Voir / modifier ce concert →</a>
|
||||
<a class="back-link" href="#" data-concert-id="${concert.id}">Voir / modifier ce concert →</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -60,7 +76,7 @@ function timelineConcertBlock(concert) {
|
|||
function concertCardTemplate(c) {
|
||||
return `
|
||||
<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-subtitle">${escapeHtml(c.venue || '')} · ${formatDateShort(c.concert_date)}</div>
|
||||
</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() {
|
||||
const container = document.getElementById('timeline-view');
|
||||
if (!concerts.length) {
|
||||
|
|
@ -75,18 +100,20 @@ function renderTimeline() {
|
|||
return;
|
||||
}
|
||||
container.innerHTML = concerts.map(timelineConcertBlock).join('');
|
||||
bindConcertLinks(container);
|
||||
}
|
||||
|
||||
function renderCompact() {
|
||||
const container = document.getElementById('compact-view');
|
||||
renderList(container, concerts, concertCardTemplate, 'Aucun concert passé enregistré.');
|
||||
bindConcertLinks(container);
|
||||
}
|
||||
|
||||
function applyViewMode() {
|
||||
function applyHistoryViewMode() {
|
||||
const timelineEl = document.getElementById('timeline-view');
|
||||
const compactEl = document.getElementById('compact-view');
|
||||
const btn = document.getElementById('toggle-view-btn');
|
||||
if (viewMode === 'timeline') {
|
||||
const btn = document.getElementById('toggle-history-mode-btn');
|
||||
if (historyViewMode === 'timeline') {
|
||||
timelineEl.style.display = 'block';
|
||||
compactEl.style.display = 'none';
|
||||
btn.textContent = 'Vue réduite';
|
||||
|
|
@ -97,23 +124,81 @@ function applyViewMode() {
|
|||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
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 {
|
||||
async function loadHistory() {
|
||||
concerts = await api.get('/api/setlists/history?full=1');
|
||||
renderTimeline();
|
||||
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) {
|
||||
showError(err.message);
|
||||
}
|
||||
|
|
@ -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();
|
||||
})();
|
||||
|
|
@ -28,9 +28,8 @@ async function initNav(activePage) {
|
|||
<div class="nav-links" id="nav-links">
|
||||
<a href="/index.html" class="${activePage === 'repertoire' ? 'active' : ''}">Répertoire</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="/history.html" class="${activePage === 'history' ? 'active' : ''}">Historique</a>
|
||||
</div>
|
||||
<div class="nav-user">
|
||||
<div class="nav-profile" id="nav-profile">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})();
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Shared setlist editor used by both "prochain concert" (setlist.js) and
|
||||
// "historique" (history-detail.js): choose songs from the repertoire, order
|
||||
// them, add per-song notes, mark encore songs, save. Parameterized so the
|
||||
// caller decides how to load/create/delete the underlying setlist.
|
||||
// Shared setlist editor used by concerts.js for both the "prochain concert"
|
||||
// tab and a past concert's detail view: choose songs from the repertoire,
|
||||
// order them, add per-song notes, mark encore songs, save. Parameterized so
|
||||
// the caller decides how to load/create/delete the underlying setlist.
|
||||
function createSetlistEditor({
|
||||
containerId = 'content',
|
||||
allSongs,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue