feat(web): restyle Artists, Discover, Wanted, discography + preview (Phase 2)

Convert the remaining pages onto the Pressing Plant system: PageHead
component, shared list-row / toggle / small+ghost button / tracklist classes,
and page wrappers trimmed now that the shell provides masthead + nav. All
logic and test hooks (aria-labels, button text) preserved. Verified in the
real app across both themes (Playwright): artists/discover/wanted/discography
render cohesively with real data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 14:27:43 +02:00
parent 7f1fe52a7c
commit ead5f33bcc
12 changed files with 404 additions and 204 deletions
+94 -58
View File
@@ -1,6 +1,8 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { PageHead } from "../_ui/page-head";
import { SectionHeader } from "../_ui/section-header";
type Suggestion = {
id: string;
@@ -93,80 +95,114 @@ export function DiscoverClient() {
return (
<div>
<section>
<button onClick={discoverNow} disabled={running}>
<PageHead title="Discover" eyebrow="A&R · similar artists & new releases" />
<div className="tool-row">
<button className="btn accent" onClick={discoverNow} disabled={running}>
{running ? "Discovering…" : "Discover now"}
</button>
{result && <span> Last run: {result}</span>}
</section>
{result ? <span className="rmeta">last run · {result}</span> : null}
</div>
<section>
<h2>Seed search</h2>
<form onSubmit={runSearch}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Artist name…"
aria-label="Seed artist"
/>
<button type="submit" disabled={busy}>Find similar</button>
</form>
{search && !search.seed && <p>No MusicBrainz match.</p>}
{search?.seed && (
<ul>
{search.similar.map((s) => (
<li key={s.mbid}>
<a href={`/discover/artist/${s.mbid}?name=${encodeURIComponent(s.name)}`}>{s.name}</a>{" "}
<small>({s.score.toFixed(1)})</small>{" "}
<SectionHeader title="Find similar" />
<form className="request-form" onSubmit={runSearch}>
<label className="field">
<span>Seed artist</span>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Artist name…" aria-label="Seed artist" />
</label>
<button type="submit" className="btn" disabled={busy}>
{busy ? "Searching…" : "Find similar"}
</button>
</form>
{search && !search.seed ? <p className="muted-note">No MusicBrainz match.</p> : null}
{search?.seed ? (
<ul className="list">
{search.similar.map((s) => (
<li key={s.mbid} className="list-row">
<div className="main">
<div className="rtitle">
<a href={`/discover/artist/${s.mbid}?name=${encodeURIComponent(s.name)}`}>{s.name}</a>
</div>
<div className="rmeta">
<span className="score">similarity {s.score.toFixed(1)}</span>
</div>
</div>
<div className="actions">
{followedSimilar.has(s.mbid) ? (
<span>Followed</span>
<span className="following">Following</span>
) : (
<button onClick={() => followSimilar(s)}>Follow</button>
<button className="btn sm" onClick={() => followSimilar(s)}>
Follow
</button>
)}
</li>
))}
</ul>
)}
</section>
</div>
</li>
))}
</ul>
) : null}
<section>
<h2>Suggested artists ({feed.artists.length})</h2>
<ul>
<SectionHeader title="Suggested artists" note={`${feed.artists.length}`} />
{feed.artists.length === 0 ? (
<p className="muted-note">No suggestions yet run Discover, or follow a few artists to seed it.</p>
) : (
<ul className="list">
{feed.artists.map((s) => (
<li key={s.id}>
<strong>
<a href={`/discover/artist/${s.artistMbid}?name=${encodeURIComponent(s.artistName)}`}>
{s.artistName}
</a>
</strong>{" "}
<small>score {s.score.toFixed(2)} · {s.seedCount} seed(s) · {s.sources.join(", ")}</small>{" "}
<button onClick={() => act(s.id, "follow")}>Follow</button>{" "}
<button onClick={() => act(s.id, "dismiss")}>Dismiss</button>
<li key={s.id} className="list-row">
<div className="main">
<div className="rtitle">
<a href={`/discover/artist/${s.artistMbid}?name=${encodeURIComponent(s.artistName)}`}>{s.artistName}</a>
</div>
<div className="rmeta">
<span className="score">score {s.score.toFixed(2)}</span>
<span className="dot">·</span>
<span>{s.seedCount} seed{s.seedCount === 1 ? "" : "s"}</span>
<span className="dot">·</span>
<span>{s.sources.join(", ")}</span>
</div>
</div>
<div className="actions">
<button className="btn sm" onClick={() => act(s.id, "follow")}>
Follow
</button>
<button className="btn sm ghost" onClick={() => act(s.id, "dismiss")}>
Dismiss
</button>
</div>
</li>
))}
</ul>
</section>
)}
<section>
<h2>Suggested albums ({feed.albums.length})</h2>
<ul>
<SectionHeader title="Suggested albums" note={`${feed.albums.length}`} />
{feed.albums.length === 0 ? (
<p className="muted-note">No album suggestions yet.</p>
) : (
<ul className="list">
{feed.albums.map((s) => (
<li key={s.id}>
<strong>
<a
href={`/discover/artist/${s.artistMbid}?name=${encodeURIComponent(s.artistName)}#rg-${s.rgMbid}`}
>
{s.album}
</a>
</strong>{" "}
{s.artistName}{" "}
<small>score {s.score.toFixed(2)}</small>{" "}
<button onClick={() => act(s.id, "want")}>Want</button>{" "}
<button onClick={() => act(s.id, "dismiss")}>Dismiss</button>
<li key={s.id} className="list-row">
<div className="main">
<div className="rtitle">
<a href={`/discover/artist/${s.artistMbid}?name=${encodeURIComponent(s.artistName)}#rg-${s.rgMbid}`}>
{s.album}
</a>{" "}
<span className="artist">· {s.artistName}</span>
</div>
<div className="rmeta">
<span className="score">score {s.score.toFixed(2)}</span>
</div>
</div>
<div className="actions">
<button className="btn sm" onClick={() => act(s.id, "want")}>
Want
</button>
<button className="btn sm ghost" onClick={() => act(s.id, "dismiss")}>
Dismiss
</button>
</div>
</li>
))}
</ul>
</section>
)}
</div>
);
}