Apply the theme: tokens, Oswald, song cards, toasts

The theme handoff encoded as CSS custom properties rather than a Tailwind
config, since there is no Tailwind here. Palette, spacing, radii, shadows and
motion follow it as written; docs/theme.md lists what differs and why.

- Oswald vendored as a 21KB variable woff2, latin subset, no CDN. Its phantom
  weight 900 resolved to 700 — loading a weight you don't have is what made
  the brand render differently per platform
- color-scheme: dark makes the native audio element fit the palette, which
  was the handoff's complaint about it
- Songs are text cards rather than artwork tiles, because there is no
  artwork. The unreviewed state keeps its red-brown border and gains a badge,
  so it is never carried by colour alone
- Nav is the three-column grid; the mobile menu is <details>, no JS
- Flash messages became bottom-right toasts

Favicon carried over from the Nuxt project.
This commit is contained in:
Esa Kataja
2026-07-31 22:16:29 +03:00
parent 91e136055c
commit f33f4fa4d6
16 changed files with 724 additions and 232 deletions
+17 -11
View File
@@ -1,9 +1,12 @@
{{define "content"}}
<h1>Ylläpito</h1>
<section>
<h2>Kutsut</h2>
<form method="post" action="/admin/invites"><button type="submit">Luo kutsukoodi</button></form>
<section class="adminsection">
<header>
<h2>Kutsut</h2>
<form method="post" action="/admin/invites"><button type="submit">Luo kutsukoodi</button></form>
</header>
<div class="body">
<table>
<thead><tr><th>Kutsulinkki</th><th>Tila</th><th>Luotu</th></tr></thead>
<tbody>
@@ -11,12 +14,12 @@
<tr>
<td>
{{if .IsValid}}
<a href="{{.Link}}" class="invite">{{.Link}}</a>
<a href="{{.Link}}">{{.Link}}</a>
{{else}}
<code class="muted">{{.Code}}</code>
{{end}}
</td>
<td>{{if .IsValid}}käyttämätön{{else}}käytetty{{end}}</td>
<td class="nowrap"><span class="dot {{if .IsValid}}on{{else}}off{{end}}"></span> {{if .IsValid}}käyttämätön{{else}}käytetty{{end}}</td>
<td>{{fidate .CreatedAt}}</td>
</tr>
{{else}}
@@ -24,26 +27,28 @@
{{end}}
</tbody>
</table>
<p class="muted">Lähetä linkki kaverille — se avaa liittymislomakkeen koodi valmiiksi täytettynä.</p>
<p class="muted small">Lähetä linkki kaverille — se avaa liittymislomakkeen koodi valmiiksi täytettynä.</p>
</div>
</section>
<section>
<h2>Jäsenet</h2>
<section class="adminsection">
<header><h2>Jäsenet</h2></header>
<div class="body">
<table>
<thead><tr><th>Nimi</th><th>Sähköposti</th><th>Liittyi</th><th>Toiminnot</th></tr></thead>
<tbody>
{{range .Data.Members}}
<tr{{if .Banned}} class="banned"{{end}}>
<td>{{.Name}}{{if .Banned}} <span class="tag">estetty</span>{{end}}</td>
<td>{{.Name}}{{if .Banned}} <span class="badge pending">estetty</span>{{end}}</td>
<td>{{.Email}}</td>
<td>{{fidate .CreatedAt}}</td>
<td class="actions">
<form method="post" action="/admin/users/{{.ID}}/ban">
<button type="submit">{{if .Banned}}Poista esto{{else}}Estä{{end}}</button>
<button type="submit" class="ghost">{{if .Banned}}Poista esto{{else}}Estä{{end}}</button>
</form>
<form method="post" action="/admin/users/{{.ID}}/password">
<input type="password" name="password" placeholder="uusi salasana" required>
<button type="submit">Vaihda salasana</button>
<button type="submit" class="ghost">Vaihda salasana</button>
</form>
</td>
</tr>
@@ -52,5 +57,6 @@
{{end}}
</tbody>
</table>
</div>
</section>
{{end}}
+47 -13
View File
@@ -4,29 +4,63 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} — Levyraati</title>
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="preload" href="/static/fonts/oswald.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/static/style.css">
<script src="/static/htmx.min.js" defer></script>
</head>
<body>
<header>
<a class="brand" href="/">Levyraati{{if .Admin}} <span class="tag">ylläpito</span>{{end}}</a>
<nav>
<header class="topbar">
<div class="topbar-inner">
<a class="brand" href="/">Levyraati{{if .Admin}} <span class="badge admin">ylläpito</span>{{end}}</a>
{{if .Admin}}
<a href="/admin">Ylläpito</a>
<nav class="navlinks"><a href="/admin" aria-current="page">Ylläpito</a></nav>
<span></span>
{{else if .Member}}
<a href="/">Jono</a>
<a href="/songs">Kappaleet</a>
<a href="/submit">Lähetä</a>
<span class="avatar" title="{{.Member.Name}}">{{.Member.Initials}}</span>
<form method="post" action="/logout"><button class="link">Kirjaudu ulos</button></form>
<nav class="navlinks">
<a href="/" {{if eq .Path "/"}}aria-current="page"{{end}}>Jono</a>
<a href="/songs" {{if eq .Path "/songs"}}aria-current="page"{{end}}>Kappaleet</a>
<a href="/submit" {{if eq .Path "/submit"}}aria-current="page"{{end}}>Lähetä</a>
</nav>
<div class="userblock">
<span class="lines">
<span class="name">{{.Member.Name}}</span>
<span class="email">{{.Member.Email}}</span>
</span>
<span class="avatar" title="{{.Member.Name}}">{{.Member.Initials}}</span>
<form method="post" action="/logout"><button class="link">Kirjaudu ulos</button></form>
<!-- <details> is the mobile panel: no JS, and Esc/click-away come free. -->
<details class="mobilenav">
<summary aria-label="Valikko"></summary>
<div class="panel">
<a href="/">Jono</a>
<a href="/songs">Kappaleet</a>
<a href="/submit">Lähetä</a>
<form method="post" action="/logout"><button type="submit">Kirjaudu ulos</button></form>
</div>
</details>
</div>
{{else}}
<a href="/login">Kirjaudu</a>
<span></span>
<nav class="navlinks"><a href="/login">Kirjaudu</a></nav>
{{end}}
</nav>
</div>
</header>
{{with .Flash}}<p class="flash">{{.}}</p>{{end}}
<main {{if .Narrow}}class="narrow"{{end}}>{{template "content" .}}</main>
<main>{{template "content" .}}</main>
<footer class="sitefooter">Levyraati — kymmenen kaverin levyraati.</footer>
{{with .Flash}}
<div class="toasts">
<div class="toast" role="status">
<p>{{.}}</p>
<button class="dismiss" aria-label="Sulje"
onclick="this.closest('.toast').remove()">&times;</button>
</div>
</div>
{{end}}
</body>
</html>
+18 -15
View File
@@ -2,21 +2,21 @@
<audio controls preload="none" src="/audio/{{.ID}}" class="player"></audio>
{{end}}
{{define "songrow"}}
<tr>
<td>
<a href="/songs/{{.ID}}">{{.Title}}</a>
<span class="muted">{{.Artist}}</span>
{{if .Own}}<span class="tag">oma</span>{{else if .Reviewed}}<span class="tag done">arvosteltu</span>{{end}}
</td>
<td class="nowrap">{{.GenreLabel}}</td>
<td class="nowrap">{{.Length}}</td>
<td class="nowrap">
{{if .Average}}<strong>{{score .Average}}</strong> <span class="muted">({{.ReviewCount}})</span>
{{else if .ReviewCount}}<span class="muted">{{.ReviewCount}} arvostelua</span>
{{else}}<span class="muted"></span>{{end}}
</td>
</tr>
{{define "songcard"}}
<a class="songcard{{if and (not .Own) (not .Reviewed)}} unreviewed{{end}}" href="/songs/{{.ID}}">
{{with .Average}}<span class="scorebadge">{{score .}}</span>{{end}}
<span class="title">{{.Title}}</span>
<span class="artist">{{.Artist}}</span>
<span class="meta">
<span class="badge genre">{{.GenreLabel}}</span>
<span>{{.Length}}</span>
<span>{{.Submitter}}</span>
{{if .Own}}<span class="badge admin">oma</span>
{{else if .Reviewed}}<span class="badge reviewed">arvosteltu</span>
{{else}}<span class="badge pending">arvostelematta</span>{{end}}
{{if .ReviewCount}}<span>{{.ReviewCount}} arvostelua</span>{{end}}
</span>
</a>
{{end}}
{{define "scorefield"}}
@@ -27,4 +27,7 @@
<output>{{.}}</output>
</span>
</label>
<span class="scorescale" aria-hidden="true">
<span>1</span><span>25</span><span>50</span><span>75</span><span>100</span>
</span>
{{end}}
+3 -6
View File
@@ -4,12 +4,9 @@
{{if .Data.Items}}
<p class="muted">Arvostelemattomat kappaleet, vanhimmasta uusimpaan. Pisteet paljastuvat kun
olet kirjoittanut oman arvostelusi.</p>
<table>
<thead><tr><th>Kappale</th><th>Genre</th><th>Kesto</th><th>Arvostelut</th></tr></thead>
<tbody>
{{range .Data.Items}}{{template "songrow" .}}{{end}}
</tbody>
</table>
<div class="songgrid">
{{range .Data.Items}}{{template "songcard" .}}{{end}}
</div>
{{with .Data.NextCursor}}<p><a href="/?cursor={{.}}">Lisää →</a></p>{{end}}
{{else}}
<p class="empty">Jono on tyhjä. Olet arvostellut kaiken, mitä muut ovat lähettäneet.</p>
+11 -9
View File
@@ -1,10 +1,12 @@
{{define "content"}}
{{$s := .Data}}
<h1>{{$s.Title}}</h1>
<p class="byline">
{{$s.Artist}} · {{$s.GenreLabel}} · {{$s.Length}} ·
lähettänyt {{$s.Submitter}} {{fidate $s.CreatedAt}}
{{if $s.Own}}<span class="tag">oma kappale</span>{{end}}
<p class="songmeta">
<strong>{{$s.Artist}}</strong>
<span class="badge genre">{{$s.GenreLabel}}</span>
<span>{{$s.Length}}</span>
<span>lähettänyt {{$s.Submitter}} {{fidate $s.CreatedAt}}</span>
{{if $s.Own}}<span class="badge admin">oma kappale</span>{{end}}
</p>
{{template "player" $s}}
@@ -31,7 +33,7 @@
</form>
<form method="post" action="/songs/{{$s.ID}}/delete"
onsubmit="return confirm('Poistetaanko kappale lopullisesti?')">
<button type="submit" class="danger">Poista kappale</button>
<button type="submit" class="danger ghost">Poista kappale</button>
</form>
<p class="muted small">Muokkaus ja poisto ovat mahdollisia vain ennen ensimmäistä arvostelua.</p>
</details>
@@ -85,12 +87,12 @@
{{if $s.Revealed}}
{{if $s.Average}}<p class="average">Keskiarvo <strong>{{score $s.Average}}</strong></p>{{end}}
{{range $s.Reviews}}
<article class="review">
<article class="review{{if .Own}} own{{end}}">
<header>
<span class="avatar">{{.Initials}}</span>
<strong>{{.Reviewer}}</strong>
<span class="avatar small">{{.Initials}}</span>
<span class="who">{{.Reviewer}}</span>
<span class="score">{{.Score}}</span>
<span class="muted small">{{fidate .CreatedAt}}</span>
<span class="meta">{{fidate .CreatedAt}}</span>
</header>
<p>{{.Text}}</p>
</article>
+3 -6
View File
@@ -2,12 +2,9 @@
<h1>Kappaleet</h1>
{{if .Data.Items}}
<table>
<thead><tr><th>Kappale</th><th>Genre</th><th>Kesto</th><th>Pisteet</th></tr></thead>
<tbody>
{{range .Data.Items}}{{template "songrow" .}}{{end}}
</tbody>
</table>
<div class="songgrid">
{{range .Data.Items}}{{template "songcard" .}}{{end}}
</div>
{{with .Data.NextCursor}}<p><a href="/songs?cursor={{.}}">Vanhempia →</a></p>{{end}}
{{else}}
<p class="empty">Yhtään kappaletta ei ole vielä julkaistu.</p>
+1 -1
View File
@@ -1,5 +1,5 @@
{{define "submission-status"}}
<div class="status" {{if not .Done}}hx-get="/submit/{{.ID}}/status" hx-trigger="every 2s" hx-swap="outerHTML"{{end}}>
<div class="status{{if .Failed}} failed{{end}}" {{if not .Done}}hx-get="/submit/{{.ID}}/status" hx-trigger="every 2s" hx-swap="outerHTML"{{end}}>
<p class="{{if .Failed}}error{{end}}"><strong>{{.Label}}</strong></p>
{{if .Failed}}
{{with .StatusMsg}}<p class="muted small">{{.}}</p>{{end}}