feat(web): cover art on Wanted + Discover album rows

Wanted API returns rgMbid/type/year; Wanted rows show a cover thumb and open
the album modal. Discover suggested-album rows get a cover thumb too. Completes
cover art across the app (artist detail, Library, modals, Wanted, Discover).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 15:37:02 +02:00
parent 76eab9e703
commit c027945191
3 changed files with 58 additions and 15 deletions
+3
View File
@@ -15,6 +15,9 @@ export async function GET() {
currentQualityClass: r.currentQualityClass, currentQualityClass: r.currentQualityClass,
lastSearchedAt: r.lastSearchedAt, lastSearchedAt: r.lastSearchedAt,
watchedArtistId: r.watchedArtistId, watchedArtistId: r.watchedArtistId,
rgMbid: r.rgMbid,
primaryType: r.primaryType,
firstReleaseDate: r.firstReleaseDate,
})), })),
}); });
} }
+11 -8
View File
@@ -5,6 +5,7 @@ import { PageHead } from "../_ui/page-head";
import { SectionHeader } from "../_ui/section-header"; import { SectionHeader } from "../_ui/section-header";
import { ArtistModal } from "../_ui/artist-modal"; import { ArtistModal } from "../_ui/artist-modal";
import { AlbumModal } from "../_ui/album-modal"; import { AlbumModal } from "../_ui/album-modal";
import { CoverArt } from "../_ui/cover-art";
type Suggestion = { type Suggestion = {
id: string; id: string;
@@ -189,15 +190,17 @@ export function DiscoverClient() {
{feed.albums.map((s) => ( {feed.albums.map((s) => (
<li key={s.id} className="list-row"> <li key={s.id} className="list-row">
<div className="main"> <div className="main">
<div className="rtitle"> <button className="disco-open" onClick={() => setAlbumModal(s)} aria-label={`Open ${s.album ?? ""}`}>
<button className="linkish" onClick={() => setAlbumModal(s)}> <CoverArt rgMbid={s.rgMbid} alt={s.album ?? ""} className="thumb" />
{s.album} <span>
</button>{" "} <span className="rtitle">
<span className="artist">· {s.artistName}</span> {s.album} <span className="artist">· {s.artistName}</span>
</div> </span>
<div className="rmeta"> <span className="rmeta">
<span className="score">score {s.score.toFixed(2)}</span> <span className="score">score {s.score.toFixed(2)}</span>
</div> </span>
</span>
</button>
</div> </div>
<div className="actions"> <div className="actions">
<button className="btn sm" onClick={() => act(s.id, "want")}> <button className="btn sm" onClick={() => act(s.id, "want")}>
+42 -5
View File
@@ -3,6 +3,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { PageHead } from "../_ui/page-head"; import { PageHead } from "../_ui/page-head";
import { SectionHeader } from "../_ui/section-header"; import { SectionHeader } from "../_ui/section-header";
import { CoverArt } from "../_ui/cover-art";
import { AlbumModal } from "../_ui/album-modal";
type Wanted = { type Wanted = {
id: string; id: string;
@@ -11,6 +13,9 @@ type Wanted = {
state: string; state: string;
currentQualityClass: number | null; currentQualityClass: number | null;
lastSearchedAt: string | null; lastSearchedAt: string | null;
rgMbid: string | null;
primaryType: string | null;
firstReleaseDate: string | null;
}; };
function stateLabel(w: Wanted): string { function stateLabel(w: Wanted): string {
@@ -23,6 +28,7 @@ export function WantedClient() {
const [artist, setArtist] = useState(""); const [artist, setArtist] = useState("");
const [album, setAlbum] = useState(""); const [album, setAlbum] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [open, setOpen] = useState<Wanted | null>(null);
async function refresh() { async function refresh() {
const res = await fetch("/api/wanted"); const res = await fetch("/api/wanted");
@@ -82,12 +88,17 @@ export function WantedClient() {
{rows.map((w) => ( {rows.map((w) => (
<li key={w.id} className="list-row"> <li key={w.id} className="list-row">
<div className="main"> <div className="main">
<div className="rtitle"> <button className="disco-open" onClick={() => setOpen(w)} aria-label={`Open ${w.album}`}>
<CoverArt rgMbid={w.rgMbid} alt={w.album} className="thumb" />
<span>
<span className="rtitle">
{w.album} <span className="artist">· {w.artistName}</span> {w.album} <span className="artist">· {w.artistName}</span>
</div> </span>
<div className="rmeta"> <span className="rmeta">
<span>{w.lastSearchedAt ? `last tried ${new Date(w.lastSearchedAt).toLocaleDateString()}` : "never tried"}</span> {w.lastSearchedAt ? `last tried ${new Date(w.lastSearchedAt).toLocaleDateString()}` : "never tried"}
</div> </span>
</span>
</button>
</div> </div>
<div className="actions"> <div className="actions">
<span className="chip">{stateLabel(w)}</span> <span className="chip">{stateLabel(w)}</span>
@@ -99,6 +110,32 @@ export function WantedClient() {
))} ))}
</ul> </ul>
)} )}
{open ? (
<AlbumModal
open
onClose={() => setOpen(null)}
album={{
rgMbid: open.rgMbid,
album: open.album,
artist: open.artistName,
year: open.firstReleaseDate?.slice(0, 4) ?? null,
type: open.primaryType,
}}
status={<span className="chip working">{stateLabel(open)}</span>}
actions={
<button
className="btn sm ghost"
onClick={() => {
searchNow(open);
setOpen(null);
}}
>
Search now
</button>
}
/>
) : null}
</div> </div>
); );
} }