- {#if loading}
+ {#if initialLoading}
Loading...
- {:else if error}
+ {:else if error && !project}
{error}
{:else if project}
@@ -119,6 +129,9 @@
+ {#if loading}
+
Loading...
+ {/if}
{
offset = Math.max(0, offset - limit);
- load();
}}
>
Previous
@@ -154,7 +166,6 @@
disabled={offset + limit >= total}
onclick={() => {
offset = offset + limit;
- load();
}}
>
Next
diff --git a/frontend/src/lib/pages/projects.svelte b/frontend/src/lib/pages/projects.svelte
index 8a43987..6694b84 100644
--- a/frontend/src/lib/pages/projects.svelte
+++ b/frontend/src/lib/pages/projects.svelte
@@ -13,6 +13,7 @@
import SearchIcon from "@lucide/svelte/icons/search";
import XIcon from "@lucide/svelte/icons/x";
import type { Project } from "$lib/types/api";
+ import { createLatestRequestGuard, runLatest } from "$lib/latest-request";
let { params = {} }: { params?: Record } = $props();
@@ -25,7 +26,7 @@
let searchTerm = $state("");
let debouncedSearch = $state("");
let searchTimeout: ReturnType | null = null;
- let requestVersion = 0;
+ const requestGuard = createLatestRequestGuard();
function handleSearchInput(value: string) {
searchTerm = value;
@@ -45,24 +46,25 @@
}
function load() {
- const version = ++requestVersion;
loading = true;
error = "";
- listProjects(limit, offset, debouncedSearch || undefined)
- .then((res) => {
- if (version !== requestVersion) return;
- projects = res.items;
- total = res.total;
- enrichRecentFromProjects(res.items);
- })
- .catch((e) => {
- if (version !== requestVersion) return;
- error = e.message || "Failed to load projects";
- })
- .finally(() => {
- if (version !== requestVersion) return;
- loading = false;
- });
+ runLatest(
+ requestGuard,
+ () => listProjects(limit, offset, debouncedSearch || undefined),
+ {
+ onSuccess: (res) => {
+ projects = res.items;
+ total = res.total;
+ enrichRecentFromProjects(res.items);
+ },
+ onError: (e) => {
+ error = (e as Error).message || "Failed to load projects";
+ },
+ onFinally: () => {
+ loading = false;
+ },
+ },
+ );
}
$effect(load);