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:
@@ -9,12 +9,5 @@ export default async function PreviewPage({
|
||||
}) {
|
||||
const { mbid } = await params;
|
||||
const { name } = await searchParams;
|
||||
return (
|
||||
<main>
|
||||
<p>
|
||||
<a href="/discover">← Discover</a> · <a href="/artists">Artists</a>
|
||||
</p>
|
||||
<PreviewClient mbid={mbid} initialName={name ?? ""} />
|
||||
</main>
|
||||
);
|
||||
return <PreviewClient mbid={mbid} initialName={name ?? ""} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { PageHead } from "../../../_ui/page-head";
|
||||
|
||||
type Release = {
|
||||
rgMbid: string;
|
||||
@@ -105,52 +106,78 @@ export function PreviewClient({ mbid, initialName }: { mbid: string; initialName
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<p>
|
||||
Couldn’t load this artist. <button onClick={load}>Retry</button>
|
||||
<p className="empty">
|
||||
Couldn’t load this artist.{" "}
|
||||
<button className="btn sm" onClick={load}>
|
||||
Retry
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
if (!data) return <p>Loading…</p>;
|
||||
if (!data) return <p className="empty">Loading…</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.artistName || "Artist"}</h1>
|
||||
<p>
|
||||
{followed ? <span>Following</span> : <button onClick={follow}>Follow</button>}
|
||||
{" · "}
|
||||
<PageHead title={data.artistName || "Artist"} eyebrow="Preview · discography" />
|
||||
|
||||
<div className="tool-row">
|
||||
{followed ? (
|
||||
<span className="following">Following</span>
|
||||
) : (
|
||||
<button className="btn accent" onClick={follow}>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<a href={`https://musicbrainz.org/artist/${mbid}`} target="_blank" rel="noreferrer">
|
||||
MusicBrainz ↗
|
||||
</a>
|
||||
</p>
|
||||
<label>
|
||||
<input type="checkbox" checked={showAll} onChange={(e) => setShowAll(e.target.checked)} /> Show all release
|
||||
types
|
||||
</label>
|
||||
<ul>
|
||||
{data.releases.map((r) => (
|
||||
<li key={r.rgMbid} id={`rg-${r.rgMbid}`}>
|
||||
<strong>{r.album}</strong>{" "}
|
||||
<small>
|
||||
{r.primaryType}
|
||||
{r.firstReleaseDate ? ` · ${r.firstReleaseDate.slice(0, 4)}` : ""}
|
||||
</small>{" "}
|
||||
{r.have ? <em>In library</em> : r.monitored ? <em>Monitored</em> : null}{" "}
|
||||
<button onClick={() => want(r)} disabled={r.have || r.monitored}>
|
||||
Want
|
||||
</button>{" "}
|
||||
<button onClick={() => toggleTracks(r.rgMbid)}>{tracks[r.rgMbid] ? "Hide tracks" : "Tracks"}</button>
|
||||
{tracks[r.rgMbid] === "loading" && <p>Loading tracks…</p>}
|
||||
{tracks[r.rgMbid] === "error" && <p>Couldn’t load tracks.</p>}
|
||||
{Array.isArray(tracks[r.rgMbid]) && (
|
||||
<ol>
|
||||
{(tracks[r.rgMbid] as Track[]).map((t) => (
|
||||
<li key={t.position}>
|
||||
{t.title} {t.lengthMs != null && <small>({fmt(t.lengthMs)})</small>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<label className="toggle">
|
||||
<input type="checkbox" checked={showAll} onChange={(e) => setShowAll(e.target.checked)} />
|
||||
Show all types
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul className="list">
|
||||
{data.releases.map((r) => {
|
||||
const t = tracks[r.rgMbid];
|
||||
return (
|
||||
<li key={r.rgMbid} id={`rg-${r.rgMbid}`} className="list-row">
|
||||
<div className="main">
|
||||
<div className="rtitle">{r.album}</div>
|
||||
<div className="rmeta">
|
||||
{r.primaryType ? <span>{r.primaryType}</span> : null}
|
||||
{r.firstReleaseDate ? (
|
||||
<>
|
||||
<span className="dot">·</span>
|
||||
<span>{r.firstReleaseDate.slice(0, 4)}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{r.have ? <span className="chip done">In library</span> : r.monitored ? <span className="chip working">Monitored</span> : null}
|
||||
<button className="btn sm" onClick={() => want(r)} disabled={r.have || r.monitored}>
|
||||
Want
|
||||
</button>
|
||||
<button className="btn sm ghost" onClick={() => toggleTracks(r.rgMbid)}>
|
||||
{t ? "Hide tracks" : "Tracks"}
|
||||
</button>
|
||||
</div>
|
||||
{t === "loading" ? <p className="muted-note" style={{ flexBasis: "100%" }}>Loading tracks…</p> : null}
|
||||
{t === "error" ? <p className="notice" style={{ flexBasis: "100%" }}>Couldn’t load tracks.</p> : null}
|
||||
{Array.isArray(t) ? (
|
||||
<ol className="tracks" style={{ flexBasis: "100%" }}>
|
||||
{t.map((tr) => (
|
||||
<li key={tr.position}>
|
||||
<span className="tnum">{tr.position}</span>
|
||||
<span className="tname">{tr.title}</span>
|
||||
{tr.lengthMs != null ? <span>{fmt(tr.lengthMs)}</span> : null}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { DiscoverClient } from "./discover-client";
|
||||
|
||||
export default function DiscoverPage() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Discover</h1>
|
||||
<p>
|
||||
<a href="/">Home</a> · <a href="/artists">Artists</a> · <a href="/wanted">Wanted</a> ·{" "}
|
||||
<a href="/settings">Settings</a>
|
||||
</p>
|
||||
<DiscoverClient />
|
||||
</main>
|
||||
);
|
||||
return <DiscoverClient />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user