155 lines
4.0 KiB
TypeScript
155 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Box,
|
|
Card,
|
|
Pagination,
|
|
Stack,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
useMediaQuery,
|
|
useTheme,
|
|
} from "@mui/material";
|
|
import { ReactNode } from "react";
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
|
|
export type Column<T> = {
|
|
id: string;
|
|
label: string;
|
|
align?: "left" | "right" | "center";
|
|
width?: string | number;
|
|
render?: (row: T) => ReactNode;
|
|
};
|
|
|
|
type DataTableProps<T> = {
|
|
columns: Column<T>[];
|
|
data: T[];
|
|
keyField: keyof T;
|
|
emptyMessage?: string;
|
|
loading?: boolean;
|
|
onRowClick?: (row: T) => void;
|
|
pagination?: {
|
|
total: number;
|
|
page: number;
|
|
perPage: number;
|
|
};
|
|
/** If provided, renders this instead of the table on xs/sm screens */
|
|
mobileCard?: (row: T) => ReactNode;
|
|
};
|
|
|
|
function PaginationBar({ page, perPage, total }: { page: number; perPage: number; total: number }) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const pageCount = Math.ceil(total / perPage);
|
|
|
|
if (pageCount <= 1) return null;
|
|
|
|
function handlePageChange(_: React.ChangeEvent<unknown>, newPage: number) {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
params.set("page", String(newPage));
|
|
router.push(`${pathname}?${params.toString()}`);
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
|
|
<Pagination
|
|
count={pageCount}
|
|
page={page}
|
|
onChange={handlePageChange}
|
|
color="primary"
|
|
shape="rounded"
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export function DataTable<T>({
|
|
columns,
|
|
data,
|
|
keyField,
|
|
emptyMessage = "No data available",
|
|
loading = false,
|
|
onRowClick,
|
|
pagination,
|
|
mobileCard,
|
|
}: DataTableProps<T>) {
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
|
|
|
const isEmpty = data.length === 0 && !loading;
|
|
|
|
if (isMobile && mobileCard) {
|
|
return (
|
|
<Box>
|
|
{isEmpty ? (
|
|
<Card variant="outlined" sx={{ py: 6, textAlign: "center" }}>
|
|
<Typography color="text.secondary">{emptyMessage}</Typography>
|
|
</Card>
|
|
) : (
|
|
<Stack spacing={1.5}>
|
|
{data.map((row) => (
|
|
<Box key={String(row[keyField])}>
|
|
{mobileCard(row)}
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
{pagination && <PaginationBar {...pagination} />}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<TableContainer component={Card} variant="outlined" sx={{ overflowX: "auto" }}>
|
|
<Table sx={{ minWidth: 600 }}>
|
|
<TableHead>
|
|
<TableRow>
|
|
{columns.map((col) => (
|
|
<TableCell
|
|
key={col.id}
|
|
align={col.align || "left"}
|
|
width={col.width}
|
|
>
|
|
{col.label}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{isEmpty ? (
|
|
<TableRow>
|
|
<TableCell colSpan={columns.length} align="center" sx={{ py: 8 }}>
|
|
<Typography color="text.secondary">{emptyMessage}</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
data.map((row) => (
|
|
<TableRow
|
|
key={String(row[keyField])}
|
|
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
|
sx={onRowClick ? { cursor: "pointer", "&:hover": { bgcolor: "action.hover" } } : undefined}
|
|
>
|
|
{columns.map((col) => (
|
|
<TableCell key={col.id} align={col.align || "left"}>
|
|
{col.render ? col.render(row) : (row as Record<string, unknown>)[col.id] as ReactNode}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
{pagination && <PaginationBar {...pagination} />}
|
|
</Box>
|
|
);
|
|
}
|