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,250 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { format } from "date-fns";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
export const pageSizes = [10, 25, 50, 75, 100, 250, 500, 750, 1000]
|
||||
|
||||
// ── Badge style map for enum values ───────────────────────────────────────────
|
||||
const BADGE_STYLES = {
|
||||
"Active": "bg-emerald-100 text-emerald-800 border-emerald-200",
|
||||
"Not Active": "bg-rose-100 text-rose-700 border-rose-200",
|
||||
"Verified": "bg-blue-100 text-blue-800 border-blue-200",
|
||||
"Not Verified": "bg-amber-100 text-amber-700 border-amber-200",
|
||||
"system": "bg-violet-50 text-violet-700 border-violet-200",
|
||||
"google": "bg-orange-50 text-orange-700 border-orange-200",
|
||||
"admin": "bg-red-100 text-red-700 border-red-200",
|
||||
"user": "bg-blue-100 text-blue-700 border-blue-200",
|
||||
"staff": "bg-green-100 text-green-700 border-green-200",
|
||||
};
|
||||
|
||||
function EnumBadge({ value }) {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[11px] font-medium px-2 py-0.5 ${BADGE_STYLES[value] ?? ""}`}
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDate(value) {
|
||||
if (!value) return null;
|
||||
try { return format(new Date(value), "MMM d, yyyy"); }
|
||||
catch { return value; }
|
||||
}
|
||||
|
||||
function renderCell(attr, value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return <span className="text-muted-foreground/40">-</span>;
|
||||
}
|
||||
|
||||
|
||||
switch (attr.type) {
|
||||
case "enum":
|
||||
if (attr.field === "is_active")
|
||||
return <EnumBadge value={value ? "Active" : "Not Active"} />;
|
||||
else if (attr.field === "is_verified")
|
||||
return <EnumBadge value={value ? "Verified" : "Not Verified"} />;
|
||||
else
|
||||
return <EnumBadge value={value} />;
|
||||
case "date": return <span className="text-xs text-muted-foreground">{formatDate(value)}</span>;
|
||||
case "number": return <span className="font-mono text-xs font-semibold text-muted-foreground">{value}</span>;
|
||||
default: return <span className="text-sm">{value}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export function ColumnFilter({ column, attr }) {
|
||||
const columnValue = column.getFilterValue() ?? "";
|
||||
const [inputValue, setInputValue] = useState(columnValue);
|
||||
|
||||
// keep in sync when external reset happens
|
||||
useEffect(() => {
|
||||
setInputValue(columnValue);
|
||||
}, [columnValue]);
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// ENUM (AUTO APPLY)
|
||||
// ─────────────────────────────────────────────
|
||||
if (attr.type === "enum") {
|
||||
return (
|
||||
<Select
|
||||
value={inputValue || "__all__"}
|
||||
onValueChange={(v) => {
|
||||
const value = v === "__all__" ? "" : v;
|
||||
setInputValue(value);
|
||||
column.setFilterValue(value); // ✅ auto apply
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs w-full">
|
||||
<SelectValue placeholder="All" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">All</SelectItem>
|
||||
{attr.options?.choices?.map((c) => (
|
||||
<SelectItem key={c} value={c}>
|
||||
{c}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// DATE (SINGLE)
|
||||
// ─────────────────────────────────────────────
|
||||
if (attr.type === "date") {
|
||||
const value =
|
||||
inputValue &&
|
||||
typeof inputValue === "object" &&
|
||||
("from" in inputValue || "to" in inputValue)
|
||||
? inputValue
|
||||
: null;
|
||||
|
||||
const isRange = !!value;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* MODE TOGGLE */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="radio"
|
||||
checked={!isRange}
|
||||
onChange={() => {
|
||||
setInputValue("");
|
||||
column.setFilterValue("");
|
||||
}}
|
||||
/>
|
||||
Single
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="radio"
|
||||
checked={isRange}
|
||||
onChange={() => {
|
||||
setInputValue({ from: "", to: "" });
|
||||
column.setFilterValue({ from: "", to: "" });
|
||||
}}
|
||||
/>
|
||||
Range
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* SINGLE DATE */}
|
||||
{!isRange && (
|
||||
<Input
|
||||
type="date"
|
||||
value={inputValue || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setInputValue(val);
|
||||
column.setFilterValue(val);
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* RANGE */}
|
||||
{isRange && (
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
type="date"
|
||||
value={value.from}
|
||||
onChange={(e) => {
|
||||
const next = {
|
||||
from: e.target.value,
|
||||
to: value.to || "",
|
||||
};
|
||||
setInputValue(next);
|
||||
column.setFilterValue(next);
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
value={value.to}
|
||||
onChange={(e) => {
|
||||
const next = {
|
||||
from: value.from || "",
|
||||
to: e.target.value,
|
||||
};
|
||||
setInputValue(next);
|
||||
column.setFilterValue(next);
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// TEXT FILTER (manual apply)
|
||||
// ─────────────────────────────────────────────
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
column.setFilterValue(inputValue);
|
||||
}
|
||||
}}
|
||||
placeholder="Filter..."
|
||||
className="h-7 text-xs w-full"
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-7 w-7"
|
||||
onClick={() => column.setFilterValue(inputValue)}
|
||||
>
|
||||
<Search className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sort icon ──────────────────────────────────────────────────────────────────
|
||||
export function SortIcon({ column }) {
|
||||
const s = column.getIsSorted();
|
||||
if (s === "asc") return <ArrowUp className="ml-1 h-3 w-3 text-primary shrink-0" />;
|
||||
if (s === "desc") return <ArrowDown className="ml-1 h-3 w-3 text-primary shrink-0" />;
|
||||
return <ArrowUpDown className="ml-1 h-3 w-3 text-muted-foreground/40 shrink-0" />;
|
||||
}
|
||||
|
||||
// ── Build columns dynamically from attributes ──────────────────────────────────
|
||||
const columnHelper = createColumnHelper();
|
||||
|
||||
export function buildColumns(attrs) {
|
||||
return attrs.map((attr) =>
|
||||
columnHelper.accessor(attr.field, {
|
||||
id: attr.field,
|
||||
header: attr.name,
|
||||
enableSorting: true,
|
||||
enableColumnFilter: true,
|
||||
filterFn: (row, colId, filterValue) => {
|
||||
if (!filterValue) return true;
|
||||
const val = String(row.getValue(colId) ?? "").toLowerCase();
|
||||
return val.includes(String(filterValue).toLowerCase());
|
||||
},
|
||||
cell: (info) => renderCell(attr, info.getValue()),
|
||||
meta: { attr },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user