+
${escapeHtml(setlist.name || 'Concert')}
+
${escapeHtml(setlist.venue || '')} · ${formatDate(setlist.concert_date)}
+
+
♫ Octane
+
+
`;
+
+ const toggle = document.getElementById('nav-toggle');
+ const links = document.getElementById('nav-links');
+ toggle.addEventListener('click', () => links.classList.toggle('open'));
+ links.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => links.classList.remove('open')));
+
return me;
}
diff --git a/public/js/render.js b/public/js/render.js
index 9bac909..3f72145 100644
--- a/public/js/render.js
+++ b/public/js/render.js
@@ -12,19 +12,30 @@ function escapeHtml(str) {
}[c]));
}
-function youtubeEmbedUrl(url) {
+function youtubeVideoId(url) {
try {
const parsed = new URL(url);
- let videoId = null;
if (parsed.hostname === 'youtu.be') {
- videoId = parsed.pathname.slice(1);
- } else if (parsed.pathname === '/watch') {
- videoId = parsed.searchParams.get('v');
- } else if (parsed.pathname.startsWith('/embed/')) {
- videoId = parsed.pathname.split('/embed/')[1];
+ return parsed.pathname.slice(1) || null;
}
- return videoId ? `https://www.youtube.com/embed/${videoId}` : null;
+ if (parsed.pathname === '/watch') {
+ return parsed.searchParams.get('v');
+ }
+ if (parsed.pathname.startsWith('/embed/')) {
+ return parsed.pathname.split('/embed/')[1] || null;
+ }
+ return null;
} catch {
return null;
}
}
+
+function youtubeEmbedUrl(url) {
+ const videoId = youtubeVideoId(url);
+ return videoId ? `https://www.youtube.com/embed/${videoId}` : null;
+}
+
+function youtubeThumbnailUrl(url) {
+ const videoId = youtubeVideoId(url);
+ return videoId ? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg` : null;
+}
diff --git a/public/js/repertoire.js b/public/js/repertoire.js
index e7567a2..779b82f 100644
--- a/public/js/repertoire.js
+++ b/public/js/repertoire.js
@@ -2,6 +2,7 @@ let me = null;
let instruments = [];
let songs = [];
const expanded = new Set();
+const editing = new Set();
async function loadInstruments() {
instruments = await api.get('/api/instruments');
@@ -16,21 +17,56 @@ function instrumentOptions() {
return instruments.map((i) => `
`).join('');
}
+function songThumbTemplate(song) {
+ const thumb = song.youtube_url ? youtubeThumbnailUrl(song.youtube_url) : null;
+ if (thumb) {
+ return `
+
+

+
►
+
+ `;
+ }
+ return `
♫
`;
+}
+
+function songLinksTemplate(song) {
+ const links = [];
+ if (song.youtube_url) {
+ links.push(`
► YouTube`);
+ }
+ if (song.spotify_url) {
+ links.push(`
♫ Spotify`);
+ }
+ return links.length ? `
${links.join('')}
` : '';
+}
+
function songCardTemplate(song) {
+ if (editing.has(song.id)) return editSongCardTemplate(song);
+
const isOpen = expanded.has(song.id);
return `
-