2026-07-08 11:05:21 +02:00
|
|
|
let me = null;
|
|
|
|
|
let instruments = [];
|
|
|
|
|
let songs = [];
|
2026-07-08 17:20:40 +02:00
|
|
|
let searchQuery = '';
|
2026-07-08 11:05:21 +02:00
|
|
|
const expanded = new Set();
|
2026-07-08 11:50:12 +02:00
|
|
|
const editing = new Set();
|
2026-07-08 11:05:21 +02:00
|
|
|
|
2026-07-08 17:20:40 +02:00
|
|
|
function matchesSearch(song, query) {
|
|
|
|
|
const q = query.trim().toLowerCase();
|
|
|
|
|
if (!q) return true;
|
|
|
|
|
return song.title.toLowerCase().includes(q) || song.artist.toLowerCase().includes(q);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
async function loadInstruments() {
|
|
|
|
|
instruments = await api.get('/api/instruments');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadSongs() {
|
|
|
|
|
songs = await api.get('/api/songs');
|
|
|
|
|
renderSongs();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function instrumentOptions() {
|
|
|
|
|
return instruments.map((i) => `<option value="${i.id}">${escapeHtml(i.name)}</option>`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:50:12 +02:00
|
|
|
function songThumbTemplate(song) {
|
|
|
|
|
const thumb = song.youtube_url ? youtubeThumbnailUrl(song.youtube_url) : null;
|
|
|
|
|
if (thumb) {
|
|
|
|
|
return `
|
|
|
|
|
<div class="song-thumb">
|
|
|
|
|
<img src="${thumb}" alt="${escapeHtml(song.title)}" loading="lazy">
|
|
|
|
|
<a class="play-overlay" href="${escapeHtml(song.youtube_url)}" target="_blank" rel="noopener" title="Écouter sur YouTube">►</a>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
return `<div class="song-thumb placeholder">♫</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function songLinksTemplate(song) {
|
|
|
|
|
const links = [];
|
|
|
|
|
if (song.youtube_url) {
|
|
|
|
|
links.push(`<a class="pill-link youtube" href="${escapeHtml(song.youtube_url)}" target="_blank" rel="noopener">► YouTube</a>`);
|
|
|
|
|
}
|
|
|
|
|
if (song.spotify_url) {
|
|
|
|
|
links.push(`<a class="pill-link spotify" href="${escapeHtml(song.spotify_url)}" target="_blank" rel="noopener">♫ Spotify</a>`);
|
|
|
|
|
}
|
|
|
|
|
return links.length ? `<div class="song-links">${links.join('')}</div>` : '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
function songCardTemplate(song) {
|
2026-07-08 11:50:12 +02:00
|
|
|
if (editing.has(song.id)) return editSongCardTemplate(song);
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
const isOpen = expanded.has(song.id);
|
|
|
|
|
return `
|
|
|
|
|
<div class="card" data-song-id="${song.id}">
|
2026-07-08 11:50:12 +02:00
|
|
|
<div class="song-card">
|
|
|
|
|
${songThumbTemplate(song)}
|
|
|
|
|
<div class="song-body">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="card-title">${escapeHtml(song.title)}</div>
|
|
|
|
|
<div class="card-subtitle">${escapeHtml(song.artist)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
${song.notes ? `<p class="note">${escapeHtml(song.notes)}</p>` : ''}
|
|
|
|
|
${songLinksTemplate(song)}
|
|
|
|
|
<div class="song-links">
|
|
|
|
|
<button class="secondary icon-btn toggle-tutorials" data-id="${song.id}">
|
|
|
|
|
${isOpen ? 'Masquer les tutos' : `Tutos (${song.tutorial_count})`}
|
|
|
|
|
</button>
|
2026-07-08 13:30:14 +02:00
|
|
|
<button class="secondary icon-btn edit-song" data-id="${song.id}">Modifier</button>
|
2026-07-08 11:50:12 +02:00
|
|
|
</div>
|
2026-07-08 11:05:21 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-07-08 11:50:12 +02:00
|
|
|
<div class="tutorials-panel panel" style="${isOpen ? '' : 'display:none'}">
|
2026-07-08 13:30:14 +02:00
|
|
|
<div class="tutorial-grid" data-tutorials-for="${song.id}"><p class="empty">Chargement…</p></div>
|
2026-07-08 11:05:21 +02:00
|
|
|
<form class="inline-form add-tutorial-form" data-song-id="${song.id}">
|
|
|
|
|
<label>Instrument
|
|
|
|
|
<select name="instrumentId" required>${instrumentOptions()}</select>
|
|
|
|
|
</label>
|
2026-07-20 14:30:25 +02:00
|
|
|
<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>
|
2026-07-08 11:05:21 +02:00
|
|
|
<label>Libellé <input name="label" placeholder="ex: tuto solo"></label>
|
|
|
|
|
<button type="submit">Ajouter le tuto</button>
|
2026-07-08 13:30:14 +02:00
|
|
|
</form>
|
2026-07-08 11:05:21 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:50:12 +02:00
|
|
|
function editSongCardTemplate(song) {
|
|
|
|
|
return `
|
|
|
|
|
<div class="card" data-song-id="${song.id}">
|
|
|
|
|
<form class="stacked-form edit-song-form" data-id="${song.id}">
|
|
|
|
|
<label>Titre <input name="title" value="${escapeHtml(song.title)}" required></label>
|
|
|
|
|
<label>Artiste <input name="artist" value="${escapeHtml(song.artist)}" required></label>
|
|
|
|
|
<label>Notes <input name="notes" value="${escapeHtml(song.notes || '')}"></label>
|
|
|
|
|
<label>Lien YouTube <input name="youtubeUrl" type="url" value="${escapeHtml(song.youtube_url || '')}" placeholder="https://youtube.com/watch?v=..."></label>
|
|
|
|
|
<label>Lien Spotify <input name="spotifyUrl" type="url" value="${escapeHtml(song.spotify_url || '')}" placeholder="https://open.spotify.com/track/..."></label>
|
|
|
|
|
<div class="song-links">
|
|
|
|
|
<button type="submit">Enregistrer</button>
|
|
|
|
|
<button type="button" class="secondary cancel-edit" data-id="${song.id}">Annuler</button>
|
|
|
|
|
<button type="button" class="danger delete-song" data-id="${song.id}">Supprimer</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 14:30:25 +02:00
|
|
|
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>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:30:14 +02:00
|
|
|
function tutorialCardTemplate(t) {
|
2026-07-20 14:30:25 +02:00
|
|
|
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>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:30:14 +02:00
|
|
|
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">►</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tutorial-meta">
|
|
|
|
|
<span class="tag">${escapeHtml(t.instrument_name)}</span>
|
|
|
|
|
<div class="tutorial-label">${escapeHtml(t.label || 'Tuto')}</div>
|
|
|
|
|
</div>
|
2026-07-20 14:30:25 +02:00
|
|
|
${textBlock}
|
2026-07-08 13:30:14 +02:00
|
|
|
<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">🔗</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>
|
2026-07-20 14:30:25 +02:00
|
|
|
${textBlock}
|
2026-07-08 13:30:14 +02:00
|
|
|
<button class="secondary icon-btn remove-tutorial" data-song-id="${t.song_id}" data-id="${t.id}">Retirer</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
function renderSongs() {
|
|
|
|
|
const container = document.getElementById('songs-list');
|
2026-07-08 17:20:40 +02:00
|
|
|
const visibleSongs = songs.filter((s) => matchesSearch(s, searchQuery));
|
|
|
|
|
const emptyMessage = searchQuery.trim()
|
|
|
|
|
? 'Aucun morceau ne correspond à la recherche.'
|
|
|
|
|
: 'Aucun morceau au répertoire pour le moment.';
|
|
|
|
|
renderList(container, visibleSongs, songCardTemplate, emptyMessage);
|
2026-07-08 11:50:12 +02:00
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
document.querySelectorAll('.toggle-tutorials').forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', () => onToggleTutorials(parseInt(btn.dataset.id, 10)));
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.add-tutorial-form').forEach((form) => {
|
|
|
|
|
form.addEventListener('submit', onAddTutorial);
|
|
|
|
|
});
|
2026-07-08 11:50:12 +02:00
|
|
|
document.querySelectorAll('.edit-song').forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
editing.add(parseInt(btn.dataset.id, 10));
|
|
|
|
|
renderSongs();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.cancel-edit').forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
editing.delete(parseInt(btn.dataset.id, 10));
|
|
|
|
|
renderSongs();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.edit-song-form').forEach((form) => {
|
|
|
|
|
form.addEventListener('submit', onSaveSong);
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.delete-song').forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', () => onDeleteSong(parseInt(btn.dataset.id, 10)));
|
|
|
|
|
});
|
2026-07-08 11:05:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onToggleTutorials(songId) {
|
|
|
|
|
if (expanded.has(songId)) {
|
|
|
|
|
expanded.delete(songId);
|
|
|
|
|
} else {
|
|
|
|
|
expanded.add(songId);
|
|
|
|
|
}
|
|
|
|
|
renderSongs();
|
|
|
|
|
if (expanded.has(songId)) {
|
|
|
|
|
await loadTutorials(songId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadTutorials(songId) {
|
|
|
|
|
try {
|
|
|
|
|
const detail = await api.get(`/api/songs/${songId}`);
|
|
|
|
|
const container = document.querySelector(`[data-tutorials-for="${songId}"]`);
|
|
|
|
|
if (!container) return;
|
|
|
|
|
if (!detail.tutorials.length) {
|
|
|
|
|
container.innerHTML = '<p class="empty">Aucun lien pour le moment.</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-08 13:30:14 +02:00
|
|
|
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)));
|
|
|
|
|
});
|
2026-07-20 14:30:25 +02:00
|
|
|
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';
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-08 11:05:21 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
showError(err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onAddTutorial(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const form = e.target;
|
|
|
|
|
const songId = parseInt(form.dataset.songId, 10);
|
|
|
|
|
const instrumentId = parseInt(form.instrumentId.value, 10);
|
|
|
|
|
const url = form.url.value.trim();
|
2026-07-20 14:30:25 +02:00
|
|
|
const content = form.content.value.trim();
|
2026-07-08 11:05:21 +02:00
|
|
|
const label = form.label.value.trim();
|
2026-07-20 14:30:25 +02:00
|
|
|
if (!url && !content) {
|
|
|
|
|
showError('Renseignez un lien ou un texte pour le tuto.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-08 11:05:21 +02:00
|
|
|
try {
|
2026-07-20 14:30:25 +02:00
|
|
|
await api.post(`/api/songs/${songId}/tutorials`, { instrumentId, url, content, label });
|
2026-07-08 11:05:21 +02:00
|
|
|
form.reset();
|
|
|
|
|
const song = songs.find((s) => s.id === songId);
|
|
|
|
|
if (song) song.tutorial_count += 1;
|
|
|
|
|
await loadTutorials(songId);
|
|
|
|
|
renderSongs();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
showError(err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:30:14 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
async function onAddSong(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const form = e.target;
|
|
|
|
|
const title = form.title.value.trim();
|
|
|
|
|
const artist = form.artist.value.trim();
|
|
|
|
|
const notes = form.notes.value.trim();
|
2026-07-08 11:50:12 +02:00
|
|
|
const youtubeUrl = form.youtubeUrl.value.trim();
|
|
|
|
|
const spotifyUrl = form.spotifyUrl.value.trim();
|
2026-07-08 11:05:21 +02:00
|
|
|
try {
|
2026-07-08 11:50:12 +02:00
|
|
|
await api.post('/api/songs', { title, artist, notes, youtubeUrl, spotifyUrl });
|
2026-07-08 11:05:21 +02:00
|
|
|
form.reset();
|
2026-07-08 14:46:48 +02:00
|
|
|
document.getElementById('song-links-status').textContent = '';
|
2026-07-08 15:27:34 +02:00
|
|
|
songAutocomplete.close();
|
2026-07-08 14:46:48 +02:00
|
|
|
setAddSongPanelOpen(false);
|
2026-07-08 11:05:21 +02:00
|
|
|
await loadSongs();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
showError(err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 14:46:48 +02:00
|
|
|
function setAddSongPanelOpen(open) {
|
|
|
|
|
document.getElementById('add-song-panel').style.display = open ? 'block' : 'none';
|
|
|
|
|
document.getElementById('toggle-add-song').textContent = open ? 'Annuler' : '+ Ajouter un morceau';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:50:12 +02:00
|
|
|
async function onSaveSong(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const form = e.target;
|
|
|
|
|
const id = parseInt(form.dataset.id, 10);
|
|
|
|
|
const title = form.title.value.trim();
|
|
|
|
|
const artist = form.artist.value.trim();
|
|
|
|
|
const notes = form.notes.value.trim();
|
|
|
|
|
const youtubeUrl = form.youtubeUrl.value.trim();
|
|
|
|
|
const spotifyUrl = form.spotifyUrl.value.trim();
|
|
|
|
|
try {
|
|
|
|
|
await api.patch(`/api/songs/${id}`, { title, artist, notes, youtubeUrl, spotifyUrl });
|
|
|
|
|
editing.delete(id);
|
|
|
|
|
await loadSongs();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
showError(err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onDeleteSong(id) {
|
|
|
|
|
try {
|
|
|
|
|
await api.del(`/api/songs/${id}`);
|
|
|
|
|
editing.delete(id);
|
|
|
|
|
await loadSongs();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
showError(err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
function showError(message) {
|
|
|
|
|
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:27:34 +02:00
|
|
|
let songAutocomplete = null;
|
2026-07-08 14:46:48 +02:00
|
|
|
|
2026-07-08 15:27:34 +02:00
|
|
|
async function onSongCandidateSelected(candidate) {
|
2026-07-08 14:46:48 +02:00
|
|
|
document.getElementById('song-title-input').value = candidate.title;
|
|
|
|
|
document.getElementById('song-artist-input').value = candidate.artist;
|
|
|
|
|
|
|
|
|
|
const statusEl = document.getElementById('song-links-status');
|
|
|
|
|
statusEl.textContent = 'Recherche des liens YouTube / Spotify…';
|
|
|
|
|
try {
|
|
|
|
|
const links = await api.get(
|
|
|
|
|
`/api/music-search/links?title=${encodeURIComponent(candidate.title)}&artist=${encodeURIComponent(candidate.artist)}`
|
|
|
|
|
);
|
|
|
|
|
const youtubeInput = document.getElementById('song-youtube-input');
|
|
|
|
|
const spotifyInput = document.getElementById('song-spotify-input');
|
|
|
|
|
if (links.youtubeUrl && !youtubeInput.value) youtubeInput.value = links.youtubeUrl;
|
|
|
|
|
if (links.spotifyUrl && !spotifyInput.value) spotifyInput.value = links.spotifyUrl;
|
|
|
|
|
|
|
|
|
|
if (links.youtubeUrl && links.spotifyUrl) statusEl.textContent = 'Liens YouTube et Spotify trouvés automatiquement.';
|
|
|
|
|
else if (links.youtubeUrl) statusEl.textContent = 'Lien YouTube trouvé automatiquement. Aucun lien Spotify trouvé — à saisir manuellement si besoin.';
|
|
|
|
|
else if (links.spotifyUrl) statusEl.textContent = 'Lien Spotify trouvé automatiquement. Aucun lien YouTube trouvé — à saisir manuellement si besoin.';
|
|
|
|
|
else statusEl.textContent = 'Aucun lien trouvé automatiquement — vous pouvez les saisir manuellement.';
|
|
|
|
|
} catch (err) {
|
|
|
|
|
statusEl.textContent = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:05:21 +02:00
|
|
|
(async function init() {
|
|
|
|
|
me = await initNav('repertoire');
|
|
|
|
|
await loadInstruments();
|
2026-07-08 13:30:14 +02:00
|
|
|
document.getElementById('add-song-form').addEventListener('submit', onAddSong);
|
2026-07-08 14:46:48 +02:00
|
|
|
document.getElementById('toggle-add-song').addEventListener('click', () => {
|
|
|
|
|
const isOpen = document.getElementById('add-song-panel').style.display !== 'none';
|
|
|
|
|
setAddSongPanelOpen(!isOpen);
|
|
|
|
|
});
|
2026-07-08 15:27:34 +02:00
|
|
|
songAutocomplete = createTitleAutocomplete({
|
|
|
|
|
inputId: 'song-title-input',
|
|
|
|
|
dropdownId: 'song-title-dropdown',
|
|
|
|
|
onSelect: onSongCandidateSelected,
|
|
|
|
|
});
|
2026-07-08 17:20:40 +02:00
|
|
|
document.getElementById('song-search-input').addEventListener('input', (e) => {
|
|
|
|
|
searchQuery = e.target.value;
|
|
|
|
|
renderSongs();
|
|
|
|
|
});
|
2026-07-08 11:05:21 +02:00
|
|
|
await loadSongs();
|
|
|
|
|
})();
|