diff --git a/web/src/app/artists/[id]/discography-client.tsx b/web/src/app/artists/[id]/discography-client.tsx new file mode 100644 index 0000000..abe4f30 --- /dev/null +++ b/web/src/app/artists/[id]/discography-client.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type Release = { + id: string; + album: string; + primaryType: string | null; + secondaryTypes: string[]; + firstReleaseDate: string | null; + monitored: boolean; + state: string; + currentQualityClass: number | null; +}; +type Detail = { id: string; name: string; autoMonitorFuture: boolean; releases: Release[] }; + +function badge(r: Release): string { + if (r.currentQualityClass !== null) return `Have (q${r.currentQualityClass})`; + if (r.monitored) return r.state === "wanted" ? "Wanted" : r.state; + return "Dormant"; +} + +export function DiscographyClient({ id }: { id: string }) { + const [detail, setDetail] = useState(null); + + async function refresh() { + const res = await fetch(`/api/artists/${id}`); + setDetail(res.ok ? await res.json() : null); + } + useEffect(() => { + refresh(); + }, [id]); + + async function toggleMonitor(r: Release) { + await fetch(`/api/releases/${r.id}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ monitored: !r.monitored }), + }); + refresh(); + } + async function searchNow(r: Release) { + await fetch(`/api/releases/${r.id}/search`, { method: "POST" }); + refresh(); + } + + if (!detail) return

Loading…

; + return ( +
+

{detail.name}

+ +
+ ); +} diff --git a/web/src/app/artists/[id]/page.tsx b/web/src/app/artists/[id]/page.tsx new file mode 100644 index 0000000..33339b6 --- /dev/null +++ b/web/src/app/artists/[id]/page.tsx @@ -0,0 +1,11 @@ +import { DiscographyClient } from "./discography-client"; + +export default async function ArtistDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ( +
+

← Artists

+ +
+ ); +} diff --git a/web/src/app/wanted/page.tsx b/web/src/app/wanted/page.tsx new file mode 100644 index 0000000..34b9aa1 --- /dev/null +++ b/web/src/app/wanted/page.tsx @@ -0,0 +1,11 @@ +import { WantedClient } from "./wanted-client"; + +export default function WantedPage() { + return ( +
+

Wanted

+

Home · Artists

+ +
+ ); +} diff --git a/web/src/app/wanted/wanted-client.tsx b/web/src/app/wanted/wanted-client.tsx new file mode 100644 index 0000000..ef9e32b --- /dev/null +++ b/web/src/app/wanted/wanted-client.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type Wanted = { + id: string; + artistName: string; + album: string; + state: string; + currentQualityClass: number | null; + lastSearchedAt: string | null; +}; + +export function WantedClient() { + const [rows, setRows] = useState([]); + const [artist, setArtist] = useState(""); + const [album, setAlbum] = useState(""); + const [error, setError] = useState(""); + + async function refresh() { + const res = await fetch("/api/wanted"); + setRows((await res.json()).wanted); + } + useEffect(() => { + refresh(); + }, []); + + async function add(e: React.FormEvent) { + e.preventDefault(); + setError(""); + if (!artist.trim() || !album.trim()) return; + const res = await fetch("/api/wanted", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ artist, album }), + }); + if (!res.ok) { + setError((await res.json()).error ?? "failed to add"); + return; + } + setArtist(""); + setAlbum(""); + refresh(); + } + + async function searchNow(w: Wanted) { + await fetch(`/api/releases/${w.id}/search`, { method: "POST" }); + refresh(); + } + + return ( +
+
+ setArtist(e.target.value)} /> + setAlbum(e.target.value)} /> + + {error ? {error} : null} +
+
    + {rows.map((w) => ( +
  • + {w.artistName} — {w.album} · {w.currentQualityClass !== null ? `have q${w.currentQualityClass}, upgrading` : w.state} + {w.lastSearchedAt ? ` · last tried ${new Date(w.lastSearchedAt).toLocaleString()}` : " · never tried"}{" "} + +
  • + ))} +
+
+ ); +}