feat: artist discography page + wanted page
This commit is contained in:
@@ -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<Detail | null>(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 <p>Loading…</p>;
|
||||
return (
|
||||
<div>
|
||||
<h1>{detail.name}</h1>
|
||||
<ul>
|
||||
{detail.releases.map((r) => (
|
||||
<li key={r.id}>
|
||||
<strong>{r.album}</strong>
|
||||
{r.firstReleaseDate ? ` (${r.firstReleaseDate.slice(0, 4)})` : ""}
|
||||
{r.primaryType ? ` · ${r.primaryType}` : ""}
|
||||
{r.secondaryTypes.length ? ` [${r.secondaryTypes.join(", ")}]` : ""} · <em>{badge(r)}</em>{" "}
|
||||
<label>
|
||||
<input type="checkbox" aria-label={`monitor ${r.album}`} checked={r.monitored} onChange={() => toggleMonitor(r)} /> monitor
|
||||
</label>{" "}
|
||||
<button onClick={() => searchNow(r)}>Search now</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DiscographyClient } from "./discography-client";
|
||||
|
||||
export default async function ArtistDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return (
|
||||
<main>
|
||||
<p><a href="/artists">← Artists</a></p>
|
||||
<DiscographyClient id={id} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WantedClient } from "./wanted-client";
|
||||
|
||||
export default function WantedPage() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Wanted</h1>
|
||||
<p><a href="/">Home</a> · <a href="/artists">Artists</a></p>
|
||||
<WantedClient />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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<Wanted[]>([]);
|
||||
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 (
|
||||
<div>
|
||||
<form onSubmit={add}>
|
||||
<input aria-label="wanted artist" placeholder="Artist" value={artist} onChange={(e) => setArtist(e.target.value)} />
|
||||
<input aria-label="wanted album" placeholder="Album" value={album} onChange={(e) => setAlbum(e.target.value)} />
|
||||
<button type="submit">Add to wanted</button>
|
||||
{error ? <span> {error}</span> : null}
|
||||
</form>
|
||||
<ul>
|
||||
{rows.map((w) => (
|
||||
<li key={w.id}>
|
||||
{w.artistName} — {w.album} · <em>{w.currentQualityClass !== null ? `have q${w.currentQualityClass}, upgrading` : w.state}</em>
|
||||
{w.lastSearchedAt ? ` · last tried ${new Date(w.lastSearchedAt).toLocaleString()}` : " · never tried"}{" "}
|
||||
<button onClick={() => searchNow(w)}>Search now</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user