feat(web): add search query helpers (summary + 3 facet infinite queries)

createSearchQuery hits /api/search with limit=10 (the main /search
page summary). createSearchArtistsInfiniteQuery /
createSearchAlbumsInfiniteQuery / createSearchTracksInfiniteQuery each
hit /api/search with limit=50 and project to their own facet, used by
the per-facet overflow pages. All four are gated by q.length > 0 so
empty queries don't fire.
This commit is contained in:
2026-04-25 15:30:06 -04:00
parent 7dd77a4723
commit 7eb051c6f7
2 changed files with 81 additions and 1 deletions
+15
View File
@@ -15,3 +15,18 @@ describe('qk key generators', () => {
expect(qk.album('xyz')).toEqual(['album', 'xyz']);
});
});
describe('qk search keys', () => {
test('search summary key includes q', () => {
expect(qk.search('foo')).toEqual(['search', { q: 'foo' }]);
});
test('searchArtists key includes q', () => {
expect(qk.searchArtists('foo')).toEqual(['searchArtists', { q: 'foo' }]);
});
test('searchAlbums key includes q', () => {
expect(qk.searchAlbums('foo')).toEqual(['searchAlbums', { q: 'foo' }]);
});
test('searchTracks key includes q', () => {
expect(qk.searchTracks('foo')).toEqual(['searchTracks', { q: 'foo' }]);
});
});
+66 -1
View File
@@ -1,14 +1,20 @@
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query';
import { api } from './client';
import type { ArtistRef, ArtistDetail, AlbumDetail, Page } from './types';
import type { ArtistRef, ArtistDetail, AlbumDetail, Page, SearchResponse } from './types';
export type ArtistSort = 'alpha' | 'newest';
export const ARTIST_PAGE_SIZE = 50;
export const SEARCH_SUMMARY_LIMIT = 10;
export const SEARCH_FACET_PAGE_SIZE = 50;
export const qk = {
artists: (sort: ArtistSort) => ['artists', { sort }] as const,
artist: (id: string) => ['artist', id] as const,
album: (id: string) => ['album', id] as const,
search: (q: string) => ['search', { q }] as const,
searchArtists: (q: string) => ['searchArtists', { q }] as const,
searchAlbums: (q: string) => ['searchAlbums', { q }] as const,
searchTracks: (q: string) => ['searchTracks', { q }] as const,
};
export function createArtistsQuery(sort: ArtistSort) {
@@ -39,3 +45,62 @@ export function createAlbumQuery(id: string) {
queryFn: () => api.get<AlbumDetail>(`/api/albums/${id}`),
});
}
export function createSearchQuery(q: string) {
return createQuery({
queryKey: qk.search(q),
queryFn: () =>
api.get<SearchResponse>(
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_SUMMARY_LIMIT}`
),
enabled: q.length > 0
});
}
export function createSearchArtistsInfiniteQuery(q: string) {
return createInfiniteQuery({
queryKey: qk.searchArtists(q),
queryFn: ({ pageParam = 0 }) =>
api.get<SearchResponse>(
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
).then((r) => r.artists),
initialPageParam: 0,
getNextPageParam: (last) => {
const loaded = last.offset + last.items.length;
return loaded >= last.total ? undefined : loaded;
},
enabled: q.length > 0
});
}
export function createSearchAlbumsInfiniteQuery(q: string) {
return createInfiniteQuery({
queryKey: qk.searchAlbums(q),
queryFn: ({ pageParam = 0 }) =>
api.get<SearchResponse>(
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
).then((r) => r.albums),
initialPageParam: 0,
getNextPageParam: (last) => {
const loaded = last.offset + last.items.length;
return loaded >= last.total ? undefined : loaded;
},
enabled: q.length > 0
});
}
export function createSearchTracksInfiniteQuery(q: string) {
return createInfiniteQuery({
queryKey: qk.searchTracks(q),
queryFn: ({ pageParam = 0 }) =>
api.get<SearchResponse>(
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
).then((r) => r.tracks),
initialPageParam: 0,
getNextPageParam: (last) => {
const loaded = last.offset + last.items.length;
return loaded >= last.total ? undefined : loaded;
},
enabled: q.length > 0
});
}