wip: course layout and func to context

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-05-16 11:05:04 +08:00
parent dcf5f43fd5
commit 2b1955608d
38 changed files with 3086 additions and 9 deletions
+195
View File
@@ -0,0 +1,195 @@
import { useEffect, useState } 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 { Spinner } from "@/components/ui/spinner";
import { Badge } from "@/components/ui/badge";
import { useAssets } from "@/contexts/AdminAssetsContext";
// ─── Empty ────────────────────────────────────────────────────────────────────
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>
);
}
// ─── Asset Card ───────────────────────────────────────────────────────────────
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(" ")}
>
{/* Thumbnail */}
<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>
{/* Name */}
<div className="p-2">
<p className="text-xs font-medium truncate">{asset.display_name}</p>
</div>
{/* Selected checkmark */}
{selected && (
<div className="absolute top-1.5 right-1.5">
<CheckCircle2 className="h-5 w-5 text-primary fill-white" />
</div>
)}
</button>
);
}
// ─── Sheet ────────────────────────────────────────────────────────────────────
export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) {
const { fetchAssets, assets, pagination, loading } = useAssets();
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [selected, setSelected] = useState(null);
const LIMIT = 12;
// ── Fetch on open / search / page change ──────────────────────────────────
useEffect(() => {
if (!open) return;
fetchAssets({
page,
limit: LIMIT,
filters: [
...(fileType ? [{ id: "file_type", value: [fileType] }] : []),
...(search ? [{ id: "display_name", value: [search] }] : []),
],
});
}, [open, page, search, fileType]);
// ── Reset on close ────────────────────────────────────────────────────────
useEffect(() => {
if (!open) {
setSearch("");
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="relative">
<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); setPage(1); }}
className="pl-9"
/>
</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>
);
}