From 44dacf4196e590417ce845064f5873a456c0e8db Mon Sep 17 00:00:00 2001 From: Nathan FONTEYNE Date: Mon, 20 Jul 2026 14:30:25 +0200 Subject: [PATCH] update: fix hidden filter in calendar and add text zone for tutorials in repertory --- public/css/style.css | 20 +++++++++ public/js/repertoire.js | 43 ++++++++++++++++++- .../migrations/017_song_tutorial_content.sql | 3 ++ src/repositories/songsRepo.js | 12 +++--- src/routes/songs.js | 14 ++++-- 5 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/db/migrations/017_song_tutorial_content.sql diff --git a/public/css/style.css b/public/css/style.css index 408ae61..fa03533 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -479,6 +479,25 @@ h3 { font-size: 1rem; margin: 0 0 0.5rem; } .tutorial-label a { color: var(--accent); text-decoration: none; } +.tutorial-text-wrap { display: flex; flex-direction: column; gap: 0.25rem; } + +.tutorial-text { + margin: 0; + max-height: 4.5em; + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + font-family: ui-monospace, Consolas, monospace; + font-size: 0.8rem; + background: var(--surface-alt, rgba(127, 127, 127, 0.08)); + border-radius: 6px; + padding: 0.4rem 0.5rem; +} + +.tutorial-text.expanded { max-height: none; } + +.tutorial-text-toggle { align-self: flex-start; font-size: 0.75rem; padding: 0.15rem 0.5rem; } + .song-body { flex: 1 1 auto; min-width: 0; @@ -1198,6 +1217,7 @@ button.calendar-filters-close { display: none; } .profile-header { flex-direction: column; text-align: center; } + button.calendar-filters-toggle, button.calendar-filters-close { display: inline-flex; } .calendar-header-actions { width: 100%; } diff --git a/public/js/repertoire.js b/public/js/repertoire.js index 413cee4..3ab3472 100644 --- a/public/js/repertoire.js +++ b/public/js/repertoire.js @@ -79,7 +79,8 @@ function songCardTemplate(song) { - + + @@ -107,7 +108,31 @@ function editSongCardTemplate(song) { `; } +function tutorialTextBlockTemplate(content) { + return ` +
+
${escapeHtml(content)}
+ +
+ `; +} + function tutorialCardTemplate(t) { + const textBlock = t.content ? tutorialTextBlockTemplate(t.content) : ''; + + if (!t.url) { + return ` +
+
+ ${escapeHtml(t.instrument_name)} +
${escapeHtml(t.label || 'Tuto')}
+
+ ${textBlock} + +
+ `; + } + const thumb = youtubeThumbnailUrl(t.url); if (thumb) { return ` @@ -120,6 +145,7 @@ function tutorialCardTemplate(t) { ${escapeHtml(t.instrument_name)}
${escapeHtml(t.label || 'Tuto')}
+ ${textBlock} `; @@ -131,6 +157,7 @@ function tutorialCardTemplate(t) { ${escapeHtml(t.instrument_name)}
${escapeHtml(t.label || t.url)}
+ ${textBlock} `; @@ -195,6 +222,13 @@ async function loadTutorials(songId) { container.querySelectorAll('.remove-tutorial').forEach((btn) => { btn.addEventListener('click', () => onRemoveTutorial(parseInt(btn.dataset.songId, 10), parseInt(btn.dataset.id, 10))); }); + container.querySelectorAll('.tutorial-text-toggle').forEach((btn) => { + btn.addEventListener('click', () => { + const text = btn.previousElementSibling; + const isExpanded = text.classList.toggle('expanded'); + btn.textContent = isExpanded ? 'Voir moins' : 'Voir plus'; + }); + }); } catch (err) { showError(err.message); } @@ -206,9 +240,14 @@ async function onAddTutorial(e) { const songId = parseInt(form.dataset.songId, 10); const instrumentId = parseInt(form.instrumentId.value, 10); const url = form.url.value.trim(); + const content = form.content.value.trim(); const label = form.label.value.trim(); + if (!url && !content) { + showError('Renseignez un lien ou un texte pour le tuto.'); + return; + } try { - await api.post(`/api/songs/${songId}/tutorials`, { instrumentId, url, label }); + await api.post(`/api/songs/${songId}/tutorials`, { instrumentId, url, content, label }); form.reset(); const song = songs.find((s) => s.id === songId); if (song) song.tutorial_count += 1; diff --git a/src/db/migrations/017_song_tutorial_content.sql b/src/db/migrations/017_song_tutorial_content.sql new file mode 100644 index 0000000..1ab0ed4 --- /dev/null +++ b/src/db/migrations/017_song_tutorial_content.sql @@ -0,0 +1,3 @@ +ALTER TABLE song_tutorials + ALTER COLUMN url DROP NOT NULL, + ADD COLUMN content TEXT; diff --git a/src/repositories/songsRepo.js b/src/repositories/songsRepo.js index 99a8cbd..0978b13 100644 --- a/src/repositories/songsRepo.js +++ b/src/repositories/songsRepo.js @@ -48,7 +48,7 @@ async function remove(id) { async function findTutorials(songId) { const { rows } = await pool.query( `SELECT st.id, st.song_id, st.instrument_id, i.name AS instrument_name, - st.url, st.label, st.added_by, st.created_at + st.url, st.content, st.label, st.added_by, st.created_at FROM song_tutorials st JOIN instruments i ON i.id = st.instrument_id WHERE st.song_id = $1 @@ -58,12 +58,12 @@ async function findTutorials(songId) { return rows; } -async function addTutorial(songId, { instrumentId, url, label, addedBy }) { +async function addTutorial(songId, { instrumentId, url, content, label, addedBy }) { const { rows } = await pool.query( - `INSERT INTO song_tutorials (song_id, instrument_id, url, label, added_by) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, song_id, instrument_id, url, label, added_by, created_at`, - [songId, instrumentId, url, label || null, addedBy] + `INSERT INTO song_tutorials (song_id, instrument_id, url, content, label, added_by) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, song_id, instrument_id, url, content, label, added_by, created_at`, + [songId, instrumentId, url || null, content || null, label || null, addedBy] ); return rows[0]; } diff --git a/src/routes/songs.js b/src/routes/songs.js index 3746b81..2d7834a 100644 --- a/src/routes/songs.js +++ b/src/routes/songs.js @@ -91,15 +91,21 @@ router.delete( router.post( '/:id/tutorials', asyncHandler(async (req, res) => { - const { instrumentId, url, label } = req.body || {}; - if (!instrumentId || !url || !url.trim()) { - return res.status(400).json({ error: 'instrument_and_url_required' }); + const { instrumentId, url, content, label } = req.body || {}; + const trimmedUrl = url && url.trim() ? url.trim() : null; + const trimmedContent = content && content.trim() ? content.trim() : null; + if (!instrumentId) { + return res.status(400).json({ error: 'instrument_required' }); + } + if (!trimmedUrl && !trimmedContent) { + return res.status(400).json({ error: 'url_or_content_required' }); } const song = await songsRepo.findById(req.params.id); if (!song) return res.status(404).json({ error: 'not_found' }); const tutorial = await songsRepo.addTutorial(req.params.id, { instrumentId, - url: url.trim(), + url: trimmedUrl, + content: trimmedContent, label, addedBy: req.user.id, });