mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
integrate users
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
// ─── Field-specific display maps ──────────────────────────────────────────────
|
||||
const FIELD_DISPLAY_MAP = {
|
||||
is_active: { true: "Active", false: "Inactive" },
|
||||
is_verified: { true: "Verified", false: "Not Verified" },
|
||||
};
|
||||
|
||||
const BOOLEAN_FIELDS = Object.keys(FIELD_DISPLAY_MAP);
|
||||
|
||||
// ─── Generic formatter ────────────────────────────────────────────────────────
|
||||
const formatFilterItem = (item, field, type) => {
|
||||
if (FIELD_DISPLAY_MAP[field]) {
|
||||
return FIELD_DISPLAY_MAP[field][String(item)] ?? item;
|
||||
}
|
||||
if (type === "date" && item) {
|
||||
return new Date(item).toLocaleDateString("en-US", {
|
||||
year: "numeric", month: "long", day: "numeric",
|
||||
});
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
// ─── Reusable empty state ─────────────────────────────────────────────────────
|
||||
const EmptyState = ({ search }) => (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
{search ? `No results for "${search}".` : "No data found."}
|
||||
</p>
|
||||
);
|
||||
|
||||
// ─── Reusable item list renderer ──────────────────────────────────────────────
|
||||
const FilterList = ({ items, field, type, selected, onToggle, inputType = "checkbox" }) => {
|
||||
if (items.length === 0) return <EmptyState />;
|
||||
|
||||
return items.map((item) => (
|
||||
<label key={item} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type={inputType}
|
||||
name={inputType === "radio" ? field : undefined}
|
||||
checked={selected.includes(String(item))}
|
||||
onChange={() => onToggle(item)}
|
||||
/>
|
||||
{formatFilterItem(item, field, type)}
|
||||
</label>
|
||||
));
|
||||
};
|
||||
|
||||
export function FilterSheet({ open, onOpenChange, column, attr, data = [], loading }) {
|
||||
const type = attr?.type;
|
||||
const field = attr?.field;
|
||||
|
||||
const enumData = attr?.options?.choices || [];
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const val = column?.getFilterValue();
|
||||
setSelected(Array.isArray(val) ? val.map(String) : []);
|
||||
}, [column]);
|
||||
|
||||
const sourceData = useMemo(() => {
|
||||
if (BOOLEAN_FIELDS.includes(field)) return ["true", "false"];
|
||||
if (type === "enum") return enumData;
|
||||
return data || [];
|
||||
}, [field, type, enumData, data]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!search) return sourceData;
|
||||
return sourceData.filter((item) =>
|
||||
formatFilterItem(item, field, type)
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
);
|
||||
}, [search, sourceData, field, type]);
|
||||
|
||||
const toggle = (item) => {
|
||||
setSelected((prev) =>
|
||||
prev.includes(String(item))
|
||||
? prev.filter((v) => v !== String(item))
|
||||
: [...prev, String(item)]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleRadio = (item) => setSelected([String(item)]);
|
||||
|
||||
const isBoolean = BOOLEAN_FIELDS.includes(field);
|
||||
const isEnum = type === "enum" && !isBoolean;
|
||||
const isList = !isBoolean && !isEnum;
|
||||
|
||||
const isEmpty = !loading && sourceData.length === 0;
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="right" className="w-[380px] flex flex-col">
|
||||
<SheetHeader className="shrink-0 border-b">
|
||||
<SheetTitle>Filter by {column?.columnDef?.header}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col gap-4 px-4 relative">
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/60 backdrop-blur-sm z-10">
|
||||
<Spinner className="size-8" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================= EMPTY STATE ================= */}
|
||||
{isEmpty && <EmptyState />}
|
||||
|
||||
{/* ================= BOOLEAN ================= */}
|
||||
{!isEmpty && isBoolean && (
|
||||
<div className="space-y-2 overflow-auto pr-2 pt-2">
|
||||
<FilterList
|
||||
items={sourceData}
|
||||
field={field}
|
||||
type={type}
|
||||
selected={selected}
|
||||
onToggle={toggleRadio}
|
||||
inputType="radio"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================= ENUM ================= */}
|
||||
{!isEmpty && isEnum && (
|
||||
<div className="space-y-2 overflow-auto pr-2 pt-2">
|
||||
<FilterList
|
||||
items={sourceData}
|
||||
field={field}
|
||||
type={type}
|
||||
selected={selected}
|
||||
onToggle={toggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ========== TEXT / NUMBER / DATE ========== */}
|
||||
{!isEmpty && isList && (
|
||||
<>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={`Search ${type}...`}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 h-[550px] pr-2">
|
||||
<div className="space-y-2">
|
||||
{filteredData.length === 0
|
||||
? <EmptyState search={search} />
|
||||
: <FilterList
|
||||
items={filteredData}
|
||||
field={field}
|
||||
type={type}
|
||||
selected={selected}
|
||||
onToggle={toggle}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<ScrollBar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Footer — hidden when empty ──────────────────────────────────── */}
|
||||
{!isEmpty && (
|
||||
<div className="shrink-0 flex gap-2 border-t p-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setSelected([]);
|
||||
column.setFilterValue(undefined);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
column.setFilterValue(selected);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user