commit message
{"subject":"fix(search): prevent empty-state flash before TMDB fallback renders","body":"- Add isResolving to useCanRequest and gate empty states on it across GlobalSearch and Catalog\n- Debounce TMDB query in Catalog and hide ItemGrid when the request section may rescue an empty library\n- Track per-card submit state in RequestToAddSection grid so concurrent requests don't trample each other\n- Suppress anonymous TMDB request-search fetches to avoid cross-viewer cache leakage"}
This commit is contained in:
@@ -1,12 +1,21 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Film, Tv } from "lucide-react";
|
||||
import { Film, Sparkles, Tv } from "lucide-react";
|
||||
import { useCanRequest } from "@/hooks/useCanRequest";
|
||||
import { useRequestSearch } from "@/hooks/queries/useRequests";
|
||||
import { useCreateMediaRequest, useRequestSearch } from "@/hooks/queries/useRequests";
|
||||
import type { RequestMediaResult } from "@/api/types";
|
||||
import { formatRequestReason, tmdbImageURL } from "@/lib/mediaRequests";
|
||||
import {
|
||||
formatRequestReason,
|
||||
requestInputFromMediaResult,
|
||||
tmdbImageURL,
|
||||
} from "@/lib/mediaRequests";
|
||||
import { cn } from "@/lib/utils";
|
||||
import RequestPosterCard from "./RequestPosterCard";
|
||||
|
||||
function cardKey(item: Pick<RequestMediaResult, "media_type" | "tmdb_id">): string {
|
||||
return `${item.media_type}-${item.tmdb_id}`;
|
||||
}
|
||||
|
||||
const DIALOG_LIMIT = 4;
|
||||
const GRID_LIMIT = 20;
|
||||
|
||||
@@ -135,24 +144,76 @@ function GridVariant({
|
||||
items: RequestMediaResult[];
|
||||
libraryHadHits: boolean;
|
||||
}) {
|
||||
const count = items.length;
|
||||
const createRequest = useCreateMediaRequest();
|
||||
// Track each in-flight card key independently; the shared `useMutation`
|
||||
// observer overwrites its `variables` on every `mutate` call, so rapid
|
||||
// clicks on different cards would otherwise trample each other's spinner.
|
||||
const [pendingKeys, setPendingKeys] = useState<ReadonlySet<string>>(new Set());
|
||||
const submitCard = (item: RequestMediaResult) => {
|
||||
const key = cardKey(item);
|
||||
setPendingKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(key);
|
||||
return next;
|
||||
});
|
||||
createRequest.mutate(requestInputFromMediaResult(item), {
|
||||
onSettled: () => {
|
||||
setPendingKeys((prev) => {
|
||||
if (!prev.has(key)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(key);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-3 text-amber-300/85">
|
||||
<div className="h-px flex-1 bg-amber-400/20" />
|
||||
<h2 className="text-[11px] font-semibold tracking-[0.12em] uppercase">
|
||||
{libraryHadHits ? "Request to Add" : "Not in your library, but you can request"}
|
||||
</h2>
|
||||
<div className="h-px flex-1 bg-amber-400/20" />
|
||||
</div>
|
||||
<section
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-[28px] border border-amber-400/[0.14]",
|
||||
"bg-[radial-gradient(120%_60%_at_50%_0%,rgba(245,158,11,0.07)_0%,rgba(245,158,11,0.015)_45%,transparent_75%)]",
|
||||
"shadow-[inset_0_1px_0_0_rgba(255,255,255,0.04),0_28px_60px_-44px_rgba(0,0,0,0.7)]",
|
||||
"px-4 pt-7 pb-7 sm:px-7 sm:pt-8",
|
||||
libraryHadHits && "mt-12! sm:mt-16!",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-16 top-0 h-px bg-gradient-to-r from-transparent via-amber-300/45 to-transparent"
|
||||
/>
|
||||
|
||||
<header className="mb-7 flex flex-wrap items-end justify-between gap-x-5 gap-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-amber-200/85">
|
||||
<Sparkles className="h-3.5 w-3.5" strokeWidth={2.2} aria-hidden />
|
||||
<span className="text-[10px] font-semibold tracking-[0.24em] uppercase">
|
||||
{libraryHadHits ? "Discover · Outside your library" : "Outside your library"}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-display text-foreground text-[clamp(1.25rem,1.6vw,1.55rem)] leading-tight font-semibold tracking-tight">
|
||||
{libraryHadHits ? "Request to Add" : "Not in your library, but you can request"}
|
||||
</h2>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-1.5 self-end rounded-full border border-amber-400/15 bg-amber-400/[0.06] px-2.5 py-1 text-[11px] font-medium tracking-wide text-amber-100/75 tabular-nums">
|
||||
{count} {count === 1 ? "result" : "results"}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8">
|
||||
{items.map((item) => (
|
||||
<RequestPosterCard
|
||||
key={`${item.media_type}-${item.tmdb_id}`}
|
||||
variant="discover"
|
||||
item={item}
|
||||
fluid
|
||||
/>
|
||||
))}
|
||||
{items.map((item) => {
|
||||
const key = cardKey(item);
|
||||
return (
|
||||
<RequestPosterCard
|
||||
key={key}
|
||||
variant="discover"
|
||||
item={item}
|
||||
isSubmitting={pendingKeys.has(key)}
|
||||
onRequest={() => submitCard(item)}
|
||||
fluid
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user