songs remove complicated params that don't work
This commit is contained in:
@@ -9,26 +9,15 @@
|
|||||||
import { trackFromSongRow } from "$lib/player/types";
|
import { trackFromSongRow } from "$lib/player/types";
|
||||||
import { SongCategoryMap, SongTypeReverseMap } from "$lib/utils/amq";
|
import { SongCategoryMap, SongTypeReverseMap } from "$lib/utils/amq";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import { type SvelteSearchForm, SvelteSearchParamsSchema } from "./schema";
|
import { SearchParamsSchema } from "./schema";
|
||||||
|
|
||||||
const params = useSearchParams(SvelteSearchParamsSchema, {
|
const params = useSearchParams(SearchParamsSchema, {
|
||||||
pushHistory: false,
|
pushHistory: false,
|
||||||
showDefaults: false,
|
showDefaults: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
// Local form values, bound to inputs, which will update params.
|
|
||||||
let form: SvelteSearchForm = $state({
|
|
||||||
q: params.q,
|
|
||||||
artist: params.artist,
|
|
||||||
anime: params.anime,
|
|
||||||
type: params.type,
|
|
||||||
gpm: params.gpm ?? "",
|
|
||||||
gpx: params.gpx ?? "",
|
|
||||||
cat: params.cat ?? "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// If SSR returned no songRows (because client DB wasn't available),
|
// If SSR returned no songRows (because client DB wasn't available),
|
||||||
// re-run load on the client once the DB is ready by invalidating.
|
// re-run load on the client once the DB is ready by invalidating.
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -50,11 +39,6 @@
|
|||||||
return SongTypeReverseMap[type] || `Type ${type}`;
|
return SongTypeReverseMap[type] || `Type ${type}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to map song category number back to string for UI display
|
|
||||||
function getSongCategoryLabel(category: number): string {
|
|
||||||
return SongCategoryMap[category] || `Category ${category}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracksFromResults = $derived.by(() =>
|
const tracksFromResults = $derived.by(() =>
|
||||||
data.songRows
|
data.songRows
|
||||||
.map((r) =>
|
.map((r) =>
|
||||||
@@ -70,27 +54,6 @@
|
|||||||
)
|
)
|
||||||
.filter((t) => t !== null),
|
.filter((t) => t !== null),
|
||||||
);
|
);
|
||||||
|
|
||||||
function applyFilters() {
|
|
||||||
params.q = form.q;
|
|
||||||
params.artist = form.artist;
|
|
||||||
params.anime = form.anime;
|
|
||||||
params.type = form.type;
|
|
||||||
// Only set gpm/gpx if they are valid numbers
|
|
||||||
params.gpm = form.gpm !== "" ? String(form.gpm) : undefined;
|
|
||||||
params.gpx = form.gpx !== "" ? String(form.gpx) : undefined;
|
|
||||||
// Only set cat if it's a valid number
|
|
||||||
params.cat = form.cat !== "" ? String(form.cat) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle checkbox changes for song types
|
|
||||||
function handleSongTypeChange(typeValue: string, isChecked: boolean) {
|
|
||||||
if (isChecked) {
|
|
||||||
form.type = [...form.type, typeValue];
|
|
||||||
} else {
|
|
||||||
form.type = form.type.filter((t) => t !== typeValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-2xl font-semibold">Songs Search</h1>
|
<h1 class="text-2xl font-semibold">Songs Search</h1>
|
||||||
@@ -101,14 +64,21 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form
|
<form class="mt-4 flex flex-col gap-4">
|
||||||
class="mt-4 flex flex-col gap-4"
|
|
||||||
onsubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
applyFilters();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground" for="anime-name"
|
||||||
|
>Anime Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="anime-name"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
placeholder="e.g. Bakemonogatari"
|
||||||
|
bind:value={params.anime}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-sm text-muted-foreground" for="song-name"
|
<label class="text-sm text-muted-foreground" for="song-name"
|
||||||
>Song Name</label
|
>Song Name</label
|
||||||
@@ -117,7 +87,7 @@
|
|||||||
id="song-name"
|
id="song-name"
|
||||||
class="rounded border px-3 py-2 text-sm"
|
class="rounded border px-3 py-2 text-sm"
|
||||||
placeholder="e.g. Renai Circulation"
|
placeholder="e.g. Renai Circulation"
|
||||||
bind:value={form.q}
|
bind:value={params.song}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
/>
|
/>
|
||||||
@@ -131,44 +101,12 @@
|
|||||||
id="artist-name"
|
id="artist-name"
|
||||||
class="rounded border px-3 py-2 text-sm"
|
class="rounded border px-3 py-2 text-sm"
|
||||||
placeholder="e.g. Kana Hanazawa"
|
placeholder="e.g. Kana Hanazawa"
|
||||||
bind:value={form.artist}
|
bind:value={params.artist}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-sm text-muted-foreground" for="anime-name"
|
|
||||||
>Anime Name</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="anime-name"
|
|
||||||
class="rounded border px-3 py-2 text-sm"
|
|
||||||
placeholder="e.g. Bakemonogatari"
|
|
||||||
bind:value={form.anime}
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-sm text-muted-foreground">Song Type</label>
|
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-2">
|
|
||||||
{#each [1, 2, 3] as typeNum}
|
|
||||||
<label class="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
value={getSongTypeLabel(typeNum)}
|
|
||||||
checked={form.type.includes(getSongTypeLabel(typeNum))}
|
|
||||||
onchange={(e) =>
|
|
||||||
handleSongTypeChange(e.target.value, e.target.checked)}
|
|
||||||
/>
|
|
||||||
{getSongTypeLabel(typeNum)}
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-sm text-muted-foreground" for="global-percent-min"
|
<label class="text-sm text-muted-foreground" for="global-percent-min"
|
||||||
>Global Percent Range</label
|
>Global Percent Range</label
|
||||||
@@ -181,7 +119,7 @@
|
|||||||
max="100"
|
max="100"
|
||||||
class="rounded border px-3 py-2 text-sm w-1/2"
|
class="rounded border px-3 py-2 text-sm w-1/2"
|
||||||
placeholder="Min (0-100)"
|
placeholder="Min (0-100)"
|
||||||
bind:value={form.gpm}
|
bind:value={params.gpm}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
id="global-percent-max"
|
id="global-percent-max"
|
||||||
@@ -190,35 +128,10 @@
|
|||||||
max="100"
|
max="100"
|
||||||
class="rounded border px-3 py-2 text-sm w-1/2"
|
class="rounded border px-3 py-2 text-sm w-1/2"
|
||||||
placeholder="Max (0-100)"
|
placeholder="Max (0-100)"
|
||||||
bind:value={form.gpx}
|
bind:value={params.gpx}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-sm text-muted-foreground" for="song-category"
|
|
||||||
>Category</label
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
id="song-category"
|
|
||||||
class="rounded border px-3 py-2 text-sm"
|
|
||||||
bind:value={form.cat}
|
|
||||||
>
|
|
||||||
<option value="">All</option>
|
|
||||||
{#each Object.entries(SongCategoryMap) as [key, value]}
|
|
||||||
<option value={key}>{value}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-start">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="rounded border px-4 py-2 text-sm bg-blue-500 text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Search Songs
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -266,7 +179,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else if Object.values(form).some((val) => (Array.isArray(val) && val.length > 0) || (typeof val === "string" && val.trim() !== "") || typeof val === "number")}
|
{:else if Object.values(params).some((val) => (Array.isArray(val) && val.length > 0) || (typeof val === "string" && val.trim() !== "") || typeof val === "number")}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">
|
<p class="mt-4 text-sm text-muted-foreground">
|
||||||
No songs found matching your criteria.
|
No songs found matching your criteria.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import type { SongFilters } from "$lib/db/client-db";
|
import type { SongFilters } from "$lib/db/client-db";
|
||||||
import { db, ensureSeeded, getSongsWithFilters } from "$lib/db/client-db";
|
import { db, ensureSeeded, getSongsWithFilters } from "$lib/db/client-db";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
import { LoadSearchParamsSchema } from "./schema";
|
import { SearchParamsSchema } from "./schema";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ url, fetch, depends }) => {
|
export const load: PageLoad = async ({ url, fetch, depends }) => {
|
||||||
depends("clientdb:songs");
|
depends("clientdb:songs");
|
||||||
|
|
||||||
const parsed = LoadSearchParamsSchema.safeParse(
|
const parsed = SearchParamsSchema.safeParse(
|
||||||
Object.fromEntries(url.searchParams.entries()),
|
Object.fromEntries(url.searchParams.entries()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters: SongFilters = {};
|
const filters: SongFilters = {};
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
if (parsed.data.q) filters.songName = parsed.data.q;
|
if (parsed.data.song) filters.songName = parsed.data.song;
|
||||||
if (parsed.data.artist) filters.artistName = parsed.data.artist;
|
if (parsed.data.artist) filters.artistName = parsed.data.artist;
|
||||||
if (parsed.data.anime) filters.animeName = parsed.data.anime;
|
if (parsed.data.anime) filters.animeName = parsed.data.anime;
|
||||||
if (parsed.data.type && parsed.data.type.length > 0)
|
|
||||||
filters.songTypes = parsed.data.type;
|
|
||||||
if (parsed.data.gpm !== undefined)
|
if (parsed.data.gpm !== undefined)
|
||||||
filters.globalPercentMin = parsed.data.gpm;
|
filters.globalPercentMin = parsed.data.gpm;
|
||||||
if (parsed.data.gpx !== undefined)
|
if (parsed.data.gpx !== undefined)
|
||||||
filters.globalPercentMax = parsed.data.gpx;
|
filters.globalPercentMax = parsed.data.gpx;
|
||||||
if (parsed.data.cat !== undefined) filters.category = parsed.data.cat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser.
|
// Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser.
|
||||||
|
|||||||
@@ -1,38 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SongTypeMap } from "$lib/utils/amq";
|
|
||||||
|
|
||||||
// Base schema for raw URL search parameters as strings
|
export const SearchParamsSchema = z.object({
|
||||||
const BaseSearchParamsSchema = z.object({
|
song: z.string().optional().default(""),
|
||||||
q: z.string().optional(),
|
artist: z.string().optional().default(""),
|
||||||
artist: z.string().optional(),
|
anime: z.string().optional().default(""),
|
||||||
anime: z.string().optional(),
|
gpm: z.int().optional().default(0),
|
||||||
gpm: z.string().optional(),
|
gpx: z.int().optional().default(100),
|
||||||
gpx: z.string().optional(),
|
|
||||||
cat: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema for +page.ts load function (parses comma-separated types into number array)
|
|
||||||
export const LoadSearchParamsSchema = BaseSearchParamsSchema.extend({
|
|
||||||
type: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.transform((s) => {
|
|
||||||
if (!s) return undefined;
|
|
||||||
return s
|
|
||||||
.split(",")
|
|
||||||
.map((t) => SongTypeMap[t.trim().toUpperCase()])
|
|
||||||
.filter((n) => n !== undefined);
|
|
||||||
}),
|
|
||||||
}).strict();
|
|
||||||
|
|
||||||
// Schema for +page.svelte useSearchParams (handles 'type' as an array of strings from URL)
|
|
||||||
export const SvelteSearchParamsSchema = BaseSearchParamsSchema.extend({
|
|
||||||
type: z.array(z.string()).default([]),
|
|
||||||
}).strict();
|
|
||||||
|
|
||||||
// Define the type for the Svelte form, which will have default values
|
|
||||||
export type SvelteSearchForm = z.infer<typeof SvelteSearchParamsSchema> & {
|
|
||||||
gpm: string; // To allow empty string in form input
|
|
||||||
gpx: string; // To allow empty string in form input
|
|
||||||
cat: string; // To allow empty string in form input
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user