+
+
);
}
diff --git a/src/app/suche/page.tsx b/src/app/suche/page.tsx
index a1cef24..fa09fb9 100644
--- a/src/app/suche/page.tsx
+++ b/src/app/suche/page.tsx
@@ -7,6 +7,7 @@ import { searchMovies, searchMulti, searchSeries, type TmdbSearchResult } from "
import { AppShell } from "@/components/AppShell";
import { Poster } from "@/components/Poster";
import { SearchField } from "@/components/SearchField";
+import { SearchPendingProvider, SearchResults } from "@/components/SearchPending";
export const metadata: Metadata = { title: "Suchen" };
@@ -54,27 +55,31 @@ export default async function SearchPage({
-
+
+
-
- Nichts dabei?{" "}
-
- Titel selbst anlegen
-
-
+
+ Nichts dabei?{" "}
+
+ Titel selbst anlegen
+
+
-
- {!env.tmdb.isConfigured ? (
-
- Ohne TMDB-Schlüssel kann Flimmer nicht suchen. Trage TMDB_API_KEY in der{" "}
- .env ein und starte die Instanz neu.
-
- ) : query.length < 2 ? (
- Gib mindestens zwei Zeichen ein, um zu suchen.
- ) : (
-
- )}
-
+
+
+ {!env.tmdb.isConfigured ? (
+
+ Ohne TMDB-Schlüssel kann Flimmer nicht suchen. Trage TMDB_API_KEY in
+ der .env ein und starte die Instanz neu.
+
+ ) : query.length < 2 ? (
+ Gib mindestens zwei Zeichen ein, um zu suchen.
+ ) : (
+
+ )}
+
+
+
);
}
diff --git a/src/components/SearchField.tsx b/src/components/SearchField.tsx
index fa01707..27e91b8 100644
--- a/src/components/SearchField.tsx
+++ b/src/components/SearchField.tsx
@@ -3,6 +3,7 @@
import { useEffect, useRef, useState, useTransition } from "react";
import { usePathname, useRouter } from "next/navigation";
import { clsx } from "clsx";
+import { SearchSpinner, useReportSearchPending } from "@/components/SearchPending";
/**
* Suchfeld mit Typfilter.
@@ -26,6 +27,11 @@ export function SearchField({
const [pending, startTransition] = useTransition();
const first = useRef(true);
+ const reportPending = useReportSearchPending();
+ useEffect(() => {
+ reportPending(pending);
+ }, [pending, reportPending]);
+
useEffect(() => {
// Beim ersten Rendern nichts tun — sonst ersetzt die Komponente die
// gerade geladene URL durch eine identische.
@@ -41,7 +47,7 @@ export function SearchField({
startTransition(() => {
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
});
- }, 350);
+ }, 250);
return () => clearTimeout(timer);
}, [value, filter, pathname, router]);
@@ -68,14 +74,7 @@ export function SearchField({
autoFocus
className="fl-input pl-10 text-base"
/>
- {pending && (
-
- sucht …
-
- )}
+
diff --git a/src/components/SearchPending.tsx b/src/components/SearchPending.tsx
new file mode 100644
index 0000000..5e88839
--- /dev/null
+++ b/src/components/SearchPending.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { createContext, useContext, useState, type ReactNode } from "react";
+import { clsx } from "clsx";
+
+/**
+ * Rückmeldung beim Tippen — geteilt von der Suche und der Bibliothek.
+ *
+ * Beide Filterleisten wissen über `useTransition()` längst, dass eine neue
+ * Antwort unterwegs ist. Die Trefferliste weiß es nicht: sie ist eine
+ * Server-Komponente und steht als `children` daneben. Diese schmale Klammer
+ * reicht das eine Bit vom Filter zur Liste durch.
+ *
+ * Bewusst zwei Kontexte: der Setter behält so über alle Renderdurchgänge
+ * dieselbe Identität und taugt als Abhängigkeit eines Effekts.
+ */
+
+const PendingContext = createContext(false);
+const SetPendingContext = createContext<(value: boolean) => void>(() => {});
+
+export function SearchPendingProvider({ children }: { children: ReactNode }) {
+ const [pending, setPending] = useState(false);
+ return (
+
+ {children}
+
+ );
+}
+
+/** Für die Filterleiste: meldet, ob gerade gesucht wird. */
+export function useReportSearchPending() {
+ return useContext(SetPendingContext);
+}
+
+/**
+ * Dämpft die Treffer, solange die neue Antwort unterwegs ist.
+ *
+ * Die alten Treffer bleiben stehen — sie verschwinden zu lassen wäre ein
+ * Rückschritt, weil die Liste dann bei jedem Tastendruck springt. Gedämpft
+ * und nicht anklickbar ist aber sichtbar „nicht mehr aktuell".
+ */
+export function SearchResults({ children }: { children: ReactNode }) {
+ const pending = useContext(PendingContext);
+ return (
+
+ {children}
+
+ );
+}
+
+/** Kleiner Hinweis neben dem Eingabefeld, solange gesucht wird. */
+export function SearchSpinner({ show }: { show: boolean }) {
+ if (!show) return null;
+ return (
+
+ sucht …
+
+ );
+}
diff --git a/src/components/library/LibraryFilters.tsx b/src/components/library/LibraryFilters.tsx
index 6d60ff7..48bb27a 100644
--- a/src/components/library/LibraryFilters.tsx
+++ b/src/components/library/LibraryFilters.tsx
@@ -5,6 +5,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { clsx } from "clsx";
import { STATUS_LABEL, STATUS_ORDER, type StatusKey } from "@/lib/labels";
import type { LibraryFacets } from "@/lib/queries";
+import { SearchSpinner, useReportSearchPending } from "@/components/SearchPending";
/**
* Filterleiste der Bibliothek.
@@ -26,11 +27,19 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
- const [, startTransition] = useTransition();
+ const [pending, startTransition] = useTransition();
const [query, setQuery] = useState(params.get("q") ?? "");
const first = useRef(true);
+ // Der Trefferliste mitteilen, dass eine neue Antwort unterwegs ist — ohne
+ // das Signal blieben die alten Treffer stumm stehen und es sah aus, als
+ // passiere nichts.
+ const reportPending = useReportSearchPending();
+ useEffect(() => {
+ reportPending(pending);
+ }, [pending, reportPending]);
+
function apply(changes: Record) {
const next = new URLSearchParams(params.toString());
for (const [key, value] of Object.entries(changes)) {
@@ -48,7 +57,7 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) {
first.current = false;
return;
}
- const timer = setTimeout(() => apply({ q: query.trim() || null }), 300);
+ const timer = setTimeout(() => apply({ q: query.trim() || null }), 250);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
@@ -62,14 +71,17 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) {
return (