123 lines
3.6 KiB
TypeScript

import {
createBeatSaverAPI,
type PlaylistFull,
type PlaylistSearchParams,
type PlaylistSearchResponse
} from '$lib/server/beatsaver';
import type { PageServerLoad } from './$types';
function parsePositiveInt(value: string | null, fallback: number): number {
if (!value) return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return parsed;
}
const SORT_ORDER_VALUES = ['Latest', 'Relevance', 'Curated'] as const;
type SortOrderOption = (typeof SORT_ORDER_VALUES)[number];
const SORT_ORDER_SET = new Set<string>(SORT_ORDER_VALUES);
const DEFAULT_SORT_ORDER: SortOrderOption = 'Latest';
function parseSortOrder(value: string | null): SortOrderOption {
if (!value) return DEFAULT_SORT_ORDER;
const normalized = value.trim();
return SORT_ORDER_SET.has(normalized) ? (normalized as SortOrderOption) : DEFAULT_SORT_ORDER;
}
type SearchInfoShape = {
page?: number;
pages?: number;
total?: number;
size?: number;
itemsPerPage?: number;
};
export const load: PageServerLoad = async ({ url, fetch, parent }) => {
const parentData = await parent();
const requestedPage = parsePositiveInt(url.searchParams.get('page'), 1);
const pageIndex = requestedPage - 1;
const api = createBeatSaverAPI(fetch);
const MIN_MAPS = 3;
const hasSubmitted = url.searchParams.has('submitted');
const rawQuery = url.searchParams.get('q') ?? '';
const query = rawQuery.trim();
const rawSortOrder = url.searchParams.get('order') ?? url.searchParams.get('sortOrder');
const sortOrder = parseSortOrder(rawSortOrder);
const curated = url.searchParams.has('curated');
const verified = url.searchParams.has('verified') ? true : hasSubmitted ? false : true;
const searchState = {
submitted: hasSubmitted,
query,
sortOrder,
curated,
verified
};
const searchParams: PlaylistSearchParams = {
page: pageIndex,
sortOrder,
includeEmpty: false
};
if (query) {
searchParams.query = query;
}
if (curated) {
searchParams.curated = true;
}
if (verified) {
searchParams.verified = true;
}
try {
const response = (await api.searchPlaylists({
...searchParams
})) as PlaylistSearchResponse<PlaylistFull>;
const docsRaw = Array.isArray(response?.docs) ? response.docs : [];
const docs = docsRaw.filter((playlist) => {
const totalMaps = playlist?.stats?.totalMaps ?? 0;
return Number.isFinite(totalMaps) && totalMaps >= MIN_MAPS;
});
const info = (response?.info && typeof response.info === 'object' ? (response.info as SearchInfoShape) : null) ?? null;
const infoPage = typeof info?.page === 'number' ? info.page : pageIndex;
const currentPage = infoPage + 1;
const totalPages = typeof info?.pages === 'number' ? info.pages : null;
const total = typeof info?.total === 'number' ? info.total : null;
const pageSize = typeof info?.size === 'number'
? info.size
: typeof info?.itemsPerPage === 'number'
? info.itemsPerPage
: docs.length;
return {
...parentData,
playlists: docs,
info,
page: currentPage,
totalPages,
total,
pageSize,
error: null,
search: searchState
};
} catch (err) {
console.error('Failed to load BeatSaver playlists', err);
return {
...parentData,
playlists: [] as PlaylistFull[],
info: null,
page: requestedPage,
totalPages: null,
total: null,
pageSize: 0,
error: err instanceof Error ? err.message : 'Failed to load playlists',
search: searchState
};
}
};