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:
@@ -38,6 +38,7 @@ export async function GET(request: Request) {
|
|||||||
id: a.id,
|
id: a.id,
|
||||||
mbid: a.mbid,
|
mbid: a.mbid,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
|
createdAt: a.createdAt,
|
||||||
autoMonitorFuture: a.autoMonitorFuture,
|
autoMonitorFuture: a.autoMonitorFuture,
|
||||||
showAllTypes: a.showAllTypes,
|
showAllTypes: a.showAllTypes,
|
||||||
releaseCount: visible.length,
|
releaseCount: visible.length,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type Artist = {
|
|||||||
id: string;
|
id: string;
|
||||||
mbid: string;
|
mbid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
autoMonitorFuture: boolean;
|
autoMonitorFuture: boolean;
|
||||||
releaseCount: number;
|
releaseCount: number;
|
||||||
monitoredCount: number;
|
monitoredCount: number;
|
||||||
@@ -17,11 +18,21 @@ type Artist = {
|
|||||||
};
|
};
|
||||||
type Hit = { mbid: string; name: string; disambiguation: string };
|
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 A–Z", 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() {
|
export function ArtistsClient() {
|
||||||
const [artists, setArtists] = useState<Artist[]>([]);
|
const [artists, setArtists] = useState<Artist[]>([]);
|
||||||
const [ownedNotFollowed, setOwnedNotFollowed] = useState<string[]>([]);
|
const [ownedNotFollowed, setOwnedNotFollowed] = useState<string[]>([]);
|
||||||
const [justFollowed, setJustFollowed] = useState<Set<string>>(new Set());
|
const [justFollowed, setJustFollowed] = useState<Set<string>>(new Set());
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
|
const [sort, setSort] = useState<SortKey>("followed");
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [hits, setHits] = useState<Hit[]>([]);
|
const [hits, setHits] = useState<Hit[]>([]);
|
||||||
@@ -113,8 +124,10 @@ export function ArtistsClient() {
|
|||||||
const followedMbids = new Set(artists.map((a) => a.mbid));
|
const followedMbids = new Set(artists.map((a) => a.mbid));
|
||||||
const shown = useMemo(() => {
|
const shown = useMemo(() => {
|
||||||
const n = filter.trim().toLowerCase();
|
const n = filter.trim().toLowerCase();
|
||||||
return n ? artists.filter((a) => a.name.toLowerCase().includes(n)) : artists;
|
const filtered = n ? artists.filter((a) => a.name.toLowerCase().includes(n)) : artists;
|
||||||
}, [artists, filter]);
|
const cmp = (SORTS.find((s) => s.key === sort) ?? SORTS[0]).cmp;
|
||||||
|
return [...filtered].sort(cmp);
|
||||||
|
}, [artists, filter, sort]);
|
||||||
const ownedShown = useMemo(
|
const ownedShown = useMemo(
|
||||||
() => ownedNotFollowed.filter((name) => !justFollowed.has(name)),
|
() => ownedNotFollowed.filter((name) => !justFollowed.has(name)),
|
||||||
[ownedNotFollowed, justFollowed],
|
[ownedNotFollowed, justFollowed],
|
||||||
@@ -137,6 +150,14 @@ export function ArtistsClient() {
|
|||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
|
|||||||
Reference in New Issue
Block a user