Files
amqtrain/src/routes/+page.svelte

132 lines
3.1 KiB
Svelte

<script lang="ts">
import { Debounced } from "runed";
import { useSearchParams } from "runed/kit";
import { onMount } from "svelte";
import {
ensureSeeded,
getAnimeList,
getClientDb,
searchAnimeByName,
} from "$lib/db/client-db";
import { AmqBrowseSearchSchema } from "$lib/types/search/amq-browse";
import { seasonName } from "$lib/utils/amq";
const params = useSearchParams(AmqBrowseSearchSchema, {
debounce: 250,
pushHistory: false,
showDefaults: false,
});
let status = $state<"idle" | "loading" | "ready" | "error">("idle");
let error = $state<string | null>(null);
let isSearching = $state(false);
// Debounce the actual DB query updates (separate from URL debounce)
const debouncedQuery = new Debounced(() => params.q, 250);
type AnimeItem = Awaited<ReturnType<typeof getAnimeList>>[number];
let anime = $state<AnimeItem[]>([]);
async function loadListFor(query: string) {
const q = query.trim();
try {
isSearching = true;
const { db } = await getClientDb();
if (!q) {
anime = await getAnimeList(db, 20);
} else {
anime = await searchAnimeByName(db, q, 20);
}
} finally {
isSearching = false;
}
}
function animeHref(annId: number) {
return `/anime/${annId}`;
}
async function loadInitial() {
status = "loading";
error = null;
try {
await ensureSeeded();
await loadListFor(params.q);
status = "ready";
} catch (e) {
error = e instanceof Error ? e.message : String(e);
status = "error";
}
}
onMount(() => {
void loadInitial();
});
// React to debounced query changes (URL updates + user typing)
$effect(() => {
if (status !== "ready") return;
// Track debounced query to avoid hammering the DB while typing
const q = debouncedQuery.current;
void (async () => {
try {
await loadListFor(q);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
status = "error";
}
})();
});
</script>
<h1 class="text-2xl font-semibold">AMQ Browser</h1>
{#if status === "loading"}
<p class="mt-3 text-sm text-muted-foreground">Loading client database…</p>
{:else if status === "error"}
<p class="mt-3 text-sm text-red-600">Error: {error}</p>
{:else if status === "ready"}
<div class="mt-3 flex flex-col gap-2">
<label class="text-sm text-muted-foreground" for="anime-search">
Search anime
</label>
<input
id="anime-search"
class="rounded border px-3 py-2 text-sm"
placeholder="Type to search by name…"
value={params.q}
oninput={(e) => (params.q = (e.currentTarget as HTMLInputElement).value)}
autocomplete="off"
spellcheck={false}
/>
<p class="text-sm text-muted-foreground">
{#if isSearching}
Searching…
{:else}
Showing {anime.length} anime
{/if}
</p>
</div>
<ul class="mt-4 space-y-2">
{#each anime as a (a.annId)}
<li class="rounded border px-3 py-2">
<a class="font-medium hover:underline" href={animeHref(a.annId)}>
{a.mainName}
</a>
<div class="text-sm text-muted-foreground">
{a.year}
{seasonName(a.seasonId)} • ANN {a.annId} • MAL {a.malId}
</div>
</li>
{/each}
</ul>
{/if}