feat: artists page with live-search follow picker

This commit is contained in:
Jonathan
2026-07-11 14:08:10 +02:00
parent cd652c7285
commit f7135b9046
3 changed files with 111 additions and 1 deletions
+99
View File
@@ -0,0 +1,99 @@
"use client";
import { useEffect, useState } from "react";
type Artist = {
id: string;
mbid: string;
name: string;
autoMonitorFuture: boolean;
releaseCount: number;
monitoredCount: number;
haveCount: number;
};
type Hit = { mbid: string; name: string; disambiguation: string };
export function ArtistsClient() {
const [artists, setArtists] = useState<Artist[]>([]);
const [query, setQuery] = useState("");
const [hits, setHits] = useState<Hit[]>([]);
const [searching, setSearching] = useState(false);
async function refresh() {
const res = await fetch("/api/artists");
setArtists((await res.json()).artists);
}
useEffect(() => {
refresh();
}, []);
async function search(e: React.FormEvent) {
e.preventDefault();
if (!query.trim()) return;
setSearching(true);
try {
const res = await fetch(`/api/mb/artists?q=${encodeURIComponent(query.trim())}`);
setHits(res.ok ? (await res.json()).artists : []);
} finally {
setSearching(false);
}
}
async function follow(hit: Hit) {
await fetch("/api/artists", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ mbid: hit.mbid, name: hit.name }),
});
setHits([]);
setQuery("");
refresh();
}
async function toggleAuto(a: Artist) {
await fetch(`/api/artists/${a.id}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ autoMonitorFuture: !a.autoMonitorFuture }),
});
refresh();
}
async function unfollow(a: Artist) {
await fetch(`/api/artists/${a.id}`, { method: "DELETE" });
refresh();
}
return (
<div>
<form onSubmit={search}>
<input aria-label="artist search" placeholder="Search an artist…" value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
{searching ? <p>Searching</p> : null}
{hits.length > 0 ? (
<ul>
{hits.map((h) => (
<li key={h.mbid}>
{h.name}
{h.disambiguation ? ` (${h.disambiguation})` : ""}{" "}
<button onClick={() => follow(h)}>Follow</button>
</li>
))}
</ul>
) : null}
<ul>
{artists.map((a) => (
<li key={a.id}>
<a href={`/artists/${a.id}`}>{a.name}</a> · {a.haveCount}/{a.monitoredCount} have/monitored of {a.releaseCount}{" "}
<label>
<input type="checkbox" aria-label={`auto-monitor ${a.name}`} checked={a.autoMonitorFuture} onChange={() => toggleAuto(a)} /> auto-monitor future
</label>{" "}
<button onClick={() => unfollow(a)}>Unfollow</button>
</li>
))}
</ul>
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { ArtistsClient } from "./artists-client";
export default function ArtistsPage() {
return (
<main>
<h1>Watched artists</h1>
<p><a href="/">Home</a> · <a href="/wanted">Wanted</a></p>
<ArtistsClient />
</main>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@ export default function Home() {
return (
<main>
<h1>Lyra</h1>
<p><a href="/settings">Settings</a></p>
<p><a href="/artists">Artists</a> · <a href="/wanted">Wanted</a> · <a href="/settings">Settings</a></p>
<Queue />
</main>
);