list: prototype with combined list field

This commit is contained in:
2026-02-12 23:43:49 -08:00
parent f90cf66cc1
commit 21d62f8c6f
5 changed files with 98 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import z from "zod";
export const MAL_URL = "https://myanimelist.net";
export const ANILIST_URL = "https://anilist.co";
export const KITSU_URL = "https://kitsu.io";
export const AnimeListKind = z.enum([
"mal",
"anilist",
"kitsu",
])
export const AnimeListWatchStatus = z.enum({
"completed": "c",
"watching": "w",
"plan_to_watch": "p",
"on_hold": "h",
"dropped": "d",
} as const)
export const AnimeList = z.object({
kind: AnimeListKind,
username: z.string(),
status: z.array(AnimeListWatchStatus).default([]),
});
export function listExternalUrl(list: z.infer<typeof AnimeList>) {
switch (list.kind) {
case "mal":
return `${MAL_URL}/profile/${encodeURIComponent(list.username)}`;
case "anilist":
return `${ANILIST_URL}/user/${encodeURIComponent(list.username)}`;
case "kitsu":
return `${KITSU_URL}/username/${encodeURIComponent(list.username)}`;
}
}

View File

@@ -29,6 +29,7 @@
<nav class="flex items-center gap-2 text-sm"> <nav class="flex items-center gap-2 text-sm">
<a href={resolve("/")}>Anime</a> <a href={resolve("/")}>Anime</a>
<a href={resolve("/songs")}>Songs</a> <a href={resolve("/songs")}>Songs</a>
<a href={resolve("/list")}>List</a>
<a href={resolve("/mal")}>MAL</a> <a href={resolve("/mal")}>MAL</a>
</nav> </nav>
</div> </div>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { useSearchParams } from "runed/kit";
import type { PageData } from "./$types";
import { SearchParamsSchema } from "./schema";
import { ChipGroup } from "$lib/components/ui/chip-group";
import { AnimeListWatchStatus } from "$lib/utils/list";
let { data }: { data: PageData } = $props();
const params = useSearchParams(SearchParamsSchema, {
pushHistory: false,
});
</script>
<h1 class="text-2xl font-semibold">List Search WIP</h1>
<form>
<label for="list-kind">Kind</label>
<!-- nested stuff won't work with useSearchParams -->
<select id="list-kind" bind:value={params.list.kind}>
<option value="mal">MAL</option>
<option value="anilist">AniList</option>
<option value="kitsu">Kitsu</option>
</select>
<label for="list-username">Username</label>
<input id="list-username" bind:value={params.list.username} />
<ChipGroup
label="Status"
items={AnimeListWatchStatus.options.map((v) => ({
label: v.toUpperCase(),
value: v,
}))}
bind:value={params.list.status}
/>
</form>

7
src/routes/list/+page.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
import { SearchParamsSchema } from "./schema";
export const load: PageLoad = async ({ url, fetch, depends }) => {
const parsed = SearchParamsSchema.safeParse(url.searchParams);
console.log(parsed);
}

19
src/routes/list/schema.ts Normal file
View File

@@ -0,0 +1,19 @@
import { AnimeList, AnimeListWatchStatus } from "$lib/utils/list";
import { z } from "zod";
const fieldSep = ":";
const valueSep = ",";
const listCodec = z.codec(z.string(), AnimeList, {
decode: (s) => {
const [kind, ...rest] = decodeURIComponent(s).split(fieldSep);
const status = rest.pop()?.split(valueSep).map((v) => AnimeListWatchStatus.parse(v)) ?? [];
const username = rest.join("");
return AnimeList.parse({ kind, username, status });
},
encode: (list) => encodeURIComponent(`${list.kind}${fieldSep}${list.username}${fieldSep}${list.status?.join(valueSep)}`),
});
export const SearchParamsSchema = z.object({
list: listCodec.default({ kind: "mal", username: "", status: [] }),
})