"use client"; import { useEffect, useState } from "react"; import { Modal } from "./modal"; import { CoverArt } from "./cover-art"; import { toast } from "./toast"; import type { ArtistPlays } from "@/lib/lastfm"; type Rel = { rgMbid: string; album: string; primaryType: string | null; secondaryTypes: string[]; firstReleaseDate: string | null; monitored: boolean; have: boolean; }; type Preview = { artistName: string; followed: boolean; releases: Rel[] }; type ReleaseGroupMatch = { rgMbid: string; title: string; primaryType: string | null; secondaryTypes: string[]; firstReleaseDate: string | null; artistMbid: string; artistName: string; }; /** Lightweight artist preview in a modal: follow + a cover-art grid of their releases with * per-album Want. Reuses the discovery preview endpoint. The full preview page stays for * deep links (linked here as "Full page"). When `lastfmName` is set (the /lastfm page only) * it also shows the caller's most-played songs/albums for this artist, fetched independently * of the preview above so it never blocks the existing grid. */ export function ArtistModal({ open, onClose, mbid, initialName, lastfmName, }: { open: boolean; onClose: () => void; mbid: string; initialName: string; lastfmName?: string; }) { const [data, setData] = useState("loading"); const [followed, setFollowed] = useState(false); const [wanted, setWanted] = useState>(new Set()); const [plays, setPlays] = useState(null); const [wantedAlbums, setWantedAlbums] = useState>(new Set()); useEffect(() => { if (!open) return; setData("loading"); setWanted(new Set()); let cancelled = false; const qs = new URLSearchParams(); if (initialName) qs.set("name", initialName); fetch(`/api/discover/preview/${mbid}?${qs.toString()}`) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d: Preview) => { if (cancelled) return; setData(d); setFollowed(d.followed); }) .catch(() => !cancelled && setData("error")); return () => { cancelled = true; }; }, [open, mbid, initialName]); useEffect(() => { if (!open || !lastfmName) { setPlays(null); setWantedAlbums(new Set()); return; } setPlays("loading"); setWantedAlbums(new Set()); let cancelled = false; fetch(`/api/lastfm/artist-plays?artist=${encodeURIComponent(lastfmName)}`) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d: ArtistPlays) => { if (!cancelled) setPlays(d); }) .catch(() => !cancelled && setPlays("error")); return () => { cancelled = true; }; }, [open, lastfmName]); const name = typeof data === "object" ? data.artistName || initialName : initialName; async function follow() { const res = await fetch("/api/artists", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ mbid, name }), }); if (res.ok || res.status === 409) { setFollowed(true); toast(`Following ${name}`); } else { toast(`Couldn't follow ${name}`); } } async function want(r: Rel) { const res = await fetch("/api/discover/want", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ rgMbid: r.rgMbid, artistMbid: mbid, artistName: name, album: r.album, primaryType: r.primaryType, secondaryTypes: r.secondaryTypes, firstReleaseDate: r.firstReleaseDate, }), }); if (res.ok) { setWanted((s) => new Set(s).add(r.rgMbid)); toast(`Added ${r.album}`); } else { toast(`Couldn't add ${r.album}`); } } async function wantAlbum(albumName: string) { const res = await fetch( `/api/mb/release-group?artist=${encodeURIComponent(lastfmName!)}&album=${encodeURIComponent(albumName)}`, ); if (!res.ok) { toast(`No MusicBrainz match for ${albumName}`); return; } const m: ReleaseGroupMatch = await res.json(); const wantRes = await fetch("/api/discover/want", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ rgMbid: m.rgMbid, artistMbid: mbid, artistName: name, album: m.title, primaryType: m.primaryType, secondaryTypes: m.secondaryTypes, firstReleaseDate: m.firstReleaseDate, }), }); if (wantRes.ok) { setWantedAlbums((s) => new Set(s).add(albumName)); toast(`Added ${m.title}`); } } return (

{name || "Artist"}

{followed ? Following : ( )} Full page ↗
{lastfmName ? (
{plays === "loading" ?

Loading your plays…

: null} {plays === "error" ?

Couldn’t load your Last.fm plays.

: null} {plays && typeof plays === "object" ? ( plays.topTracks.length === 0 && plays.topAlbums.length === 0 ? (

No play data.

) : ( <> {plays.topTracks.length > 0 ? ( <>

Most played songs

    {plays.topTracks.map((t, i) => (
  1. {i + 1} {t.name} {t.plays} plays
  2. ))}
) : null} {plays.topAlbums.length > 0 ? ( <>

Most played albums

    {plays.topAlbums.map((album) => { const rel = typeof data === "object" ? data.releases.find( (r) => r.album.toLowerCase().trim() === album.name.toLowerCase().trim(), ) : undefined; const owned = rel?.have ?? false; const working = (rel?.monitored ?? false) || wantedAlbums.has(album.name); return (
  • {album.name}
    {album.plays} plays
    {owned ? ( In library ) : working ? ( Wanted ) : ( )}
  • ); })}
) : null} {plays.sampled ? (

Top of your most recent ~1000 scrobbles.

) : null} ) ) : null}
) : null} {data === "loading" ?

Loading…

: null} {data === "error" ?

Couldn’t load this artist.

: null} {typeof data === "object" ? ( data.releases.length === 0 ? (

No releases found.

) : (
{data.releases.slice(0, 18).map((r) => { const owned = r.have; const monitored = r.monitored || wanted.has(r.rgMbid); return (
{r.album}
{r.primaryType} {r.firstReleaseDate ? ` · ${r.firstReleaseDate.slice(0, 4)}` : ""}
{owned ? ( In library ) : monitored ? ( Wanted ) : ( )}
); })}
) ) : null}
); }