Files
var-monorepo/apps/hub/app/_components/PaginatedTable.tsx
2026-01-30 16:19:00 +01:00

171 lines
4.4 KiB
TypeScript

"use client";
import { useState, Ref, useImperativeHandle, useEffect, useCallback } from "react";
import SortableTable, { Pagination, RowsPerPage, SortableTableProps } from "./Table";
import { PrismaClient } from "@repo/db";
import { getData } from "./pagiantedTableActions";
import { cn, useDebounce } from "@repo/shared-components";
export interface PaginatedTableRef {
refresh: () => void;
}
interface PaginatedTableProps<TData, TWhere extends object>
extends Omit<SortableTableProps<TData>, "data"> {
prismaModel: keyof PrismaClient;
stickyHeaders?: boolean;
initialRowsPerPage?: number;
showSearch?: boolean;
getFilter?: (searchTerm: string) => TWhere;
include?: Record<string, boolean>;
strictQuery?: boolean;
leftOfSearch?: React.ReactNode;
rightOfSearch?: React.ReactNode;
leftOfPagination?: React.ReactNode;
rightOfPagination?: React.ReactNode;
supressQuery?: boolean;
ref?: Ref<PaginatedTableRef>;
}
export function PaginatedTable<TData, TWhere extends object>({
prismaModel,
initialRowsPerPage = 30,
getFilter,
showSearch = false,
include,
ref,
strictQuery = false,
stickyHeaders = false,
leftOfSearch,
rightOfSearch,
leftOfPagination,
rightOfPagination,
supressQuery,
...restProps
}: PaginatedTableProps<TData, TWhere>) {
const [data, setData] = useState<TData[]>([]);
const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage);
const [page, setPage] = useState(0);
const [total, setTotal] = useState(0);
const [searchTerm, setSearchTerm] = useState("");
const [orderBy, setOrderBy] = useState<Record<string, "asc" | "desc">>(
restProps.initialOrderBy
? restProps.initialOrderBy.reduce(
(acc, sort) => {
acc[sort.id] = sort.desc ? "desc" : "asc";
return acc;
},
{} as Record<string, "asc" | "desc">,
)
: {},
);
const [loading, setLoading] = useState(false);
const refreshTableData = useCallback(async () => {
if (supressQuery) {
setLoading(false);
return;
}
setLoading(true);
getData({
model: prismaModel,
limit: rowsPerPage,
offset: page * rowsPerPage,
where: getFilter ? getFilter(searchTerm) : undefined,
include,
orderBy,
select: strictQuery
? restProps.columns
.filter(
(col): col is { accessorKey: string } =>
typeof (col as { accessorKey?: unknown }).accessorKey === "string",
)
.map((col) => col.accessorKey)
.reduce<Record<string, boolean>>((acc, key) => {
acc[key] = true;
return acc;
}, {})
: undefined,
})
.then((result) => {
if (result) {
setData(result.data);
setTotal(result.total);
}
})
.finally(() => {
setLoading(false);
});
}, [
supressQuery,
prismaModel,
rowsPerPage,
page,
searchTerm,
getFilter,
include,
orderBy,
strictQuery,
restProps.columns,
]);
useImperativeHandle(ref, () => ({
refresh: () => {
refreshTableData();
},
}));
// useEffect to show loading spinner
useEffect(() => {
if (supressQuery) return;
setLoading(true);
}, [searchTerm, page, rowsPerPage, orderBy, getFilter, setLoading, supressQuery]);
useDebounce(
() => {
refreshTableData();
},
500,
[searchTerm, page, rowsPerPage, orderBy, getFilter],
);
return (
<div className="m-4 space-y-4">
{(rightOfSearch || leftOfSearch || showSearch) && (
<div
className={cn(
"sticky z-20 flex items-center gap-2 py-2",
stickyHeaders && "bg-base-100/80 sticky top-0 border-b backdrop-blur",
)}
>
<div className="flex flex-1 gap-2">
<div>{leftOfSearch}</div>
<div>{loading && <span className="loading loading-dots loading-md" />}</div>
</div>
{showSearch && (
<input
type="text"
placeholder="Suchen..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setPage(0); // Reset to first page on search
}}
className="input input-bordered w-full max-w-xs justify-end"
/>
)}
<div className="flex justify-center">{rightOfSearch}</div>
</div>
)}
<SortableTable data={data} prismaModel={prismaModel} setOrderBy={setOrderBy} {...restProps} />
<div className="items-between flex">
{leftOfPagination}
<RowsPerPage rowsPerPage={rowsPerPage} setRowsPerPage={setRowsPerPage} />
{rightOfPagination}
<Pagination totalPages={Math.ceil(total / rowsPerPage)} page={page} setPage={setPage} />
</div>
</div>
);
}