prototype anime list input

This commit is contained in:
2026-02-13 01:05:09 -08:00
parent a144baba2b
commit b37eef8f31
6 changed files with 85 additions and 41 deletions

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { AnimeListCodec } from "./schema";
import { ChipGroup } from "$lib/components/ui/chip-group";
import { AnimeListWatchStatus } from "$lib/utils/list";
import NativeSelect from "$lib/components/ui/native-select/native-select.svelte";
import NativeSelectOption from "$lib/components/ui/native-select/native-select-option.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import { Label } from "$lib/components/ui/label";
import { z } from "zod";
let { value = $bindable() }: { value: z.infer<typeof AnimeListCodec> } =
$props();
</script>
<div class="flex flex-col gap-2">
<Label for="list-kind">Kind</Label>
<NativeSelect id="list-kind" bind:value={value.kind}>
<NativeSelectOption value="mal">MAL</NativeSelectOption>
<NativeSelectOption value="anilist">AniList</NativeSelectOption>
<NativeSelectOption value="kitsu">Kitsu</NativeSelectOption>
</NativeSelect>
</div>
<div class="flex flex-col gap-2">
<Label for="list-username">Username</Label>
<Input id="list-username" bind:value={value.username} />
</div>
<div class="flex flex-col gap-2">
<Label for="list-status">Status</Label>
<ChipGroup
items={AnimeListWatchStatus.options.map((v) => ({
label: v.toUpperCase(),
value: v,
}))}
bind:value={value.status}
/>
</div>

View File

@@ -0,0 +1,2 @@
export { default as AnimeListInput } from "./AnimeListInput.svelte";
export * from "./schema";

View File

@@ -0,0 +1,16 @@
import { AnimeList, AnimeListWatchStatus } from "$lib/utils/list";
import { z } from "zod";
const SEP_FIELD = ":";
const SEP_VALUE = ",";
export const AnimeListCodec = z.codec(z.string(), AnimeList, {
decode: (s) => {
const [kind, ...rest] = decodeURIComponent(s).split(SEP_FIELD);
const statusStr = rest.pop();
const status = statusStr ? statusStr.split(SEP_VALUE).map((v) => AnimeListWatchStatus.parse(v)) : [];
const username = rest.join("");
return AnimeList.parse({ kind, username, status });
},
encode: (list) => encodeURIComponent(`${list.kind}${SEP_FIELD}${list.username}${SEP_FIELD}${list.status.join(SEP_VALUE)}`),
});

View File

@@ -21,7 +21,7 @@ export const AnimeListWatchStatus = z.enum({
export const AnimeList = z.object({
kind: AnimeListKind,
username: z.string(),
status: z.array(AnimeListWatchStatus).default([]),
status: z.array(AnimeListWatchStatus),
});
export function listExternalUrl(list: z.infer<typeof AnimeList>) {

View File

@@ -2,46 +2,43 @@
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";
import NativeSelect from "$lib/components/ui/native-select/native-select.svelte";
import NativeSelectOption from "$lib/components/ui/native-select/native-select-option.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import { z } from "zod";
import {
AnimeListCodec,
AnimeListInput,
} from "$lib/components/inputs/anime-list-input";
let { data }: { data: PageData } = $props();
const params = useSearchParams(SearchParamsSchema, {
showDefaults: true,
updateURL: false,
pushHistory: false,
});
let formState: z.infer<typeof AnimeListCodec> = $state({
kind: "mal",
username: "",
status: [],
});
// $inspect(formState);
$effect(() => {
console.log("formState", formState);
});
</script>
<h1 class="text-2xl font-semibold">List Search WIP</h1>
<form class="flex flex-wrap gap-2">
<div class="flex flex-col gap-2">
<Label for="list-kind">Kind</Label>
<NativeSelect id="list-kind" bind:value={params.kind}>
<NativeSelectOption value="mal">MAL</NativeSelectOption>
<NativeSelectOption value="anilist">AniList</NativeSelectOption>
<NativeSelectOption value="kitsu">Kitsu</NativeSelectOption>
</NativeSelect>
</div>
<div class="flex flex-col gap-2">
<Label for="list-username">Username</Label>
<Input id="list-username" bind:value={params.username} />
</div>
<div class="flex flex-col gap-2">
<Label for="list-status">Status</Label>
<ChipGroup
items={AnimeListWatchStatus.options.map((v) => ({
label: v.toUpperCase(),
value: v,
}))}
bind:value={params.status}
/>
</div>
<form
onsubmit={(e) => {
e.preventDefault();
params.kind = formState.kind;
params.username = formState.username;
params.status = formState.status;
}}
class="flex flex-wrap items-end gap-2"
>
<AnimeListInput bind:value={formState} />
<Button type="submit">Search</Button>
</form>

View File

@@ -1,15 +1,8 @@
import { AnimeListKind, AnimeListWatchStatus } from "$lib/utils/list";
import { z } from "zod";
const valueSep = ".";
const statusCodec = z.codec(z.string(), z.array(AnimeListWatchStatus), {
decode: (s) => s ? decodeURIComponent(s).split(valueSep).map((s) => AnimeListWatchStatus.parse(s)) : [],
encode: (v) => v.length ? encodeURIComponent(v.join(valueSep)) : "",
})
export const SearchParamsSchema = z.object({
kind: AnimeListKind.default("mal"),
username: z.string().optional().default(""),
status: statusCodec.default([]),
username: z.string().default(""),
status: z.array(AnimeListWatchStatus).default([]),
})