list page pt. 4 better search
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Debounced } from "runed";
|
|
||||||
import { useSearchParams } from "runed/kit";
|
import { useSearchParams } from "runed/kit";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -23,19 +22,23 @@
|
|||||||
|
|
||||||
type PageStatus = "idle" | "loading" | "ready" | "error";
|
type PageStatus = "idle" | "loading" | "ready" | "error";
|
||||||
|
|
||||||
|
// This is the URL-backed state. `mal` only updates when you press Search.
|
||||||
|
// `status` can auto-update and re-run queries without pressing Search again.
|
||||||
const params = useSearchParams(ListSearchSchema, {
|
const params = useSearchParams(ListSearchSchema, {
|
||||||
pushHistory: false,
|
pushHistory: false,
|
||||||
showDefaults: false,
|
showDefaults: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Local form state (username does NOT update the URL on change)
|
||||||
|
let formMal = $state<string>("");
|
||||||
|
|
||||||
let status = $state<PageStatus>("idle");
|
let status = $state<PageStatus>("idle");
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
let isLoadingMal = $state(false);
|
let isLoadingMal = $state(false);
|
||||||
let isLoadingDb = $state(false);
|
let isLoadingDb = $state(false);
|
||||||
|
|
||||||
// Keep MAL calls snappy if you later add more query params; for now it's mostly future-proofing.
|
let isSearching = $state(false);
|
||||||
const debouncedMalUser = new Debounced(() => params.mal ?? "", 1000);
|
|
||||||
|
|
||||||
type MalListResponse = z.infer<typeof MalAnimeListResponseSchema>;
|
type MalListResponse = z.infer<typeof MalAnimeListResponseSchema>;
|
||||||
type MalEntry = MalListResponse["data"][number];
|
type MalEntry = MalListResponse["data"][number];
|
||||||
@@ -47,6 +50,35 @@
|
|||||||
type SongRow = Awaited<ReturnType<typeof getSongsForMalAnimeIds>>[number];
|
type SongRow = Awaited<ReturnType<typeof getSongsForMalAnimeIds>>[number];
|
||||||
let songRows = $state<SongRow[]>([]);
|
let songRows = $state<SongRow[]>([]);
|
||||||
|
|
||||||
|
function clearResults() {
|
||||||
|
malResponse = null;
|
||||||
|
malEntries = [];
|
||||||
|
malUsername = null;
|
||||||
|
songRows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSearchFor(username: string) {
|
||||||
|
const u = username.trim();
|
||||||
|
if (!u) {
|
||||||
|
clearResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching = true;
|
||||||
|
try {
|
||||||
|
await loadAllFor(u);
|
||||||
|
} finally {
|
||||||
|
isSearching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
// Sync username -> URL only on Search
|
||||||
|
params.mal = formMal;
|
||||||
|
|
||||||
|
await runSearchFor(formMal);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch MAL animelist (limited to LIST_QUERY_LIMIT for now).
|
* Fetch MAL animelist (limited to LIST_QUERY_LIMIT for now).
|
||||||
* Uses zod schema parsing to avoid redefining types.
|
* Uses zod schema parsing to avoid redefining types.
|
||||||
@@ -86,10 +118,7 @@
|
|||||||
async function loadAllFor(username: string | undefined) {
|
async function loadAllFor(username: string | undefined) {
|
||||||
const u = (username ?? "").trim();
|
const u = (username ?? "").trim();
|
||||||
if (!u) {
|
if (!u) {
|
||||||
malResponse = null;
|
clearResults();
|
||||||
malEntries = [];
|
|
||||||
malUsername = null;
|
|
||||||
songRows = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +153,12 @@
|
|||||||
error = null;
|
error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadAllFor(params.mal);
|
// Initialize local form username from URL once on load
|
||||||
|
formMal = params.mal ?? "";
|
||||||
|
|
||||||
|
// If the URL already has a username, load immediately (no URL writes)
|
||||||
|
await loadAllFor(formMal);
|
||||||
|
|
||||||
status = "ready";
|
status = "ready";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : String(e);
|
error = e instanceof Error ? e.message : String(e);
|
||||||
@@ -136,14 +170,14 @@
|
|||||||
void loadInitial();
|
void loadInitial();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto re-run the search whenever status changes *as long as we have a loaded username*.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (status !== "ready") return;
|
if (status !== "ready") return;
|
||||||
|
if (!malUsername) return;
|
||||||
const u = debouncedMalUser.current;
|
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await loadAllFor(u);
|
await runSearchFor(malUsername);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : String(e);
|
error = e instanceof Error ? e.message : String(e);
|
||||||
status = "error";
|
status = "error";
|
||||||
@@ -174,8 +208,14 @@
|
|||||||
{: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"}
|
||||||
<form class="mt-4 flex flex-col gap-2">
|
<form
|
||||||
<div class="flex gap-2">
|
class="mt-4 flex flex-col gap-2 center"
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void onSearch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-sm text-muted-foreground" for="mal-user"
|
<label class="text-sm text-muted-foreground" for="mal-user"
|
||||||
>MAL username</label
|
>MAL username</label
|
||||||
@@ -184,9 +224,9 @@
|
|||||||
id="mal-user"
|
id="mal-user"
|
||||||
class="rounded border px-3 py-2 text-sm"
|
class="rounded border px-3 py-2 text-sm"
|
||||||
placeholder="e.g. CaZzzer"
|
placeholder="e.g. CaZzzer"
|
||||||
value={params.mal ?? ""}
|
value={formMal}
|
||||||
oninput={(e) =>
|
oninput={(e) =>
|
||||||
(params.mal = (e.currentTarget as HTMLInputElement).value)}
|
(formMal = (e.currentTarget as HTMLInputElement).value)}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
/>
|
/>
|
||||||
@@ -199,10 +239,7 @@
|
|||||||
<select
|
<select
|
||||||
id="mal-status"
|
id="mal-status"
|
||||||
class="rounded border px-3 py-2 text-sm"
|
class="rounded border px-3 py-2 text-sm"
|
||||||
value={params.status}
|
bind:value={params.status}
|
||||||
onchange={(e) =>
|
|
||||||
(params.status = (e.currentTarget as HTMLSelectElement)
|
|
||||||
.value as typeof params.status)}
|
|
||||||
>
|
>
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
<option value="watching">Watching</option>
|
<option value="watching">Watching</option>
|
||||||
@@ -214,10 +251,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-muted-foreground">
|
<div class="flex flex-col gap-2">
|
||||||
{#if !(params.mal ?? "").trim()}
|
<button
|
||||||
Waiting for username…
|
type="submit"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
disabled={isSearching ||
|
||||||
|
isLoadingMal ||
|
||||||
|
isLoadingDb ||
|
||||||
|
!(formMal ?? "").trim()}
|
||||||
|
>
|
||||||
|
{#if isSearching || isLoadingMal || isLoadingDb}
|
||||||
|
Searching…
|
||||||
{:else}
|
{:else}
|
||||||
|
Search
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
{#if !(malUsername ?? "").trim()}{:else}
|
||||||
{#if isLoadingMal}
|
{#if isLoadingMal}
|
||||||
Fetching MAL list…
|
Fetching MAL list…
|
||||||
{:else}
|
{:else}
|
||||||
@@ -231,6 +282,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if malUsername}
|
{#if malUsername}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
@@ -246,16 +298,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if (params.mal ?? "").trim() && !isLoadingMal && malEntries.length === 0}
|
{#if (formMal ?? "").trim() && !isLoadingMal && malEntries.length === 0}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">
|
<p class="mt-4 text-sm text-muted-foreground">
|
||||||
No anime returned from MAL (did you set a restrictive status/sort?).
|
No anime returned from MAL (did you set a restrictive status/sort?).
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (params.mal ?? "").trim() && !isLoadingDb && malEntries.length > 0 && songRows.length === 0}
|
{#if (formMal ?? "").trim() && !isLoadingDb && malEntries.length > 0 && songRows.length === 0}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">
|
<p class="mt-4 text-sm text-muted-foreground">
|
||||||
No songs matched in the local database. This likely means none of the MAL
|
No songs matched in the local database. This likely means none of the MAL
|
||||||
anime IDs exist in the snapshot DB.
|
anime IDs exist in the AMQ DB.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user