67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
import { type ReactNode } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { qk } from "../lib/queryKeys";
|
|
import { api, type PlexCard } from "../lib/api";
|
|
import { useDebounced } from "../lib/useDebounced";
|
|
import { LoadingState } from "./ui/QueryState";
|
|
|
|
// A debounced library search-and-pick list, shared by the "Add title" panel (PlexCollectionView) and
|
|
// the "New collection" first-title panel (plexBrowseLists) — both search the library the same way and
|
|
// render the same result rows, differing only in the per-row action. Callers keep their own chrome
|
|
// (search input, name field, close button) and supply `term` + `renderAction`; this owns the query,
|
|
// the loading/error/empty states, and the row markup so those can't drift between the two surfaces.
|
|
export default function PlexLibraryPicker({
|
|
scope,
|
|
term,
|
|
enabled = true,
|
|
emptyHint = null,
|
|
renderAction,
|
|
}: {
|
|
scope: string;
|
|
/** The raw search term; debounced internally. */
|
|
term: string;
|
|
/** Gate the query off (e.g. until a collection name is valid). When false, renders nothing. */
|
|
enabled?: boolean;
|
|
/** Shown while the (debounced) term is empty — a hint, or nothing. */
|
|
emptyHint?: ReactNode;
|
|
/** The trailing control for each result row (Add / Create-with / …). */
|
|
renderAction: (card: PlexCard) => ReactNode;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const debounced = useDebounced(term.trim(), 300);
|
|
const q = useQuery({
|
|
queryKey: qk.plexLibrary("collection-pick", scope, debounced),
|
|
queryFn: () => api.plexLibrary({ scope, q: debounced, limit: 12 }),
|
|
enabled: enabled && debounced.length > 0,
|
|
});
|
|
const results = q.data?.items ?? [];
|
|
|
|
if (!enabled) return null;
|
|
if (debounced.length === 0) return <>{emptyHint}</>;
|
|
if (q.isLoading) return <LoadingState shape="list" label={t("plex.loading")} />;
|
|
if (q.isError) return <p className="text-muted text-sm">{t("common.loadError")}</p>;
|
|
if (results.length === 0)
|
|
return <p className="text-muted text-sm">{t("plex.collection.addNoResults")}</p>;
|
|
|
|
return (
|
|
<ul className="flex flex-col gap-1">
|
|
{results.map((c) => (
|
|
<li key={c.id} className="flex items-center gap-3">
|
|
<img
|
|
src={c.thumb}
|
|
alt=""
|
|
className="h-12 w-8 shrink-0 rounded object-cover"
|
|
loading="lazy"
|
|
/>
|
|
<span className="min-w-0 flex-1 truncate text-sm">
|
|
{c.title}
|
|
{c.year != null && <span className="text-muted"> · {c.year}</span>}
|
|
</span>
|
|
{renderAction(c)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|