From 7087b2780e6b7aac8e2926256ae7b08efd99d335 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sat, 11 Jul 2026 14:11:53 +0200 Subject: [PATCH] feat: artist discography page + wanted page --- .../app/artists/[id]/discography-client.tsx | 67 ++++++++++++++++++ web/src/app/artists/[id]/page.tsx | 11 +++ web/src/app/wanted/page.tsx | 11 +++ web/src/app/wanted/wanted-client.tsx | 70 +++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 web/src/app/artists/[id]/discography-client.tsx create mode 100644 web/src/app/artists/[id]/page.tsx create mode 100644 web/src/app/wanted/page.tsx create mode 100644 web/src/app/wanted/wanted-client.tsx 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}

+
    + {detail.releases.map((r) => ( +
  • + {r.album} + {r.firstReleaseDate ? ` (${r.firstReleaseDate.slice(0, 4)})` : ""} + {r.primaryType ? ` · ${r.primaryType}` : ""} + {r.secondaryTypes.length ? ` [${r.secondaryTypes.join(", ")}]` : ""} · {badge(r)}{" "} + {" "} + +
  • + ))} +
+
+ ); +} 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"}{" "} + +
  • + ))} +
+
+ ); +}