mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
196 lines
7.8 KiB
React
196 lines
7.8 KiB
React
import { useEffect, useState, useCallback } from "react";
|
|
import { Search, CheckCircle2 } from "lucide-react";
|
|
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
|
|
import { useAssets } from "@/contexts/AdminAssetsContext";
|
|
|
|
function EmptyState({ fileType }) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-48 gap-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
No {fileType} assets found.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AssetCard({ asset, selected, onSelect }) {
|
|
const thumb = asset.thumbnail_url ?? asset.file_url;
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => onSelect(asset)}
|
|
className={[
|
|
"relative rounded-lg border-2 overflow-hidden transition-all text-left w-full",
|
|
"hover:border-primary/60 hover:shadow-sm",
|
|
selected
|
|
? "border-primary ring-2 ring-primary/20"
|
|
: "border-border",
|
|
].join(" ")}
|
|
>
|
|
<div className="aspect-video bg-muted w-full overflow-hidden">
|
|
{thumb ? (
|
|
<img
|
|
src={thumb}
|
|
alt={asset.display_name}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<span className="text-xs text-muted-foreground">No preview</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="p-2">
|
|
<p className="text-xs font-medium truncate">{asset.display_name}</p>
|
|
</div>
|
|
{selected && (
|
|
<div className="absolute top-1.5 right-1.5">
|
|
<CheckCircle2 className="h-5 w-5 text-primary fill-white" />
|
|
</div>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) {
|
|
const { fetchAssets, assets, pagination, loading } = useAssets();
|
|
|
|
const [search, setSearch] = useState("");
|
|
const [committed, setCommitted] = useState(""); // ← only updates on search trigger
|
|
const [page, setPage] = useState(1);
|
|
const [selected, setSelected] = useState(null);
|
|
|
|
const LIMIT = 12;
|
|
|
|
const triggerSearch = useCallback(() => {
|
|
setCommitted(search);
|
|
setPage(1);
|
|
}, [search]);
|
|
|
|
// ── Fetch only when committed search, page, or open changes ──────────────
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
fetchAssets({
|
|
page,
|
|
limit: LIMIT,
|
|
filters: [
|
|
...(fileType ? [{ id: "file_type", value: [fileType] }] : []),
|
|
...(committed ? [{ id: "display_name", value: [committed] }] : []),
|
|
],
|
|
});
|
|
}, [open, page, committed, fileType]);
|
|
|
|
// ── Reset on close ────────────────────────────────────────────────────────
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setSearch("");
|
|
setCommitted("");
|
|
setPage(1);
|
|
setSelected(null);
|
|
}
|
|
}, [open]);
|
|
|
|
const handleSelect = (asset) => {
|
|
setSelected(asset.asset_id);
|
|
onSelect(asset);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const label = fileType
|
|
? `${fileType.charAt(0).toUpperCase()}${fileType.slice(1)}s`
|
|
: "Assets";
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent side="right" className="w-full sm:max-w-lg flex flex-col gap-0 p-0">
|
|
|
|
{/* ── Header ── */}
|
|
<SheetHeader className="px-6 pt-6 pb-4 border-b">
|
|
<SheetTitle>Select {label}</SheetTitle>
|
|
<SheetDescription>
|
|
Click an asset to attach it.
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
{/* ── Search ── */}
|
|
<div className="px-6 py-3 border-b">
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder={`Search ${label.toLowerCase()}…`}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && triggerSearch()}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
onClick={triggerSearch}
|
|
disabled={loading}
|
|
>
|
|
{loading ? <Spinner className="h-4 w-4" /> : "Search"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Grid ── */}
|
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-48">
|
|
<Spinner className="h-5 w-5" />
|
|
</div>
|
|
) : !assets.length ? (
|
|
<EmptyState fileType={fileType ?? "asset"} />
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{assets.map((asset) => (
|
|
<AssetCard
|
|
key={asset.asset_id}
|
|
asset={asset}
|
|
selected={selected === asset.asset_id}
|
|
onSelect={handleSelect}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Pagination ── */}
|
|
{pagination.totalPages > 1 && (
|
|
<div className="flex items-center justify-between px-6 py-3 border-t text-sm">
|
|
<span className="text-muted-foreground">
|
|
Page {pagination.page} of {pagination.totalPages}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="button"
|
|
disabled={!pagination.hasPrevPage || loading}
|
|
onClick={() => setPage((p) => p - 1)}
|
|
className="px-3 py-1 rounded border text-xs disabled:opacity-40 hover:bg-muted transition-colors"
|
|
>
|
|
Prev
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={!pagination.hasNextPage || loading}
|
|
onClick={() => setPage((p) => p + 1)}
|
|
className="px-3 py-1 rounded border text-xs disabled:opacity-40 hover:bg-muted transition-colors"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
} |