From e97aa2c6c39fa61f5575cf637df285a0b11a167e Mon Sep 17 00:00:00 2001 From: Nathan FONTEYNE Date: Wed, 8 Jul 2026 11:50:12 +0200 Subject: [PATCH] update: visual update, phone responsive, db migration --- public/css/style.css | 529 +++++++++++++++--- public/history.html | 3 +- public/index.html | 13 +- public/js/history-detail.js | 12 +- public/js/nav.js | 10 +- public/js/render.js | 27 +- public/js/repertoire.js | 124 +++- public/js/setlist.js | 259 +++++---- public/js/suggestions.js | 45 +- public/suggestions.html | 20 +- .../003_song_links_and_suggestion_note.sql | 3 + src/lib/spotify.js | 10 + src/repositories/songsRepo.js | 23 +- src/repositories/suggestionsRepo.js | 24 +- src/routes/songs.js | 33 +- src/routes/suggestions.js | 3 +- 16 files changed, 894 insertions(+), 244 deletions(-) create mode 100644 src/db/migrations/003_song_links_and_suggestion_note.sql create mode 100644 src/lib/spotify.js diff --git a/public/css/style.css b/public/css/style.css index d0a7f50..2e0bbcd 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,172 +1,421 @@ :root { - --bg: #f7f7f9; + --bg: #f4f3fb; --surface: #ffffff; - --text: #1c1c1e; - --muted: #6b6b70; - --border: #e2e2e6; - --accent: #3457d5; + --surface-alt: #f7f6fd; + --text: #201f2b; + --muted: #6b6878; + --border: #e4e1f0; + --accent: #6c4cf0; + --accent-2: #ff5f8f; --accent-contrast: #ffffff; - --danger: #c0392b; - --success: #1e8e3e; - font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --danger: #d1453d; + --danger-bg: #fdecea; + --success: #1e8e5a; + --success-bg: #e8f7ef; + --radius: 14px; + --radius-sm: 8px; + --shadow: 0 1px 2px rgba(32, 31, 43, 0.04), 0 4px 16px rgba(32, 31, 43, 0.06); + font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; } @media (prefers-color-scheme: dark) { :root { - --bg: #121214; - --surface: #1c1c1f; - --text: #ececef; - --muted: #9a9aa2; - --border: #2e2e33; - --accent: #7189e8; - --accent-contrast: #0d0d0f; + --bg: #15131f; + --surface: #1e1c2b; + --surface-alt: #262436; + --text: #ece9f7; + --muted: #a29fb5; + --border: #322f42; + --accent: #9884f5; + --accent-2: #ff85ac; + --accent-contrast: #15131f; + --danger: #ef6b64; + --danger-bg: #3a201f; + --success: #4fd190; + --success-bg: #163828; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 8px 24px rgba(0, 0, 0, 0.35); } } +:root[data-theme="dark"] { + --bg: #15131f; + --surface: #1e1c2b; + --surface-alt: #262436; + --text: #ece9f7; + --muted: #a29fb5; + --border: #322f42; + --accent: #9884f5; + --accent-2: #ff85ac; + --accent-contrast: #15131f; + --danger: #ef6b64; + --danger-bg: #3a201f; + --success: #4fd190; + --success-bg: #163828; +} + +:root[data-theme="light"] { + --bg: #f4f3fb; + --surface: #ffffff; + --surface-alt: #f7f6fd; + --text: #201f2b; + --muted: #6b6878; + --border: #e4e1f0; + --accent: #6c4cf0; + --accent-2: #ff5f8f; + --accent-contrast: #ffffff; + --danger: #d1453d; + --danger-bg: #fdecea; + --success: #1e8e5a; + --success-bg: #e8f7ef; +} + * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); + -webkit-font-smoothing: antialiased; } +/* ---------- Header / nav ---------- */ + header.app-header { + position: sticky; + top: 0; + z-index: 20; background: var(--surface); border-bottom: 1px solid var(--border); - padding: 0.75rem 1.5rem; + padding: 0 1.25rem; } nav#main-nav { + max-width: 1100px; + margin: 0 auto; display: flex; - justify-content: space-between; align-items: center; + justify-content: space-between; + gap: 1rem; + min-height: 60px; + flex-wrap: wrap; +} + +.brand { + font-weight: 700; + font-size: 1.1rem; + letter-spacing: 0.01em; + color: var(--text); + display: flex; + align-items: center; + gap: 0.4rem; + margin-right: auto; +} + +.brand .brand-dot { + color: var(--accent); +} + +.nav-toggle { + display: none; + background: transparent; + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius-sm); + padding: 0.4rem 0.6rem; + font-size: 1.1rem; + line-height: 1; +} + +.nav-links { + display: flex; + align-items: center; + gap: 1.4rem; flex-wrap: wrap; - gap: 0.5rem; } .nav-links a { - margin-right: 1rem; color: var(--muted); text-decoration: none; font-weight: 500; + font-size: 0.95rem; + padding: 0.3rem 0; + border-bottom: 2px solid transparent; } -.nav-links a.active, .nav-links a:hover { +.nav-links a.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.nav-links a:hover { color: var(--accent); } .nav-user { display: flex; align-items: center; - gap: 1rem; + gap: 0.9rem; color: var(--muted); - font-size: 0.9rem; + font-size: 0.88rem; + margin-left: auto; } .nav-user a { color: var(--accent); text-decoration: none; + font-weight: 500; } .badge { - background: var(--accent); - color: var(--accent-contrast); + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + color: #fff; border-radius: 999px; - padding: 0.1rem 0.5rem; - font-size: 0.75rem; + padding: 0.1rem 0.55rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; } +/* ---------- Layout ---------- */ + main { - max-width: 900px; + max-width: 1100px; margin: 0 auto; - padding: 1.5rem; + padding: 1.75rem 1.25rem 4rem; } -h1 { font-size: 1.5rem; } -h2 { font-size: 1.15rem; margin-top: 2rem; } +h1 { + font-size: 1.65rem; + margin: 0 0 0.25rem; + letter-spacing: -0.01em; +} + +h2 { + font-size: 1.15rem; + margin: 2.25rem 0 0.75rem; +} + +h3 { font-size: 1rem; margin: 0 0 0.5rem; } + +.page-subtitle { + color: var(--muted); + margin: 0 0 1.5rem; +} + +/* ---------- Cards / grids ---------- */ + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.9rem; +} .card { background: var(--surface); border: 1px solid var(--border); - border-radius: 10px; - padding: 1rem 1.25rem; + border-radius: var(--radius); + padding: 1rem 1.15rem; margin-bottom: 0.75rem; + box-shadow: var(--shadow); } +.card-grid .card { margin-bottom: 0; } + .card-header { display: flex; justify-content: space-between; - align-items: baseline; - gap: 1rem; + align-items: flex-start; + gap: 0.75rem; } -.card-title { font-weight: 600; } -.card-subtitle { color: var(--muted); font-size: 0.9rem; } +.card-title { font-weight: 600; font-size: 1.02rem; } +.card-subtitle { color: var(--muted); font-size: 0.88rem; margin-top: 0.1rem; } -.tag-list { +/* ---------- Song card (repertoire) ---------- */ + +.song-card { + display: flex; + gap: 0.9rem; + align-items: flex-start; +} + +.song-thumb { + flex: 0 0 auto; + width: 96px; + height: 72px; + border-radius: var(--radius-sm); + overflow: hidden; + background: var(--surface-alt); + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.song-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.song-thumb.placeholder { + color: var(--muted); + font-size: 1.5rem; +} + +.song-thumb a.play-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.15); + color: #fff; + font-size: 1.4rem; + text-decoration: none; + opacity: 0; + transition: opacity 0.15s ease; +} + +.song-thumb:hover a.play-overlay { opacity: 1; } + +.song-body { + flex: 1 1 auto; + min-width: 0; +} + +.song-links { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; } -.tag { - background: var(--bg); +.pill-link { + display: inline-flex; + align-items: center; + gap: 0.3rem; + font-size: 0.78rem; + font-weight: 600; + text-decoration: none; + padding: 0.25rem 0.65rem; + border-radius: 999px; border: 1px solid var(--border); - border-radius: 6px; - padding: 0.15rem 0.5rem; - font-size: 0.8rem; + color: var(--text); + background: var(--surface-alt); } -.tag a { color: var(--accent); text-decoration: none; } +.pill-link.youtube { color: #d1272b; border-color: #f3c8c9; } +.pill-link.spotify { color: #1db954; border-color: #bdeccb; } + +@media (prefers-color-scheme: dark) { + .pill-link.youtube { border-color: #5a2b2b; } + .pill-link.spotify { border-color: #205934; } +} + +/* ---------- Tags (tutorials / instruments) ---------- */ + +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.6rem; +} + +.tag { + background: var(--surface-alt); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.2rem 0.65rem; + font-size: 0.78rem; +} + +.tag a { color: var(--accent); text-decoration: none; font-weight: 500; } .empty { color: var(--muted); font-style: italic; + font-size: 0.92rem; } +/* ---------- Forms ---------- */ + form.inline-form { display: flex; flex-wrap: wrap; - gap: 0.5rem; - margin-top: 0.75rem; + gap: 0.7rem; + margin-top: 0.85rem; align-items: flex-end; } form.inline-form label { + display: flex; + flex-direction: column; + font-size: 0.78rem; + color: var(--muted); + gap: 0.3rem; + flex: 1 1 160px; +} + +form.stacked-form { + display: flex; + flex-direction: column; + gap: 0.7rem; + max-width: 480px; +} + +form.stacked-form label { display: flex; flex-direction: column; font-size: 0.8rem; color: var(--muted); - gap: 0.25rem; + gap: 0.3rem; } input, select, textarea { font: inherit; - padding: 0.4rem 0.5rem; + padding: 0.5rem 0.65rem; border: 1px solid var(--border); - border-radius: 6px; + border-radius: var(--radius-sm); background: var(--surface); color: var(--text); + width: 100%; +} + +textarea { resize: vertical; min-height: 4.5em; } + +input:focus, select:focus, textarea:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; } button { font: inherit; - padding: 0.45rem 0.9rem; + font-weight: 600; + padding: 0.5rem 1rem; border: none; - border-radius: 6px; + border-radius: 999px; background: var(--accent); color: var(--accent-contrast); cursor: pointer; + white-space: nowrap; } +button:hover { filter: brightness(1.07); } + button.secondary { background: transparent; border: 1px solid var(--border); color: var(--text); } +button.icon-btn { + padding: 0.35rem 0.55rem; + font-size: 0.85rem; +} + button.danger { background: var(--danger); color: #fff; @@ -177,78 +426,222 @@ button:disabled { cursor: not-allowed; } +.panel { + background: var(--surface-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.15rem; + margin-top: 0.75rem; +} + +/* ---------- Suggestions / voting ---------- */ + .vote-tally { display: flex; gap: 1rem; - font-size: 0.9rem; + font-size: 0.88rem; margin-top: 0.5rem; + font-weight: 600; } .vote-tally .approve { color: var(--success); } .vote-tally .reject { color: var(--danger); } .vote-list { - margin-top: 0.5rem; + margin-top: 0.75rem; border-top: 1px solid var(--border); - padding-top: 0.5rem; + padding-top: 0.6rem; + display: flex; + flex-direction: column; + gap: 0.5rem; } .vote-item { font-size: 0.85rem; - margin-bottom: 0.35rem; + background: var(--surface-alt); + border-radius: var(--radius-sm); + padding: 0.45rem 0.65rem; } .vote-item .voter { font-weight: 600; } +.vote-item.approve { border-left: 3px solid var(--success); } +.vote-item.reject { border-left: 3px solid var(--danger); } + +.suggestion-note { + margin-top: 0.5rem; + font-size: 0.9rem; + background: var(--surface-alt); + border-radius: var(--radius-sm); + padding: 0.5rem 0.7rem; +} .youtube-embed { margin-top: 0.75rem; aspect-ratio: 16 / 9; width: 100%; max-width: 480px; + border-radius: var(--radius-sm); + overflow: hidden; + background: var(--surface-alt); } .youtube-embed iframe { width: 100%; height: 100%; border: 0; - border-radius: 8px; } -.setlist-section h3 { - margin-bottom: 0.4rem; +/* ---------- Setlist ---------- */ + +.setlist-columns { + display: grid; + grid-template-columns: 1fr 1.4fr; + gap: 1.25rem; + align-items: start; +} + +.setlist-section { margin-top: 1.5rem; } +.setlist-section h3 { margin-bottom: 0.5rem; } + +.setlist-rows { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.setlist-row { + display: flex; + align-items: center; + gap: 0.6rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.55rem 0.75rem; +} + +.setlist-row .row-index { + font-weight: 700; + color: var(--muted); + width: 1.5rem; + text-align: center; + flex: 0 0 auto; +} + +.setlist-row .row-main { + flex: 1 1 auto; + min-width: 0; +} + +.setlist-row .row-title { font-weight: 600; } +.setlist-row .row-artist { color: var(--muted); font-size: 0.85rem; } + +.setlist-row .row-note-input { + flex: 1 1 160px; +} + +.setlist-row .row-actions { + display: flex; + gap: 0.25rem; + flex: 0 0 auto; } ol.setlist { - padding-left: 1.25rem; + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; } ol.setlist li { - margin-bottom: 0.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.6rem 0.8rem; } .note { color: var(--muted); font-size: 0.85rem; display: block; + margin-top: 0.15rem; } +/* ---------- Misc ---------- */ + .error-banner { - background: #fdecea; + background: var(--danger-bg); color: var(--danger); border: 1px solid var(--danger); - border-radius: 8px; + border-radius: var(--radius-sm); padding: 0.6rem 0.9rem; - margin-bottom: 1rem; -} - -@media (prefers-color-scheme: dark) { - .error-banner { - background: #3a1f1c; - } + margin-bottom: 1.25rem; + font-size: 0.9rem; } a.back-link { color: var(--muted); text-decoration: none; font-size: 0.9rem; + display: inline-block; + margin-bottom: 1rem; +} + +a.back-link:hover { color: var(--accent); } + +/* ---------- Responsive / mobile ---------- */ + +@media (max-width: 720px) { + .setlist-columns { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + nav#main-nav { min-height: 56px; } + + .nav-toggle { display: inline-flex; } + + .nav-links { + order: 3; + flex-basis: 100%; + flex-direction: column; + align-items: flex-start; + gap: 0.1rem; + max-height: 0; + overflow: hidden; + transition: max-height 0.2s ease; + } + + .nav-links.open { + max-height: 260px; + padding: 0.4rem 0 0.8rem; + } + + .nav-links a { + width: 100%; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); + } + + .nav-user { + margin-left: 0; + } + + main { padding: 1.25rem 0.9rem 3rem; } + + .card-grid { grid-template-columns: 1fr; } + + .song-card { flex-wrap: wrap; } + .song-thumb { width: 100%; height: 160px; } + + form.inline-form { flex-direction: column; align-items: stretch; } + form.inline-form label { flex: 1 1 auto; } + + .setlist-row { + flex-wrap: wrap; + } + .setlist-row .row-note-input { flex-basis: 100%; order: 4; } + .setlist-row .row-actions { flex-basis: 100%; order: 5; justify-content: flex-end; } } diff --git a/public/history.html b/public/history.html index fbecc9f..7c7e195 100644 --- a/public/history.html +++ b/public/history.html @@ -12,8 +12,9 @@

Historique des concerts

+

Retrouvez la setlist jouée à chaque concert précédent.

-
+
diff --git a/public/index.html b/public/index.html index cbeb3c1..5d07b4c 100644 --- a/public/index.html +++ b/public/index.html @@ -12,20 +12,23 @@

Répertoire

+

Les morceaux travaillés par le groupe, avec liens d'écoute et tutos par instrument.

-
diff --git a/public/js/history-detail.js b/public/js/history-detail.js index 50eb5f4..fe13187 100644 --- a/public/js/history-detail.js +++ b/public/js/history-detail.js @@ -20,19 +20,23 @@ function showError(message) { const setlist = await api.get(`/api/setlists/${id}`); const main = setlist.songs.filter((s) => !s.is_encore); const encore = setlist.songs.filter((s) => s.is_encore); + const rowHtml = (s) => `
  • ${escapeHtml(s.title)} — ${escapeHtml(s.artist)}${s.note ? `${escapeHtml(s.note)}` : ''}
  • `; + container.innerHTML = ` -

    ${escapeHtml(setlist.name || 'Concert')}

    -

    ${escapeHtml(setlist.venue || '')} · ${formatDate(setlist.concert_date)}

    +
    +
    ${escapeHtml(setlist.name || 'Concert')}
    +
    ${escapeHtml(setlist.venue || '')} · ${formatDate(setlist.concert_date)}
    +

    Setlist

    ${main.length - ? `
      ${main.map((s) => `
    1. ${escapeHtml(s.title)} — ${escapeHtml(s.artist)}${s.note ? `${escapeHtml(s.note)}` : ''}
    2. `).join('')}
    ` + ? `
      ${main.map(rowHtml).join('')}
    ` : '

    Aucun morceau enregistré.

    '}

    Rappel

    ${encore.length - ? `
      ${encore.map((s) => `
    1. ${escapeHtml(s.title)} — ${escapeHtml(s.artist)}${s.note ? `${escapeHtml(s.note)}` : ''}
    2. `).join('')}
    ` + ? `
      ${encore.map(rowHtml).join('')}
    ` : '

    Aucun rappel enregistré.

    '}
    `; diff --git a/public/js/nav.js b/public/js/nav.js index 82bcf97..d96dd45 100644 --- a/public/js/nav.js +++ b/public/js/nav.js @@ -2,7 +2,9 @@ async function initNav(activePage) { const me = await api.get('/api/users/me'); const nav = document.getElementById('main-nav'); nav.innerHTML = ` -