mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: fix hidden filter in calendar and add text zone for tutorials in repertory
This commit is contained in:
parent
678b81e790
commit
44dacf4196
5 changed files with 80 additions and 12 deletions
|
|
@ -479,6 +479,25 @@ h3 { font-size: 1rem; margin: 0 0 0.5rem; }
|
||||||
|
|
||||||
.tutorial-label a { color: var(--accent); text-decoration: none; }
|
.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 {
|
.song-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -1198,6 +1217,7 @@ button.calendar-filters-close { display: none; }
|
||||||
|
|
||||||
.profile-header { flex-direction: column; text-align: center; }
|
.profile-header { flex-direction: column; text-align: center; }
|
||||||
|
|
||||||
|
button.calendar-filters-toggle,
|
||||||
button.calendar-filters-close { display: inline-flex; }
|
button.calendar-filters-close { display: inline-flex; }
|
||||||
|
|
||||||
.calendar-header-actions { width: 100%; }
|
.calendar-header-actions { width: 100%; }
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ function songCardTemplate(song) {
|
||||||
<label>Instrument
|
<label>Instrument
|
||||||
<select name="instrumentId" required>${instrumentOptions()}</select>
|
<select name="instrumentId" required>${instrumentOptions()}</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Lien <input name="url" type="url" required placeholder="https://..."></label>
|
<label>Lien <input name="url" type="url" placeholder="https://..."></label>
|
||||||
|
<label>Texte (paroles, grille d'accords, notes...) <textarea name="content" rows="3" placeholder="Ex: Am - F - C - G"></textarea></label>
|
||||||
<label>Libellé <input name="label" placeholder="ex: tuto solo"></label>
|
<label>Libellé <input name="label" placeholder="ex: tuto solo"></label>
|
||||||
<button type="submit">Ajouter le tuto</button>
|
<button type="submit">Ajouter le tuto</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -107,7 +108,31 @@ function editSongCardTemplate(song) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tutorialTextBlockTemplate(content) {
|
||||||
|
return `
|
||||||
|
<div class="tutorial-text-wrap">
|
||||||
|
<pre class="tutorial-text">${escapeHtml(content)}</pre>
|
||||||
|
<button type="button" class="secondary icon-btn tutorial-text-toggle">Voir plus</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function tutorialCardTemplate(t) {
|
function tutorialCardTemplate(t) {
|
||||||
|
const textBlock = t.content ? tutorialTextBlockTemplate(t.content) : '';
|
||||||
|
|
||||||
|
if (!t.url) {
|
||||||
|
return `
|
||||||
|
<div class="tutorial-card" data-tutorial-id="${t.id}">
|
||||||
|
<div class="tutorial-meta">
|
||||||
|
<span class="tag">${escapeHtml(t.instrument_name)}</span>
|
||||||
|
<div class="tutorial-label">${escapeHtml(t.label || 'Tuto')}</div>
|
||||||
|
</div>
|
||||||
|
${textBlock}
|
||||||
|
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const thumb = youtubeThumbnailUrl(t.url);
|
const thumb = youtubeThumbnailUrl(t.url);
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
return `
|
return `
|
||||||
|
|
@ -120,6 +145,7 @@ function tutorialCardTemplate(t) {
|
||||||
<span class="tag">${escapeHtml(t.instrument_name)}</span>
|
<span class="tag">${escapeHtml(t.instrument_name)}</span>
|
||||||
<div class="tutorial-label">${escapeHtml(t.label || 'Tuto')}</div>
|
<div class="tutorial-label">${escapeHtml(t.label || 'Tuto')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
${textBlock}
|
||||||
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
|
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -131,6 +157,7 @@ function tutorialCardTemplate(t) {
|
||||||
<span class="tag">${escapeHtml(t.instrument_name)}</span>
|
<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 class="tutorial-label"><a href="${escapeHtml(t.url)}" target="_blank" rel="noopener">${escapeHtml(t.label || t.url)}</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
${textBlock}
|
||||||
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
|
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -195,6 +222,13 @@ async function loadTutorials(songId) {
|
||||||
container.querySelectorAll('.remove-tutorial').forEach((btn) => {
|
container.querySelectorAll('.remove-tutorial').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => onRemoveTutorial(parseInt(btn.dataset.songId, 10), parseInt(btn.dataset.id, 10)));
|
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) {
|
} catch (err) {
|
||||||
showError(err.message);
|
showError(err.message);
|
||||||
}
|
}
|
||||||
|
|
@ -206,9 +240,14 @@ async function onAddTutorial(e) {
|
||||||
const songId = parseInt(form.dataset.songId, 10);
|
const songId = parseInt(form.dataset.songId, 10);
|
||||||
const instrumentId = parseInt(form.instrumentId.value, 10);
|
const instrumentId = parseInt(form.instrumentId.value, 10);
|
||||||
const url = form.url.value.trim();
|
const url = form.url.value.trim();
|
||||||
|
const content = form.content.value.trim();
|
||||||
const label = form.label.value.trim();
|
const label = form.label.value.trim();
|
||||||
|
if (!url && !content) {
|
||||||
|
showError('Renseignez un lien ou un texte pour le tuto.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await api.post(`/api/songs/${songId}/tutorials`, { instrumentId, url, label });
|
await api.post(`/api/songs/${songId}/tutorials`, { instrumentId, url, content, label });
|
||||||
form.reset();
|
form.reset();
|
||||||
const song = songs.find((s) => s.id === songId);
|
const song = songs.find((s) => s.id === songId);
|
||||||
if (song) song.tutorial_count += 1;
|
if (song) song.tutorial_count += 1;
|
||||||
|
|
|
||||||
3
src/db/migrations/017_song_tutorial_content.sql
Normal file
3
src/db/migrations/017_song_tutorial_content.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE song_tutorials
|
||||||
|
ALTER COLUMN url DROP NOT NULL,
|
||||||
|
ADD COLUMN content TEXT;
|
||||||
|
|
@ -48,7 +48,7 @@ async function remove(id) {
|
||||||
async function findTutorials(songId) {
|
async function findTutorials(songId) {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT st.id, st.song_id, st.instrument_id, i.name AS instrument_name,
|
`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
|
FROM song_tutorials st
|
||||||
JOIN instruments i ON i.id = st.instrument_id
|
JOIN instruments i ON i.id = st.instrument_id
|
||||||
WHERE st.song_id = $1
|
WHERE st.song_id = $1
|
||||||
|
|
@ -58,12 +58,12 @@ async function findTutorials(songId) {
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addTutorial(songId, { instrumentId, url, label, addedBy }) {
|
async function addTutorial(songId, { instrumentId, url, content, label, addedBy }) {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO song_tutorials (song_id, instrument_id, url, label, added_by)
|
`INSERT INTO song_tutorials (song_id, instrument_id, url, content, label, added_by)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id, song_id, instrument_id, url, label, added_by, created_at`,
|
RETURNING id, song_id, instrument_id, url, content, label, added_by, created_at`,
|
||||||
[songId, instrumentId, url, label || null, addedBy]
|
[songId, instrumentId, url || null, content || null, label || null, addedBy]
|
||||||
);
|
);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,15 +91,21 @@ router.delete(
|
||||||
router.post(
|
router.post(
|
||||||
'/:id/tutorials',
|
'/:id/tutorials',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
const { instrumentId, url, label } = req.body || {};
|
const { instrumentId, url, content, label } = req.body || {};
|
||||||
if (!instrumentId || !url || !url.trim()) {
|
const trimmedUrl = url && url.trim() ? url.trim() : null;
|
||||||
return res.status(400).json({ error: 'instrument_and_url_required' });
|
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);
|
const song = await songsRepo.findById(req.params.id);
|
||||||
if (!song) return res.status(404).json({ error: 'not_found' });
|
if (!song) return res.status(404).json({ error: 'not_found' });
|
||||||
const tutorial = await songsRepo.addTutorial(req.params.id, {
|
const tutorial = await songsRepo.addTutorial(req.params.id, {
|
||||||
instrumentId,
|
instrumentId,
|
||||||
url: url.trim(),
|
url: trimmedUrl,
|
||||||
|
content: trimmedContent,
|
||||||
label,
|
label,
|
||||||
addedBy: req.user.id,
|
addedBy: req.user.id,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue