feat(discover): Artists|Albums tabs on /discover

Suggested artists and Suggested albums were stacked sections; make them
tabs like /lastfm (.tabs bar + count badge). Discover-now/freshness and
Find-similar stay above the tabs.

The Albums tab filters to full-length albums (isFullAlbum) — a client-side
stopgap that hides any pre-D1 singles/EP suggestions still in the DB (new
sweeps already filter server-side via discover.albumsOnly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 20:42:33 +02:00
parent c9905937cc
commit 72e9c2d47d
+30 -7
View File
@@ -15,11 +15,22 @@ type Suggestion = {
artistName: string;
rgMbid: string | null;
album: string | null;
primaryType: string | null;
secondaryTypes: string[];
score: number;
seedCount: number;
sources: string[];
};
type Tab = "Artists" | "Albums";
/** Only full-length albums belong in Suggested albums — hide singles/EPs and
* secondary-type releases (live/comp). New sweeps already filter server-side
* (discover.albumsOnly); this also sweeps any pre-filter rows still in the DB. */
function isFullAlbum(s: Suggestion): boolean {
return (s.primaryType ?? "Album") === "Album" && (s.secondaryTypes?.length ?? 0) === 0;
}
/** "just now" / "3h ago" / "2d ago" — timeAgo has no suffix, so add one (except "just now"). */
function agoLabel(value: string): string {
const a = timeAgo(value, Date.now());
@@ -43,6 +54,9 @@ export function DiscoverClient() {
const [followedSimilar, setFollowedSimilar] = useState<Set<string>>(new Set());
const [artistModal, setArtistModal] = useState<{ mbid: string; name: string } | null>(null);
const [albumModal, setAlbumModal] = useState<Suggestion | null>(null);
const [tab, setTab] = useState<Tab>("Artists");
const albums = feed.albums.filter(isFullAlbum);
const loadFeed = useCallback(async () => {
setFeed(await (await fetch("/api/discover")).json());
@@ -172,8 +186,19 @@ export function DiscoverClient() {
</ul>
) : null}
<SectionHeader title="Suggested artists" note={`${feed.artists.length}`} />
{feed.artists.length === 0 ? (
<div className="tabs">
<button className={`tab${tab === "Artists" ? " active" : ""}`} onClick={() => setTab("Artists")}>
Suggested artists
<span className="n">{feed.artists.length}</span>
</button>
<button className={`tab${tab === "Albums" ? " active" : ""}`} onClick={() => setTab("Albums")}>
Suggested albums
<span className="n">{albums.length}</span>
</button>
</div>
{tab === "Artists" ? (
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">
@@ -204,14 +229,12 @@ export function DiscoverClient() {
</li>
))}
</ul>
)}
<SectionHeader title="Suggested albums" note={`${feed.albums.length}`} />
{feed.albums.length === 0 ? (
)
) : albums.length === 0 ? (
<p className="muted-note">No album suggestions yet.</p>
) : (
<ul className="list">
{feed.albums.map((s) => (
{albums.map((s) => (
<li key={s.id} className="list-row">
<div className="main">
<button className="disco-open" onClick={() => setAlbumModal(s)} aria-label={`Open ${s.album ?? ""}`}>