147 lines
5.5 KiB
TypeScript
147 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { Search, Award } from "lucide-react";
|
|
import {
|
|
ColumnDef,
|
|
DataTableLayout,
|
|
Card,
|
|
Input,
|
|
} from "ikoncomponents";
|
|
import { getAllGrade } from "@/app/utils/api/companyData/gradeApi.ts";
|
|
import { GradeResponseDto } from "@/app/utils/api/companyData/gradeApi.ts/type";
|
|
|
|
function GradeDataTable() {
|
|
const [gradeTableData, setGradeTableData] = useState<GradeResponseDto[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [search, setSearch] = useState("");
|
|
|
|
// DataTableLayout defaults to list view and has no prop to start in grid, so
|
|
// once mounted we click its built-in "Grid View" toggle once to default to cards.
|
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
|
const defaultedToGrid = useRef(false);
|
|
|
|
const filteredGradeTableData = useMemo(() => {
|
|
if (!search.trim()) return gradeTableData;
|
|
return gradeTableData.filter((gradeData) =>
|
|
gradeData.grade?.toLowerCase().includes(search.toLowerCase()),
|
|
);
|
|
}, [gradeTableData, search]);
|
|
|
|
const fetchGradeTableData = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const gradeData = await getAllGrade();
|
|
setGradeTableData(gradeData?.content || []);
|
|
} catch (error) {
|
|
console.error("Error fetching grade data:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchGradeTableData();
|
|
}, []);
|
|
|
|
// Switch DataTableLayout into grid (card) view as soon as it mounts, once.
|
|
useEffect(() => {
|
|
if (isLoading || defaultedToGrid.current) return;
|
|
const gridButton =
|
|
tableContainerRef.current?.querySelector<HTMLButtonElement>(
|
|
'button[title="Grid View"]',
|
|
);
|
|
if (gridButton) {
|
|
gridButton.click();
|
|
defaultedToGrid.current = true;
|
|
}
|
|
}, [isLoading]);
|
|
|
|
// ikoncomponents passes the row object directly to cell(), not { row }.
|
|
const columns: ColumnDef<GradeResponseDto>[] = [
|
|
{
|
|
accessorKey: "grade",
|
|
header: "Grade",
|
|
cell: (row) => <span>{row.grade || "n/a"}</span>,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-5 p-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-lg font-semibold">Grades</h1>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{filteredGradeTableData.length} of {gradeTableData.length} grades
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div ref={tableContainerRef}>
|
|
<DataTableLayout
|
|
data={filteredGradeTableData}
|
|
columns={columns}
|
|
extraTools={{
|
|
keyExtractor: (row: GradeResponseDto) => row.id,
|
|
totalPages: 1,
|
|
currentPage: 1,
|
|
isLoading,
|
|
onReload: fetchGradeTableData,
|
|
actionNode: (
|
|
<div className="flex w-full min-w-[260px] flex-1 items-center gap-2 rounded-lg border px-3 h-9">
|
|
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<div className="h-4 w-px shrink-0 bg-border" />
|
|
<Input
|
|
placeholder="Search grades..."
|
|
className="h-8 border-none p-0 text-sm shadow-none placeholder:text-muted-foreground focus-visible:ring-0"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
),
|
|
gridComponent: (data: GradeResponseDto[]) => (
|
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
|
{data.length > 0 ? (
|
|
data.map((gradeItem) => (
|
|
<Card
|
|
key={gradeItem.id}
|
|
// flex-row + py-4 explicitly override the base Card's
|
|
// `flex flex-col gap-6 py-6`, otherwise the icon stacks on
|
|
// top and the content centers with a lot of empty space.
|
|
className="group relative flex flex-row items-center gap-3.5 overflow-hidden rounded-xl border py-4! px-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md"
|
|
>
|
|
{/* Left accent rail */}
|
|
<span className="absolute inset-y-0 left-0 w-1 bg-primary/60 opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary transition-transform duration-200 group-hover:scale-105">
|
|
<Award className="h-5 w-5" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
|
Grade
|
|
</p>
|
|
<p
|
|
className="truncate text-sm font-semibold leading-tight text-foreground"
|
|
title={gradeItem.grade}
|
|
>
|
|
{gradeItem.grade || "-"}
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
))
|
|
) : (
|
|
<div className="col-span-2 flex items-center justify-center rounded-lg border border-dashed p-8 text-muted-foreground sm:col-span-3 lg:col-span-4">
|
|
No grades found.
|
|
</div>
|
|
)}
|
|
</div>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default GradeDataTable;
|