125 lines
2.7 KiB
Svelte
125 lines
2.7 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import {
|
|
db,
|
|
ensureSeeded,
|
|
getAnimeList,
|
|
searchAnimeByName,
|
|
} from "$lib/db/client-db";
|
|
|
|
let status = $state<"idle" | "loading" | "ready" | "error">("idle");
|
|
let error = $state<string | null>(null);
|
|
|
|
let query = $state("");
|
|
let isSearching = $state(false);
|
|
|
|
type AnimeItem = Awaited<ReturnType<typeof getAnimeList>>[number];
|
|
let anime = $state<AnimeItem[]>([]);
|
|
|
|
async function loadInitial() {
|
|
status = "loading";
|
|
error = null;
|
|
|
|
try {
|
|
await ensureSeeded();
|
|
anime = await getAnimeList(db, 20);
|
|
status = "ready";
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : String(e);
|
|
status = "error";
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
void loadInitial();
|
|
});
|
|
|
|
// Debounced search
|
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function scheduleSearch(nextQuery: string) {
|
|
query = nextQuery;
|
|
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
|
|
debounceTimer = setTimeout(async () => {
|
|
if (status !== "ready") return;
|
|
|
|
const q = query.trim();
|
|
|
|
try {
|
|
isSearching = true;
|
|
|
|
if (!q) {
|
|
anime = await getAnimeList(db, 20);
|
|
} else {
|
|
anime = await searchAnimeByName(db, q, 20);
|
|
}
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : String(e);
|
|
status = "error";
|
|
} finally {
|
|
isSearching = false;
|
|
}
|
|
}, 200);
|
|
}
|
|
|
|
function seasonName(seasonId: number) {
|
|
switch (seasonId) {
|
|
case 0:
|
|
return "Winter";
|
|
case 1:
|
|
return "Spring";
|
|
case 2:
|
|
return "Summer";
|
|
case 3:
|
|
return "Fall";
|
|
default:
|
|
return `Season ${seasonId}`;
|
|
}
|
|
}
|
|
</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={query}
|
|
oninput={(e) =>
|
|
scheduleSearch((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">
|
|
<div class="font-medium">{a.mainName}</div>
|
|
<div class="text-sm text-muted-foreground">
|
|
{a.year}
|
|
{seasonName(a.seasonId)} • ANN {a.annId} • MAL {a.malId}
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|