34235a2355
CoverArt loads album art from the Cover Art Archive CDN by release-group MBID (browser-side, cached, graceful placeholder fallback). AlbumModal shows cover + tracklist (fetched from MB) with slots for a status chip and context actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, type ReactNode } from "react";
|
|
import { Modal } from "./modal";
|
|
import { CoverArt } from "./cover-art";
|
|
|
|
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 type AlbumInfo = {
|
|
rgMbid: string | null;
|
|
album: string;
|
|
artist: string;
|
|
year?: string | null;
|
|
type?: string | null;
|
|
};
|
|
|
|
/** Reusable album modal: cover art + tracklist (fetched from MB by release-group MBID) with
|
|
* slots for a status chip and context-specific action buttons. */
|
|
export function AlbumModal({
|
|
open,
|
|
onClose,
|
|
album,
|
|
status,
|
|
actions,
|
|
}: {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
album: AlbumInfo;
|
|
status?: ReactNode;
|
|
actions?: ReactNode;
|
|
}) {
|
|
const [tracks, setTracks] = useState<Track[] | "loading" | "error">("loading");
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
if (!album.rgMbid) {
|
|
setTracks("error");
|
|
return;
|
|
}
|
|
setTracks("loading");
|
|
let cancelled = false;
|
|
fetch(`/api/mb/release-groups/${album.rgMbid}/tracks`)
|
|
.then((r) => (r.ok ? r.json() : Promise.reject()))
|
|
.then((d) => !cancelled && setTracks(d.tracks))
|
|
.catch(() => !cancelled && setTracks("error"));
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [open, album.rgMbid]);
|
|
|
|
return (
|
|
<Modal open={open} onClose={onClose} title={album.artist} wide>
|
|
<div className="album-modal-head">
|
|
<CoverArt rgMbid={album.rgMbid} alt={album.album} size={250} className="album-modal-cover" />
|
|
<div>
|
|
<h2 className="album-modal-title">{album.album}</h2>
|
|
<div className="album-modal-meta">
|
|
{album.type ? <span>{album.type}</span> : null}
|
|
{album.year ? (
|
|
<>
|
|
<span className="dot">·</span>
|
|
<span>{album.year}</span>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
{status ? <div className="album-modal-status">{status}</div> : null}
|
|
{actions ? <div className="actions album-modal-actions">{actions}</div> : null}
|
|
</div>
|
|
</div>
|
|
<div className="album-modal-tracks">
|
|
{tracks === "loading" ? <p className="muted-note">Loading tracks…</p> : null}
|
|
{tracks === "error" ? <p className="muted-note">No tracklist available.</p> : null}
|
|
{Array.isArray(tracks) ? (
|
|
<ol className="tracks">
|
|
{tracks.map((t) => (
|
|
<li key={t.position}>
|
|
<span className="tnum">{t.position}</span>
|
|
<span className="tname">{t.title}</span>
|
|
{t.lengthMs != null ? <span>{fmt(t.lengthMs)}</span> : null}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
) : null}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|