success pt. 5 search box
This commit is contained in:
@@ -1,28 +1,68 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { db, ensureSeeded, getAnimeList } from "$lib/db/client-db";
|
import {
|
||||||
|
db,
|
||||||
|
ensureSeeded,
|
||||||
|
getAnimeList,
|
||||||
|
searchAnimeByName,
|
||||||
|
} from "$lib/db/client-db";
|
||||||
|
|
||||||
let status = $state<"idle" | "loading" | "ready" | "error">("idle");
|
let status = $state<"idle" | "loading" | "ready" | "error">("idle");
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
|
let query = $state("");
|
||||||
|
let isSearching = $state(false);
|
||||||
|
|
||||||
type AnimeItem = Awaited<ReturnType<typeof getAnimeList>>[number];
|
type AnimeItem = Awaited<ReturnType<typeof getAnimeList>>[number];
|
||||||
let anime = $state<AnimeItem[]>([]);
|
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(() => {
|
onMount(() => {
|
||||||
(async () => {
|
void loadInitial();
|
||||||
status = "loading";
|
});
|
||||||
error = null;
|
|
||||||
|
// 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 {
|
try {
|
||||||
await ensureSeeded();
|
isSearching = true;
|
||||||
anime = await getAnimeList(db, 20);
|
|
||||||
status = "ready";
|
if (!q) {
|
||||||
|
anime = await getAnimeList(db, 20);
|
||||||
|
} else {
|
||||||
|
anime = await searchAnimeByName(db, q, 20);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : String(e);
|
error = e instanceof Error ? e.message : String(e);
|
||||||
status = "error";
|
status = "error";
|
||||||
|
} finally {
|
||||||
|
isSearching = false;
|
||||||
}
|
}
|
||||||
})();
|
}, 200);
|
||||||
});
|
}
|
||||||
|
|
||||||
function seasonName(seasonId: number) {
|
function seasonName(seasonId: number) {
|
||||||
switch (seasonId) {
|
switch (seasonId) {
|
||||||
@@ -47,7 +87,28 @@
|
|||||||
{:else if status === "error"}
|
{:else if status === "error"}
|
||||||
<p class="mt-3 text-sm text-red-600">Error: {error}</p>
|
<p class="mt-3 text-sm text-red-600">Error: {error}</p>
|
||||||
{:else if status === "ready"}
|
{:else if status === "ready"}
|
||||||
<p class="mt-3 text-sm text-muted-foreground">Showing {anime.length} anime</p>
|
<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">
|
<ul class="mt-4 space-y-2">
|
||||||
{#each anime as a (a.annId)}
|
{#each anime as a (a.annId)}
|
||||||
|
|||||||
Reference in New Issue
Block a user