Step 6. The surfaces around the review loop. - Nine leaderboards, ordered and limited in SQL, each with a deterministic tie-break so a tied board doesn't reshuffle between reloads. Min 3 reviews to qualify, for reviewer boards too - Profiles show counts and history-wide averages and the member's songs, never a list of their reviews — per-song opinion stays gated - Avatars: 5MB in, 256px JPEG out, ffmpeg's re-encode being the validation. No upload still means initials, and avatars are public - Changing your own password requires the current one and drops your other sessions - Palaute: free text plus the page you were on, carried in a footer link, and the user agent from the header. Reporters see their own; the admin resolves them with a timestamp rather than a status enum - Admin gained the song list with delete, the reports page, and an open-report count on the dashboard Two theme fixes the screenshots caught: leaderboard ranks need a CSS counter because display:grid suppresses list markers, and count-based boards were printing 3.0 where they mean 3.
79 lines
3.0 KiB
HTML
79 lines
3.0 KiB
HTML
<!doctype html>
|
|
<html lang="fi">
|
|
<head>
|
|
<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 class="topbar">
|
|
<div class="topbar-inner">
|
|
<a class="brand" href="/">Levyraati{{if .Admin}} <span class="badge admin">ylläpito</span>{{end}}</a>
|
|
|
|
{{if .Admin}}
|
|
<nav class="navlinks"><a href="/admin" aria-current="page">Ylläpito</a></nav>
|
|
<span></span>
|
|
{{else if .Member}}
|
|
<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>
|
|
<a href="/stats" {{if eq .Path "/stats"}}aria-current="page"{{end}}>Tilastot</a>
|
|
</nav>
|
|
<div class="userblock">
|
|
<span class="lines">
|
|
<span class="name">{{.Member.Name}}</span>
|
|
<span class="email">{{.Member.Email}}</span>
|
|
</span>
|
|
<a href="/profile" title="{{.Member.Name}}">
|
|
{{if .Member.Avatar}}<img class="avatar" src="/avatars/{{.Member.ID}}" alt="Oma profiili">
|
|
{{else}}<span class="avatar">{{.Member.Initials}}</span>{{end}}
|
|
</a>
|
|
<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>
|
|
<a href="/stats">Tilastot</a>
|
|
<a href="/profile">Oma profiili</a>
|
|
<form method="post" action="/logout"><button type="submit">Kirjaudu ulos</button></form>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
{{else}}
|
|
<span></span>
|
|
<nav class="navlinks"><a href="/login">Kirjaudu</a></nav>
|
|
{{end}}
|
|
</div>
|
|
</header>
|
|
|
|
<main {{if .Narrow}}class="narrow"{{end}}>{{template "content" .}}</main>
|
|
|
|
<footer class="sitefooter">
|
|
{{if .Member}}
|
|
<!-- The server already knows where they were, so the path travels in the link — no JS. -->
|
|
<a href="/report?from={{.Path}}">Ilmoita ongelmasta</a> ·
|
|
{{end}}
|
|
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()">×</button>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</body>
|
|
</html>
|