"use client"; import { useCallback, useEffect, useState } from "react"; type Release = { rgMbid: string; album: string; primaryType: string | null; secondaryTypes: string[]; firstReleaseDate: string | null; monitored: boolean; have: boolean; }; type Preview = { artistName: string; followed: boolean; releases: Release[] }; type Track = { position: number; title: string; lengthMs: number | null }; function fmt(ms: number | null): string { if (ms == null) return ""; const s = Math.round(ms / 1000); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; } export function PreviewClient({ mbid, initialName }: { mbid: string; initialName: string }) { const [data, setData] = useState(null); const [showAll, setShowAll] = useState(false); const [followed, setFollowed] = useState(false); const [tracks, setTracks] = useState>({}); const [error, setError] = useState(false); const load = useCallback(async () => { setError(false); const qs = new URLSearchParams(); if (initialName) qs.set("name", initialName); if (showAll) qs.set("all", "true"); const res = await fetch(`/api/discover/preview/${mbid}?${qs.toString()}`); if (!res.ok) { setError(true); return; } const d: Preview = await res.json(); setData(d); setFollowed(d.followed); }, [mbid, initialName, showAll]); useEffect(() => { load(); }, [load]); // Scroll to the album an album-suggestion deep-linked to (#rg-) once data is in. useEffect(() => { if (!data) return; const hash = window.location.hash; if (!hash) return; document.getElementById(hash.slice(1))?.scrollIntoView(); }, [data]); async function follow() { const name = data?.artistName || initialName; 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); } async function want(r: Release) { await fetch("/api/discover/want", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ rgMbid: r.rgMbid, artistMbid: mbid, artistName: data?.artistName || initialName, album: r.album, primaryType: r.primaryType, secondaryTypes: r.secondaryTypes, firstReleaseDate: r.firstReleaseDate, }), }); setData((d) => d ? { ...d, releases: d.releases.map((x) => (x.rgMbid === r.rgMbid ? { ...x, monitored: true } : x)) } : d, ); } async function toggleTracks(rgMbid: string) { if (tracks[rgMbid]) { setTracks((t) => { const n = { ...t }; delete n[rgMbid]; return n; }); return; } setTracks((t) => ({ ...t, [rgMbid]: "loading" })); const res = await fetch(`/api/mb/release-groups/${rgMbid}/tracks`); if (!res.ok) { setTracks((t) => ({ ...t, [rgMbid]: "error" })); return; } const { tracks: ts } = await res.json(); setTracks((t) => ({ ...t, [rgMbid]: ts })); } if (error) return (

Couldn’t load this artist.

); if (!data) return

Loading…

; return (

{data.artistName || "Artist"}

{followed ? Following : } {" · "} MusicBrainz ↗

    {data.releases.map((r) => (
  • {r.album}{" "} {r.primaryType} {r.firstReleaseDate ? ` · ${r.firstReleaseDate.slice(0, 4)}` : ""} {" "} {r.have ? In library : r.monitored ? Monitored : null}{" "} {" "} {tracks[r.rgMbid] === "loading" &&

    Loading tracks…

    } {tracks[r.rgMbid] === "error" &&

    Couldn’t load tracks.

    } {Array.isArray(tracks[r.rgMbid]) && (
      {(tracks[r.rgMbid] as Track[]).map((t) => (
    1. {t.title} {t.lengthMs != null && ({fmt(t.lengthMs)})}
    2. ))}
    )}
  • ))}
); }