feat: loader discret dans la barre de recherche pendant la navigation

Utilise useTransition pour wrapper les router.replace dans LiveSearchForm.
Affiche un petit spinner à droite du champ de recherche pendant que les
résultats se chargent (books, series, authors).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 18:06:12 +02:00
parent 3998d65694
commit 69de2ae237

View File

@@ -1,6 +1,6 @@
"use client";
import { useRef, useCallback, useEffect } from "react";
import { useRef, useCallback, useEffect, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "../../lib/i18n/context";
import { Icon } from "./ui";
@@ -58,6 +58,7 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const [isPending, startTransition] = useTransition();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const formRef = useRef<HTMLFormElement>(null);
@@ -96,11 +97,11 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
if (timerRef.current) clearTimeout(timerRef.current);
if (immediate) {
saveFilters();
router.replace(buildUrl() as any);
startTransition(() => { router.replace(buildUrl() as any); });
} else {
timerRef.current = setTimeout(() => {
saveFilters();
router.replace(buildUrl() as any);
startTransition(() => { router.replace(buildUrl() as any); });
}, debounceMs);
}
}, [router, buildUrl, debounceMs, saveFilters]);
@@ -131,7 +132,7 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
e.preventDefault();
if (timerRef.current) clearTimeout(timerRef.current);
saveFilters();
router.replace(buildUrl() as any);
startTransition(() => { router.replace(buildUrl() as any); });
}}
className="space-y-4"
>
@@ -139,6 +140,9 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
{textFields.map((field) => (
<div key={field.name} className="relative">
<Icon name="search" size="md" className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
{isPending && (
<Icon name="spinner" size="sm" className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground animate-spin pointer-events-none" />
)}
<input
name={field.name}
type="text"
@@ -187,7 +191,7 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
onClick={() => {
formRef.current?.reset();
try { deleteCookie(cookieName); } catch {}
router.replace(basePath as any);
startTransition(() => { router.replace(basePath as any); });
}}
className="
inline-flex items-center gap-1