mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: autosearch for musics
This commit is contained in:
parent
9426b73940
commit
6d8fe814b0
12 changed files with 380 additions and 12 deletions
|
|
@ -289,6 +289,17 @@ h3 { font-size: 1rem; margin: 0 0 0.5rem; }
|
|||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin: 2.25rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.section-header h2 { margin: 0; }
|
||||
|
||||
/* ---------- Cards / grids ---------- */
|
||||
|
||||
.card-grid {
|
||||
|
|
@ -491,6 +502,56 @@ form.stacked-form label {
|
|||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
/* ---------- Title autocomplete (add song) ---------- */
|
||||
|
||||
.autocomplete-wrapper { position: relative; }
|
||||
|
||||
.autocomplete-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0.25rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover,
|
||||
.autocomplete-item.active {
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.autocomplete-item img,
|
||||
.autocomplete-thumb-placeholder {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.autocomplete-thumb-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface-alt);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
font: inherit;
|
||||
padding: 0.5rem 0.65rem;
|
||||
|
|
|
|||
|
|
@ -16,19 +16,28 @@
|
|||
<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 class="panel">
|
||||
<h2 style="margin-top:0">Ajouter un morceau</h2>
|
||||
<div class="section-header">
|
||||
<h2 style="margin:0">Morceaux travaillés</h2>
|
||||
<button type="button" class="secondary" id="toggle-add-song">+ Ajouter un morceau</button>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="add-song-panel" style="display:none">
|
||||
<form id="add-song-form" class="stacked-form">
|
||||
<label>Titre <input name="title" required></label>
|
||||
<label>Artiste <input name="artist" required></label>
|
||||
<div class="autocomplete-wrapper">
|
||||
<label>Titre
|
||||
<input name="title" id="song-title-input" autocomplete="off" required>
|
||||
</label>
|
||||
<div class="autocomplete-dropdown" id="song-title-dropdown" style="display:none"></div>
|
||||
</div>
|
||||
<label>Artiste <input name="artist" id="song-artist-input" required></label>
|
||||
<label>Notes <input name="notes"></label>
|
||||
<label>Lien YouTube <input name="youtubeUrl" type="url" placeholder="https://youtube.com/watch?v=..."></label>
|
||||
<label>Lien Spotify <input name="spotifyUrl" type="url" placeholder="https://open.spotify.com/track/..."></label>
|
||||
<label>Lien YouTube <input name="youtubeUrl" id="song-youtube-input" type="url" placeholder="https://youtube.com/watch?v=..."></label>
|
||||
<label>Lien Spotify <input name="spotifyUrl" id="song-spotify-input" type="url" placeholder="https://open.spotify.com/track/..."></label>
|
||||
<p class="note" id="song-links-status"></p>
|
||||
<button type="submit">Ajouter au répertoire</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2>Morceaux travaillés</h2>
|
||||
<div id="songs-list" class="card-grid"></div>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ function applyTheme(theme) {
|
|||
updateThemeToggleIcon();
|
||||
}
|
||||
|
||||
const SUN_ICON = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>`;
|
||||
const MOON_ICON = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`;
|
||||
|
||||
function updateThemeToggleIcon() {
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (!btn) return;
|
||||
btn.textContent = currentTheme() === 'dark' ? '☀️' : '🌙';
|
||||
btn.innerHTML = currentTheme() === 'dark' ? SUN_ICON : MOON_ICON;
|
||||
}
|
||||
|
||||
async function initNav(activePage) {
|
||||
|
|
|
|||
|
|
@ -231,12 +231,20 @@ async function onAddSong(e) {
|
|||
try {
|
||||
await api.post('/api/songs', { title, artist, notes, youtubeUrl, spotifyUrl });
|
||||
form.reset();
|
||||
document.getElementById('song-links-status').textContent = '';
|
||||
closeAutocompleteDropdown();
|
||||
setAddSongPanelOpen(false);
|
||||
await loadSongs();
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function setAddSongPanelOpen(open) {
|
||||
document.getElementById('add-song-panel').style.display = open ? 'block' : 'none';
|
||||
document.getElementById('toggle-add-song').textContent = open ? 'Annuler' : '+ Ajouter un morceau';
|
||||
}
|
||||
|
||||
async function onSaveSong(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
|
|
@ -269,9 +277,125 @@ function showError(message) {
|
|||
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
||||
}
|
||||
|
||||
let autocompleteTimer = null;
|
||||
let autocompleteCandidates = [];
|
||||
let autocompleteHighlight = -1;
|
||||
|
||||
function candidateItemTemplate(c, index) {
|
||||
const img = c.artworkUrl
|
||||
? `<img src="${escapeHtml(c.artworkUrl)}" alt="">`
|
||||
: `<span class="autocomplete-thumb-placeholder">♫</span>`;
|
||||
return `
|
||||
<div class="autocomplete-item${index === autocompleteHighlight ? ' active' : ''}" data-index="${index}">
|
||||
${img}
|
||||
<div>
|
||||
<div>${escapeHtml(c.title)}</div>
|
||||
<div class="card-subtitle">${escapeHtml(c.artist)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAutocompleteDropdown() {
|
||||
const dropdown = document.getElementById('song-title-dropdown');
|
||||
if (!autocompleteCandidates.length) {
|
||||
dropdown.style.display = 'none';
|
||||
dropdown.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = autocompleteCandidates.map(candidateItemTemplate).join('');
|
||||
dropdown.style.display = 'block';
|
||||
dropdown.querySelectorAll('.autocomplete-item').forEach((el) => {
|
||||
el.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
selectCandidate(autocompleteCandidates[parseInt(el.dataset.index, 10)]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeAutocompleteDropdown() {
|
||||
autocompleteCandidates = [];
|
||||
autocompleteHighlight = -1;
|
||||
renderAutocompleteDropdown();
|
||||
}
|
||||
|
||||
async function selectCandidate(candidate) {
|
||||
document.getElementById('song-title-input').value = candidate.title;
|
||||
document.getElementById('song-artist-input').value = candidate.artist;
|
||||
closeAutocompleteDropdown();
|
||||
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
function initTitleAutocomplete() {
|
||||
const input = document.getElementById('song-title-input');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const query = input.value.trim();
|
||||
clearTimeout(autocompleteTimer);
|
||||
if (query.length < 2) {
|
||||
closeAutocompleteDropdown();
|
||||
return;
|
||||
}
|
||||
autocompleteTimer = setTimeout(async () => {
|
||||
try {
|
||||
autocompleteCandidates = await api.get(`/api/music-search?q=${encodeURIComponent(query)}`);
|
||||
autocompleteHighlight = -1;
|
||||
renderAutocompleteDropdown();
|
||||
} catch (err) {
|
||||
closeAutocompleteDropdown();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (!autocompleteCandidates.length) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
autocompleteHighlight = Math.min(autocompleteHighlight + 1, autocompleteCandidates.length - 1);
|
||||
renderAutocompleteDropdown();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
autocompleteHighlight = Math.max(autocompleteHighlight - 1, 0);
|
||||
renderAutocompleteDropdown();
|
||||
} else if (e.key === 'Enter' && autocompleteHighlight >= 0) {
|
||||
e.preventDefault();
|
||||
selectCandidate(autocompleteCandidates[autocompleteHighlight]);
|
||||
} else if (e.key === 'Escape') {
|
||||
closeAutocompleteDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(closeAutocompleteDropdown, 150);
|
||||
});
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
me = await initNav('repertoire');
|
||||
await loadInstruments();
|
||||
document.getElementById('add-song-form').addEventListener('submit', onAddSong);
|
||||
document.getElementById('toggle-add-song').addEventListener('click', () => {
|
||||
const isOpen = document.getElementById('add-song-panel').style.display !== 'none';
|
||||
setAddSongPanelOpen(!isOpen);
|
||||
});
|
||||
initTitleAutocomplete();
|
||||
await loadSongs();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -192,12 +192,18 @@ async function onAddSuggestion(e) {
|
|||
try {
|
||||
await api.post('/api/suggestions', { title, artist, youtubeUrl, description });
|
||||
form.reset();
|
||||
setAddSuggestionPanelOpen(false);
|
||||
await loadSuggestions();
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function setAddSuggestionPanelOpen(open) {
|
||||
document.getElementById('add-suggestion-panel').style.display = open ? 'block' : 'none';
|
||||
document.getElementById('toggle-add-suggestion').textContent = open ? 'Annuler' : '+ Proposer un morceau';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
||||
}
|
||||
|
|
@ -205,5 +211,9 @@ function showError(message) {
|
|||
(async function init() {
|
||||
me = await initNav('suggestions');
|
||||
document.getElementById('add-suggestion-form').addEventListener('submit', onAddSuggestion);
|
||||
document.getElementById('toggle-add-suggestion').addEventListener('click', () => {
|
||||
const isOpen = document.getElementById('add-suggestion-panel').style.display !== 'none';
|
||||
setAddSuggestionPanelOpen(!isOpen);
|
||||
});
|
||||
await loadSuggestions();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@
|
|||
<p class="page-subtitle">Proposez un morceau et votez sur les suggestions des autres — chaque vote est nominatif.</p>
|
||||
<div id="error"></div>
|
||||
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0">Proposer un morceau</h2>
|
||||
<div class="section-header">
|
||||
<h2 style="margin:0">Toutes les suggestions</h2>
|
||||
<button type="button" class="secondary" id="toggle-add-suggestion">+ Proposer un morceau</button>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="add-suggestion-panel" style="display:none">
|
||||
<form id="add-suggestion-form" class="stacked-form">
|
||||
<label>Titre <input name="title" required></label>
|
||||
<label>Artiste <input name="artist"></label>
|
||||
|
|
@ -27,7 +31,6 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<h2>Toutes les suggestions</h2>
|
||||
<div id="suggestions-list" class="card-grid"></div>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue