From 86657c5a4f02a1d07149e0a871d3bfa7b9eb71ef Mon Sep 17 00:00:00 2001 From: pleb Date: Tue, 4 Nov 2025 08:56:52 -0800 Subject: [PATCH] Add search to the playlist discovery --- src/app.css | 53 +++ .../tools/playlist-discovery/+page.server.ts | 60 ++- .../tools/playlist-discovery/+page.svelte | 352 +++++++++++++++++- 3 files changed, 452 insertions(+), 13 deletions(-) diff --git a/src/app.css b/src/app.css index c597220..f57e3e4 100644 --- a/src/app.css +++ b/src/app.css @@ -58,3 +58,56 @@ p a:hover { @utility neon-text { @apply text-transparent bg-clip-text bg-gradient-to-r from-neon via-accent to-neon-fuchsia; } + +input[type='checkbox'] { + appearance: none; + width: 1.05rem; + height: 1.05rem; + border-radius: 0.35rem; + border: 1px solid rgba(148, 163, 184, 0.35); + background-color: rgba(15, 23, 42, 0.85); + display: grid; + place-content: center; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease, transform 0.2s ease; + cursor: pointer; +} + +input[type='checkbox']::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + clip-path: polygon(14% 44%, 0 60%, 43% 100%, 100% 16%, 86% 0, 40% 64%); + background-color: transparent; + transform: scale(0.4) rotate(6deg); + opacity: 0; + transition: opacity 0.12s ease, transform 0.12s ease, background-color 0.12s ease; +} + +input[type='checkbox']:hover { + border-color: rgba(94, 234, 212, 0.4); +} + +input[type='checkbox']:focus-visible { + outline: none; + border-color: rgba(94, 234, 212, 0.6); + box-shadow: 0 0 0 2px rgba(94, 234, 212, 0.25); +} + +input[type='checkbox']:checked { + border-color: rgba(34, 211, 238, 0.8); + background-color: rgba(34, 211, 238, 0.18); + box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.2); +} + +input[type='checkbox']:checked::after { + background-color: var(--color-neon); + opacity: 1; + transform: scale(1) rotate(0deg); +} + +input[type='checkbox']:disabled { + cursor: not-allowed; + opacity: 0.55; + box-shadow: none; + border-color: rgba(148, 163, 184, 0.2); +} diff --git a/src/routes/tools/playlist-discovery/+page.server.ts b/src/routes/tools/playlist-discovery/+page.server.ts index e13a195..78ea2ea 100644 --- a/src/routes/tools/playlist-discovery/+page.server.ts +++ b/src/routes/tools/playlist-discovery/+page.server.ts @@ -1,4 +1,9 @@ -import { createBeatSaverAPI, type PlaylistFull, type PlaylistSearchResponse } from '$lib/server/beatsaver'; +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 { @@ -8,6 +13,17 @@ function parsePositiveInt(value: string | null, fallback: number): number { return parsed; } +const SORT_ORDER_VALUES = ['Latest', 'Relevance', 'Curated'] as const; +type SortOrderOption = (typeof SORT_ORDER_VALUES)[number]; +const SORT_ORDER_SET = new Set(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; @@ -23,13 +39,41 @@ export const load: PageServerLoad = async ({ url, fetch, parent }) => { 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({ - page: pageIndex, - sortOrder: 'Latest', - verified: true, - includeEmpty: false + ...searchParams })) as PlaylistSearchResponse; const docsRaw = Array.isArray(response?.docs) ? response.docs : []; const docs = docsRaw.filter((playlist) => { @@ -56,7 +100,8 @@ export const load: PageServerLoad = async ({ url, fetch, parent }) => { totalPages, total, pageSize, - error: null + error: null, + search: searchState }; } catch (err) { console.error('Failed to load BeatSaver playlists', err); @@ -68,7 +113,8 @@ export const load: PageServerLoad = async ({ url, fetch, parent }) => { totalPages: null, total: null, pageSize: 0, - error: err instanceof Error ? err.message : 'Failed to load playlists' + error: err instanceof Error ? err.message : 'Failed to load playlists', + search: searchState }; } }; diff --git a/src/routes/tools/playlist-discovery/+page.svelte b/src/routes/tools/playlist-discovery/+page.svelte index 351337a..41d96f9 100644 --- a/src/routes/tools/playlist-discovery/+page.svelte +++ b/src/routes/tools/playlist-discovery/+page.svelte @@ -1,4 +1,5 @@