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:
@@ -15,11 +15,22 @@ type Suggestion = {
|
|||||||
artistName: string;
|
artistName: string;
|
||||||
rgMbid: string | null;
|
rgMbid: string | null;
|
||||||
album: string | null;
|
album: string | null;
|
||||||
|
primaryType: string | null;
|
||||||
|
secondaryTypes: string[];
|
||||||
score: number;
|
score: number;
|
||||||
seedCount: number;
|
seedCount: number;
|
||||||
sources: string[];
|
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"). */
|
/** "just now" / "3h ago" / "2d ago" — timeAgo has no suffix, so add one (except "just now"). */
|
||||||
function agoLabel(value: string): string {
|
function agoLabel(value: string): string {
|
||||||
const a = timeAgo(value, Date.now());
|
const a = timeAgo(value, Date.now());
|
||||||
@@ -43,6 +54,9 @@ export function DiscoverClient() {
|
|||||||
const [followedSimilar, setFollowedSimilar] = useState<Set<string>>(new Set());
|
const [followedSimilar, setFollowedSimilar] = useState<Set<string>>(new Set());
|
||||||
const [artistModal, setArtistModal] = useState<{ mbid: string; name: string } | null>(null);
|
const [artistModal, setArtistModal] = useState<{ mbid: string; name: string } | null>(null);
|
||||||
const [albumModal, setAlbumModal] = useState<Suggestion | 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 () => {
|
const loadFeed = useCallback(async () => {
|
||||||
setFeed(await (await fetch("/api/discover")).json());
|
setFeed(await (await fetch("/api/discover")).json());
|
||||||
@@ -172,8 +186,19 @@ export function DiscoverClient() {
|
|||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<SectionHeader title="Suggested artists" note={`${feed.artists.length}`} />
|
<div className="tabs">
|
||||||
{feed.artists.length === 0 ? (
|
<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>
|
<p className="muted-note">No suggestions yet — run Discover, or follow a few artists to seed it.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="list">
|
<ul className="list">
|
||||||
@@ -204,14 +229,12 @@ export function DiscoverClient() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)
|
||||||
|
) : albums.length === 0 ? (
|
||||||
<SectionHeader title="Suggested albums" note={`${feed.albums.length}`} />
|
|
||||||
{feed.albums.length === 0 ? (
|
|
||||||
<p className="muted-note">No album suggestions yet.</p>
|
<p className="muted-note">No album suggestions yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="list">
|
<ul className="list">
|
||||||
{feed.albums.map((s) => (
|
{albums.map((s) => (
|
||||||
<li key={s.id} className="list-row">
|
<li key={s.id} className="list-row">
|
||||||
<div className="main">
|
<div className="main">
|
||||||
<button className="disco-open" onClick={() => setAlbumModal(s)} aria-label={`Open ${s.album ?? ""}`}>
|
<button className="disco-open" onClick={() => setAlbumModal(s)} aria-label={`Open ${s.album ?? ""}`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user