update: user can add musics, concerts and all

This commit is contained in:
Nathan FONTEYNE 2026-07-08 13:30:14 +02:00
parent 0683108c8e
commit 692b19a846
7 changed files with 92 additions and 42 deletions

View file

@ -281,6 +281,37 @@ h3 { font-size: 1rem; margin: 0 0 0.5rem; }
.song-thumb:hover a.play-overlay { opacity: 1; }
/* ---------- Tutorial cards (embedded links per instrument) ---------- */
.tutorial-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.7rem;
margin-top: 0.6rem;
}
.tutorial-card {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.tutorial-thumb {
width: 100%;
height: 84px;
}
.tutorial-meta { font-size: 0.8rem; }
.tutorial-label {
margin-top: 0.2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tutorial-label a { color: var(--accent); text-decoration: none; }
.song-body {
flex: 1 1 auto;
min-width: 0;

View file

@ -15,7 +15,7 @@
<p class="page-subtitle">Les morceaux travaillés par le groupe, avec liens d'écoute et tutos par instrument.</p>
<div id="error"></div>
<div id="admin-add-song" class="panel" style="display:none">
<div class="panel">
<h2 style="margin-top:0">Ajouter un morceau</h2>
<form id="add-song-form" class="stacked-form">
<label>Titre <input name="title" required></label>

View file

@ -62,13 +62,12 @@ function songCardTemplate(song) {
<button class="secondary icon-btn toggle-tutorials" data-id="${song.id}">
${isOpen ? 'Masquer les tutos' : `Tutos (${song.tutorial_count})`}
</button>
${me && me.isAdmin ? `<button class="secondary icon-btn edit-song" data-id="${song.id}">Modifier</button>` : ''}
<button class="secondary icon-btn edit-song" data-id="${song.id}">Modifier</button>
</div>
</div>
</div>
<div class="tutorials-panel panel" style="${isOpen ? '' : 'display:none'}">
<div class="tag-list" data-tutorials-for="${song.id}"><p class="empty">Chargement</p></div>
${me && me.isAdmin ? `
<div class="tutorial-grid" data-tutorials-for="${song.id}"><p class="empty">Chargement</p></div>
<form class="inline-form add-tutorial-form" data-song-id="${song.id}">
<label>Instrument
<select name="instrumentId" required>${instrumentOptions()}</select>
@ -76,7 +75,7 @@ function songCardTemplate(song) {
<label>Lien <input name="url" type="url" required placeholder="https://..."></label>
<label>Libellé <input name="label" placeholder="ex: tuto solo"></label>
<button type="submit">Ajouter le tuto</button>
</form>` : ''}
</form>
</div>
</div>
`;
@ -101,6 +100,35 @@ function editSongCardTemplate(song) {
`;
}
function tutorialCardTemplate(t) {
const thumb = youtubeThumbnailUrl(t.url);
if (thumb) {
return `
<div class="tutorial-card" data-tutorial-id="${t.id}">
<div class="song-thumb tutorial-thumb">
<img src="${thumb}" alt="${escapeHtml(t.label || t.instrument_name)}" loading="lazy">
<a class="play-overlay" href="${escapeHtml(t.url)}" target="_blank" rel="noopener" title="Regarder sur YouTube">&#9658;</a>
</div>
<div class="tutorial-meta">
<span class="tag">${escapeHtml(t.instrument_name)}</span>
<div class="tutorial-label">${escapeHtml(t.label || 'Tuto')}</div>
</div>
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
</div>
`;
}
return `
<div class="tutorial-card" data-tutorial-id="${t.id}">
<div class="song-thumb tutorial-thumb placeholder">&#128279;</div>
<div class="tutorial-meta">
<span class="tag">${escapeHtml(t.instrument_name)}</span>
<div class="tutorial-label"><a href="${escapeHtml(t.url)}" target="_blank" rel="noopener">${escapeHtml(t.label || t.url)}</a></div>
</div>
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
</div>
`;
}
function renderSongs() {
const container = document.getElementById('songs-list');
renderList(container, songs, songCardTemplate, 'Aucun morceau au répertoire pour le moment.');
@ -152,11 +180,10 @@ async function loadTutorials(songId) {
container.innerHTML = '<p class="empty">Aucun lien pour le moment.</p>';
return;
}
container.innerHTML = detail.tutorials
.map(
(t) => `<span class="tag">${escapeHtml(t.instrument_name)}: <a href="${escapeHtml(t.url)}" target="_blank" rel="noopener">${escapeHtml(t.label || t.url)}</a></span>`
)
.join('');
container.innerHTML = detail.tutorials.map(tutorialCardTemplate).join('');
container.querySelectorAll('.remove-tutorial').forEach((btn) => {
btn.addEventListener('click', () => onRemoveTutorial(parseInt(btn.dataset.songId, 10), parseInt(btn.dataset.id, 10)));
});
} catch (err) {
showError(err.message);
}
@ -181,6 +208,18 @@ async function onAddTutorial(e) {
}
}
async function onRemoveTutorial(songId, tutorialId) {
try {
await api.del(`/api/songs/${songId}/tutorials/${tutorialId}`);
const song = songs.find((s) => s.id === songId);
if (song) song.tutorial_count -= 1;
await loadTutorials(songId);
renderSongs();
} catch (err) {
showError(err.message);
}
}
async function onAddSong(e) {
e.preventDefault();
const form = e.target;
@ -233,9 +272,6 @@ function showError(message) {
(async function init() {
me = await initNav('repertoire');
await loadInstruments();
if (me.isAdmin) {
document.getElementById('admin-add-song').style.display = 'block';
document.getElementById('add-song-form').addEventListener('submit', onAddSong);
}
document.getElementById('add-song-form').addEventListener('submit', onAddSong);
await loadSongs();
})();

View file

@ -30,17 +30,15 @@ function readOnlyView() {
? `<ol class="setlist">${encore.map((s) => `<li><span class="row-title">${escapeHtml(s.title)}</span> <span class="row-artist">— ${escapeHtml(s.artist)}</span>${s.note ? `<span class="note">${escapeHtml(s.note)}</span>` : ''}</li>`).join('')}</ol>`
: '<p class="empty">Aucun morceau de rappel prévu.</p>'}
</div>
${me.isAdmin ? '<button id="edit-btn" class="secondary" style="margin-top:1rem">Modifier la setlist</button>' : ''}
<button id="edit-btn" class="secondary" style="margin-top:1rem">Modifier la setlist</button>
`;
}
function renderReadOnly() {
const container = document.getElementById('content');
if (!setlist) {
container.innerHTML = me.isAdmin
? `<p class="empty">Aucun concert à venir.</p>${metaFormTemplate()}`
: '<p class="empty">Aucun concert à venir pour le moment.</p>';
if (me.isAdmin) attachMetaFormHandler();
container.innerHTML = `<p class="empty">Aucun concert à venir.</p>${metaFormTemplate()}`;
attachMetaFormHandler();
return;
}
container.innerHTML = readOnlyView();
@ -249,9 +247,7 @@ function showError(message) {
(async function init() {
me = await initNav('setlist');
if (me.isAdmin) {
allSongs = await api.get('/api/songs');
}
allSongs = await api.get('/api/songs');
setlist = await api.get('/api/setlists/next');
renderReadOnly();
})();