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:
@@ -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,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")}>
|
||||||
|
|||||||
@@ -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}`}>
|
||||||
{w.album} <span className="artist">· {w.artistName}</span>
|
<CoverArt rgMbid={w.rgMbid} alt={w.album} className="thumb" />
|
||||||
</div>
|
<span>
|
||||||
<div className="rmeta">
|
<span className="rtitle">
|
||||||
<span>{w.lastSearchedAt ? `last tried ${new Date(w.lastSearchedAt).toLocaleDateString()}` : "never tried"}</span>
|
{w.album} <span className="artist">· {w.artistName}</span>
|
||||||
</div>
|
</span>
|
||||||
|
<span className="rmeta">
|
||||||
|
{w.lastSearchedAt ? `last tried ${new Date(w.lastSearchedAt).toLocaleDateString()}` : "never tried"}
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user