diff --git a/web/src/lib/api/queries.ts b/web/src/lib/api/queries.ts index 1d684959..307a8f90 100644 --- a/web/src/lib/api/queries.ts +++ b/web/src/lib/api/queries.ts @@ -32,6 +32,8 @@ export const qk = { adminQuarantine: () => ['adminQuarantine'] as const, adminQuarantineActions: (limit?: number) => ['adminQuarantineActions', { limit: limit ?? 50 }] as const, + suggestions: (limit?: number) => + ['suggestions', { limit: limit ?? 12 }] as const, }; export function createArtistsQuery(sort: ArtistSort) { diff --git a/web/src/lib/api/suggestions.test.ts b/web/src/lib/api/suggestions.test.ts new file mode 100644 index 00000000..094bd4ba --- /dev/null +++ b/web/src/lib/api/suggestions.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('./client', () => ({ + api: { get: vi.fn() } +})); + +import { listSuggestions } from './suggestions'; +import { qk } from './queries'; +import { api } from './client'; +import type { ArtistSuggestion } from './types'; + +afterEach(() => vi.clearAllMocks()); + +describe('suggestions client', () => { + test('listSuggestions hits the right URL with default limit', async () => { + const fixture: ArtistSuggestion[] = [ + { + mbid: 'm1', + name: 'Outsider', + score: 1.5, + attribution: [ + { artist_id: 'a1', name: 'Seed', contribution: 0.9, is_liked: true, play_count: 0 } + ] + } + ]; + (api.get as ReturnType).mockResolvedValueOnce(fixture); + const got = await listSuggestions(); + expect(api.get).toHaveBeenCalledWith('/api/discover/suggestions?limit=12'); + expect(got).toEqual(fixture); + }); + + test('listSuggestions honors a custom limit', async () => { + (api.get as ReturnType).mockResolvedValueOnce([]); + await listSuggestions(20); + expect(api.get).toHaveBeenCalledWith('/api/discover/suggestions?limit=20'); + }); + + test('qk.suggestions key shape', () => { + expect(qk.suggestions()).toEqual(['suggestions', { limit: 12 }]); + expect(qk.suggestions(20)).toEqual(['suggestions', { limit: 20 }]); + }); +}); diff --git a/web/src/lib/api/suggestions.ts b/web/src/lib/api/suggestions.ts new file mode 100644 index 00000000..2d998898 --- /dev/null +++ b/web/src/lib/api/suggestions.ts @@ -0,0 +1,16 @@ +import { createQuery } from '@tanstack/svelte-query'; +import { api } from './client'; +import { qk } from './queries'; +import type { ArtistSuggestion } from './types'; + +export async function listSuggestions(limit = 12): Promise { + return api.get(`/api/discover/suggestions?limit=${limit}`); +} + +export function createSuggestionsQuery(limit = 12) { + return createQuery({ + queryKey: qk.suggestions(limit), + queryFn: () => listSuggestions(limit), + staleTime: 5 * 60_000 // 5 minutes — see M5c spec §5 + }); +} diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts index 6b8ce1fc..86674f63 100644 --- a/web/src/lib/api/types.ts +++ b/web/src/lib/api/types.ts @@ -221,3 +221,18 @@ export type ActionResult = { affected_users: number; deleted_track_count?: number; }; + +export type SeedContribution = { + artist_id: string; + name: string; + contribution: number; + is_liked: boolean; + play_count: number; +}; + +export type ArtistSuggestion = { + mbid: string; + name: string; + score: number; + attribution: SeedContribution[]; // up to 3 entries, ordered by contribution DESC +};