feat: artists page with live-search follow picker
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user