feat(web): Follow button + clickable artist in album modals
AlbumModal showed the artist name as static text — no way to follow the artist or jump to them. Adds a Follow affordance (todo #13): - AlbumInfo gains optional `artistMbid`. When present, the modal title links the artist name to /discover/artist/{mbid} and shows a Follow/Following button that reflects live follow state. - Follow state fetched via a new lightweight GET /api/artists?mbid= check (avoids pulling the full watched list + discography); Follow reuses POST /api/artists. Non-ok POST surfaces a toast (also chips at todo #14). - Wired artistMbid in the three discovery contexts where following is valuable: Discover, Discover artist preview, and Last.fm. Library/discography callers pass none (already-followed artists) so the button stays hidden — graceful. web 152 tests (the ?mbid= follow-state check asserted), tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Modal } from "./modal";
|
||||
import { CoverArt } from "./cover-art";
|
||||
import { toast } from "./toast";
|
||||
|
||||
type Track = { position: number; title: string; lengthMs: number | null };
|
||||
|
||||
@@ -22,6 +23,8 @@ export type AlbumInfo = {
|
||||
artist: string;
|
||||
year?: string | null;
|
||||
type?: string | null;
|
||||
/** When present, the modal shows a Follow button + links the artist name to their page. */
|
||||
artistMbid?: string | null;
|
||||
};
|
||||
|
||||
/** Reusable album modal: cover art + tracklist (fetched from MB by release-group MBID) with
|
||||
@@ -40,6 +43,60 @@ export function AlbumModal({
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const [tracks, setTracks] = useState<Track[] | "loading" | "error">("loading");
|
||||
// null = unknown/loading; only meaningful when album.artistMbid is set.
|
||||
const [followed, setFollowed] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !album.artistMbid) {
|
||||
setFollowed(false);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setFollowed(null);
|
||||
fetch(`/api/artists?mbid=${encodeURIComponent(album.artistMbid)}`)
|
||||
.then((r) => (r.ok ? r.json() : { followed: false }))
|
||||
.then((d) => !cancelled && setFollowed(!!d.followed))
|
||||
.catch(() => !cancelled && setFollowed(false));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, album.artistMbid]);
|
||||
|
||||
async function follow() {
|
||||
if (!album.artistMbid) return;
|
||||
try {
|
||||
const res = await fetch("/api/artists", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ mbid: album.artistMbid, name: album.artist }),
|
||||
});
|
||||
if (res.ok || res.status === 409) {
|
||||
setFollowed(true);
|
||||
toast(`Following ${album.artist}`);
|
||||
} else {
|
||||
toast(`Couldn't follow ${album.artist}`);
|
||||
}
|
||||
} catch {
|
||||
toast(`Couldn't follow ${album.artist}`);
|
||||
}
|
||||
}
|
||||
|
||||
const titleNode: ReactNode = album.artistMbid ? (
|
||||
<span className="modal-artist">
|
||||
<a href={`/discover/artist/${album.artistMbid}`}>{album.artist}</a>
|
||||
<button
|
||||
type="button"
|
||||
className="btn sm ghost"
|
||||
onClick={follow}
|
||||
disabled={followed !== false}
|
||||
aria-label={followed ? "following" : "follow artist"}
|
||||
>
|
||||
{followed === null ? "…" : followed ? "Following" : "Follow"}
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
album.artist
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -69,7 +126,7 @@ export function AlbumModal({
|
||||
}, [open, album.rgMbid]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title={album.artist} wide>
|
||||
<Modal open={open} onClose={onClose} title={titleNode} wide>
|
||||
<div className="album-modal-head">
|
||||
<CoverArt rgMbid={album.rgMbid} alt={album.album} size={250} className="album-modal-cover" />
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user