songs page refactor schema

This commit is contained in:
2026-02-06 08:51:01 -08:00
parent 0531a1f5c0
commit 4b58d71b7c
3 changed files with 44 additions and 63 deletions

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { useSearchParams } from "runed/kit"; import { useSearchParams } from "runed/kit";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { z } from "zod";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { invalidate } from "$app/navigation"; import { invalidate } from "$app/navigation";
import SongEntry from "$lib/components/SongEntry.svelte"; import SongEntry from "$lib/components/SongEntry.svelte";
@@ -10,30 +9,9 @@
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";
// Zod schema for URL search parameters to ensure type safety and parsing const params = useSearchParams(SvelteSearchParamsSchema, {
const SongsSearchSchema = z
.object({
q: z.string().default(""), // song name
artist: z.string().default(""), // artist name
anime: z.string().default(""), // anime mainName
type: z.array(z.string()).default([]), // song type (e.g., "OP", "ED", "INS")
gpm: z
.string()
.optional()
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent min
gpx: z
.string()
.optional()
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent max
cat: z
.string()
.optional()
.transform((s) => (s ? parseInt(s, 10) : undefined)), // category
})
.strict();
const params = useSearchParams(SongsSearchSchema, {
pushHistory: false, pushHistory: false,
showDefaults: false, showDefaults: false,
}); });
@@ -41,7 +19,7 @@
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
// Local form values, bound to inputs, which will update params. // Local form values, bound to inputs, which will update params.
let form = $state({ let form: SvelteSearchForm = $state({
q: params.q, q: params.q,
artist: params.artist, artist: params.artist,
anime: params.anime, anime: params.anime,
@@ -274,7 +252,7 @@
<h2 class="mt-6 text-lg font-semibold">Songs</h2> <h2 class="mt-6 text-lg font-semibold">Songs</h2>
<ul class="mt-3 space-y-2"> <ul class="mt-3 space-y-2">
{#each data.songRows as r (String(r.annSongId) + "-" + String(r.animeAnnId) + "-" + String(r.type) + "-" + String(r.number))} {#each data.songRows as r (r.annSongId)}
<li> <li>
<SongEntry <SongEntry
annSongId={r.annSongId} annSongId={r.annSongId}

View File

@@ -1,47 +1,12 @@
import { z } from "zod";
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 {
SongCategoryMap,
SongTypeMap,
SongTypeReverseMap,
} from "$lib/utils/amq";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
import { LoadSearchParamsSchema } from "./schema";
const SearchSchema = z
.object({
q: z.string().optional(), // song name
artist: z.string().optional(), // artist name
anime: z.string().optional(), // anime mainName
type: z
.string()
.optional()
.transform((s) => {
if (!s) return undefined;
return s
.split(",")
.map((t) => SongTypeMap[t.trim().toUpperCase()])
.filter((n) => n !== undefined);
}),
gpm: z
.string()
.optional()
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent min
gpx: z
.string()
.optional()
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent max
cat: z
.string()
.optional()
.transform((s) => (s ? parseInt(s, 10) : undefined)), // category
})
.strict();
export const load: PageLoad = async ({ url, fetch, depends }) => { export const load: PageLoad = async ({ url, fetch, depends }) => {
depends("clientdb:songs"); depends("clientdb:songs");
const parsed = SearchSchema.safeParse( const parsed = LoadSearchParamsSchema.safeParse(
Object.fromEntries(url.searchParams.entries()), Object.fromEntries(url.searchParams.entries()),
); );

View File

@@ -0,0 +1,38 @@
import { z } from "zod";
import { SongTypeMap } from "$lib/utils/amq";
// Base schema for raw URL search parameters as strings
const BaseSearchParamsSchema = z.object({
q: z.string().optional(),
artist: z.string().optional(),
anime: z.string().optional(),
gpm: z.string().optional(),
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
};