feat(artists): sort control on the roster

Add a Sort dropdown to /artists next to the filter: Recently followed
(default, createdAt desc — the prior order), Name A–Z, Most in library,
Most monitored, Most releases. Client-side sort of the already-fetched
roster; the filter box and "In library, not followed" section are
unchanged. Exposes WatchedArtist.createdAt on /api/artists for the sort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 20:46:24 +02:00
parent 72e9c2d47d
commit 2c3d6e77d5
2 changed files with 24 additions and 2 deletions
+1
View File
@@ -38,6 +38,7 @@ export async function GET(request: Request) {
id: a.id,
mbid: a.mbid,
name: a.name,
createdAt: a.createdAt,
autoMonitorFuture: a.autoMonitorFuture,
showAllTypes: a.showAllTypes,
releaseCount: visible.length,
+23 -2
View File
@@ -10,6 +10,7 @@ type Artist = {
id: string;
mbid: string;
name: string;
createdAt: string;
autoMonitorFuture: boolean;
releaseCount: number;
monitoredCount: number;
@@ -17,11 +18,21 @@ type Artist = {
};
type Hit = { mbid: string; name: string; disambiguation: string };
type SortKey = "followed" | "name" | "have" | "monitored" | "releases";
const SORTS: { key: SortKey; label: string; cmp: (a: Artist, b: Artist) => number }[] = [
{ key: "followed", label: "Recently followed", cmp: (a, b) => b.createdAt.localeCompare(a.createdAt) },
{ key: "name", label: "Name AZ", cmp: (a, b) => a.name.localeCompare(b.name) },
{ key: "have", label: "Most in library", cmp: (a, b) => b.haveCount - a.haveCount || a.name.localeCompare(b.name) },
{ key: "monitored", label: "Most monitored", cmp: (a, b) => b.monitoredCount - a.monitoredCount || a.name.localeCompare(b.name) },
{ key: "releases", label: "Most releases", cmp: (a, b) => b.releaseCount - a.releaseCount || a.name.localeCompare(b.name) },
];
export function ArtistsClient() {
const [artists, setArtists] = useState<Artist[]>([]);
const [ownedNotFollowed, setOwnedNotFollowed] = useState<string[]>([]);
const [justFollowed, setJustFollowed] = useState<Set<string>>(new Set());
const [filter, setFilter] = useState("");
const [sort, setSort] = useState<SortKey>("followed");
const [showAdd, setShowAdd] = useState(false);
const [query, setQuery] = useState("");
const [hits, setHits] = useState<Hit[]>([]);
@@ -113,8 +124,10 @@ export function ArtistsClient() {
const followedMbids = new Set(artists.map((a) => a.mbid));
const shown = useMemo(() => {
const n = filter.trim().toLowerCase();
return n ? artists.filter((a) => a.name.toLowerCase().includes(n)) : artists;
}, [artists, filter]);
const filtered = n ? artists.filter((a) => a.name.toLowerCase().includes(n)) : artists;
const cmp = (SORTS.find((s) => s.key === sort) ?? SORTS[0]).cmp;
return [...filtered].sort(cmp);
}, [artists, filter, sort]);
const ownedShown = useMemo(
() => ownedNotFollowed.filter((name) => !justFollowed.has(name)),
[ownedNotFollowed, justFollowed],
@@ -137,6 +150,14 @@ export function ArtistsClient() {
onChange={(e) => setFilter(e.target.value)}
/>
</label>
<label className="field">
<span>Sort</span>
<select aria-label="sort artists" value={sort} onChange={(e) => setSort(e.target.value as SortKey)}>
{SORTS.map((s) => (
<option key={s.key} value={s.key}>{s.label}</option>
))}
</select>
</label>
</div>
<SectionHeader