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:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { PageHead } from "../../_ui/page-head";
|
||||
|
||||
type Release = {
|
||||
id: string;
|
||||
@@ -14,10 +15,10 @@ type Release = {
|
||||
};
|
||||
type Detail = { id: string; name: string; autoMonitorFuture: boolean; showAllTypes: boolean; releases: Release[] };
|
||||
|
||||
function badge(r: Release): string {
|
||||
if (r.currentQualityClass !== null) return `Have (q${r.currentQualityClass})`;
|
||||
if (r.monitored) return r.state === "wanted" ? "Wanted" : r.state;
|
||||
return "Dormant";
|
||||
function badge(r: Release): { label: string; cls: string } {
|
||||
if (r.currentQualityClass !== null) return { label: `Have q${r.currentQualityClass}`, cls: "chip done" };
|
||||
if (r.monitored) return { label: r.state === "wanted" ? "Wanted" : r.state, cls: "chip working" };
|
||||
return { label: "Dormant", cls: "chip" };
|
||||
}
|
||||
|
||||
export function DiscographyClient({ id }: { id: string }) {
|
||||
@@ -53,32 +54,56 @@ export function DiscographyClient({ id }: { id: string }) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
if (!detail) return <p>Loading…</p>;
|
||||
if (!detail) return <p className="empty">Loading…</p>;
|
||||
return (
|
||||
<div>
|
||||
<h1>{detail.name}</h1>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="show all release types"
|
||||
checked={detail.showAllTypes}
|
||||
onChange={toggleShowAll}
|
||||
/>{" "}
|
||||
Show all release types (live, compilations, singles…)
|
||||
</label>
|
||||
<ul>
|
||||
{detail.releases.map((r) => (
|
||||
<li key={r.id}>
|
||||
<strong>{r.album}</strong>
|
||||
{r.firstReleaseDate ? ` (${r.firstReleaseDate.slice(0, 4)})` : ""}
|
||||
{r.primaryType ? ` · ${r.primaryType}` : ""}
|
||||
{r.secondaryTypes.length ? ` [${r.secondaryTypes.join(", ")}]` : ""} · <em>{badge(r)}</em>{" "}
|
||||
<label>
|
||||
<input type="checkbox" aria-label={`monitor ${r.album}`} checked={r.monitored} onChange={() => toggleMonitor(r)} /> monitor
|
||||
</label>{" "}
|
||||
<button onClick={() => searchNow(r)}>Search now</button>
|
||||
</li>
|
||||
))}
|
||||
<PageHead title={detail.name} eyebrow="Discography" />
|
||||
|
||||
<div className="tool-row">
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="show all release types"
|
||||
checked={detail.showAllTypes}
|
||||
onChange={toggleShowAll}
|
||||
/>
|
||||
Show all types (live, compilations, singles…)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul className="list">
|
||||
{detail.releases.map((r) => {
|
||||
const b = badge(r);
|
||||
return (
|
||||
<li key={r.id} className="list-row">
|
||||
<div className="main">
|
||||
<div className="rtitle">
|
||||
{r.album}
|
||||
{r.firstReleaseDate ? <span className="dim"> ({r.firstReleaseDate.slice(0, 4)})</span> : null}
|
||||
</div>
|
||||
<div className="rmeta">
|
||||
{r.primaryType ? <span>{r.primaryType}</span> : null}
|
||||
{r.secondaryTypes.length ? (
|
||||
<>
|
||||
<span className="dot">·</span>
|
||||
<span>{r.secondaryTypes.join(", ")}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<span className={b.cls}>{b.label}</span>
|
||||
<label className="toggle">
|
||||
<input type="checkbox" aria-label={`monitor ${r.album}`} checked={r.monitored} onChange={() => toggleMonitor(r)} />
|
||||
monitor
|
||||
</label>
|
||||
<button className="btn sm ghost" onClick={() => searchNow(r)}>
|
||||
Search now
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,5 @@ import { DiscographyClient } from "./discography-client";
|
||||
|
||||
export default async function ArtistDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return (
|
||||
<main>
|
||||
<p><a href="/artists">← Artists</a></p>
|
||||
<DiscographyClient id={id} />
|
||||
</main>
|
||||
);
|
||||
return <DiscographyClient id={id} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { PageHead } from "../_ui/page-head";
|
||||
import { SectionHeader } from "../_ui/section-header";
|
||||
|
||||
type Artist = {
|
||||
id: string;
|
||||
@@ -68,38 +70,86 @@ export function ArtistsClient() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={search}>
|
||||
<input aria-label="artist search" placeholder="Search an artist…" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
<button type="submit">Search</button>
|
||||
<PageHead title="Artists" eyebrow="The roster · follow & watch" />
|
||||
|
||||
<form className="request-form" onSubmit={search}>
|
||||
<label className="field">
|
||||
<span>Find an artist</span>
|
||||
<input
|
||||
aria-label="artist search"
|
||||
placeholder="Search MusicBrainz…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="btn accent">
|
||||
Search
|
||||
</button>
|
||||
{searching ? <span className="rmeta">searching…</span> : null}
|
||||
</form>
|
||||
{searching ? <p>Searching…</p> : null}
|
||||
|
||||
{hits.length > 0 ? (
|
||||
<ul>
|
||||
{hits.map((h) => (
|
||||
<li key={h.mbid}>
|
||||
<a href={`/discover/artist/${h.mbid}?name=${encodeURIComponent(h.name)}`}>{h.name}</a>
|
||||
{h.disambiguation ? ` (${h.disambiguation})` : ""}{" "}
|
||||
{followedMbids.has(h.mbid) ? (
|
||||
<span> · following</span>
|
||||
) : (
|
||||
<button onClick={() => follow(h)}>Follow</button>
|
||||
)}
|
||||
<>
|
||||
<SectionHeader title="Search results" note={`${hits.length} found`} />
|
||||
<ul className="list">
|
||||
{hits.map((h) => (
|
||||
<li key={h.mbid} className="list-row">
|
||||
<div className="main">
|
||||
<div className="rtitle">
|
||||
<a href={`/discover/artist/${h.mbid}?name=${encodeURIComponent(h.name)}`}>{h.name}</a>
|
||||
{h.disambiguation ? <span className="dim"> — {h.disambiguation}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{followedMbids.has(h.mbid) ? (
|
||||
<span className="following">Following</span>
|
||||
) : (
|
||||
<button className="btn sm" onClick={() => follow(h)}>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<SectionHeader title="Watching" note={`${artists.length} artists`} />
|
||||
{artists.length === 0 ? (
|
||||
<p className="empty">Not following anyone yet. Search above and follow an artist to start watching their releases.</p>
|
||||
) : (
|
||||
<ul className="list">
|
||||
{artists.map((a) => (
|
||||
<li key={a.id} className="list-row">
|
||||
<div className="main">
|
||||
<div className="rtitle">
|
||||
<a href={`/artists/${a.id}`}>{a.name}</a>
|
||||
</div>
|
||||
<div className="rmeta">
|
||||
<span className="score">
|
||||
{a.haveCount} have <span className="dot">·</span> {a.monitoredCount} monitored <span className="dot">·</span> {a.releaseCount} releases
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`auto-monitor ${a.name}`}
|
||||
checked={a.autoMonitorFuture}
|
||||
onChange={() => toggleAuto(a)}
|
||||
/>
|
||||
auto-monitor
|
||||
</label>
|
||||
<button className="btn sm ghost" onClick={() => unfollow(a)}>
|
||||
Unfollow
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
<ul>
|
||||
{artists.map((a) => (
|
||||
<li key={a.id}>
|
||||
<a href={`/artists/${a.id}`}>{a.name}</a> · {a.haveCount}/{a.monitoredCount} have/monitored of {a.releaseCount}{" "}
|
||||
<label>
|
||||
<input type="checkbox" aria-label={`auto-monitor ${a.name}`} checked={a.autoMonitorFuture} onChange={() => toggleAuto(a)} /> auto-monitor future
|
||||
</label>{" "}
|
||||
<button onClick={() => unfollow(a)}>Unfollow</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { ArtistsClient } from "./artists-client";
|
||||
|
||||
export default function ArtistsPage() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Watched artists</h1>
|
||||
<p><a href="/">Home</a> · <a href="/wanted">Wanted</a></p>
|
||||
<ArtistsClient />
|
||||
</main>
|
||||
);
|
||||
return <ArtistsClient />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user