Delete apps/frontend/src/components/PageTemplate.tsx
This commit is contained in:
parent
7d9cd51d83
commit
d883af0f6e
|
|
@ -1,360 +0,0 @@
|
||||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
|
||||||
import React, { ReactNode, useState } from "react";
|
|
||||||
import { TbPlus, TbSearch } from "react-icons/tb";
|
|
||||||
import DashboardTable from "./DashboardTable";
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import {
|
|
||||||
QueryKey,
|
|
||||||
UseQueryOptions,
|
|
||||||
keepPreviousData,
|
|
||||||
useQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
|
||||||
import { Button } from "@/shadcn/components/ui/button";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { Card } from "@/shadcn/components/ui/card";
|
|
||||||
import { Input } from "@/shadcn/components/ui/input";
|
|
||||||
import {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
} from "@/shadcn/components/ui/pagination";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/shadcn/components/ui/select";
|
|
||||||
import { HiChevronLeft, HiChevronRight } from "react-icons/hi";
|
|
||||||
|
|
||||||
type PaginatedResponse<T extends Record<string, unknown>> = {
|
|
||||||
data: Array<T>;
|
|
||||||
_metadata: {
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
perPage: number;
|
|
||||||
totalItems: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
//ref: https://x.com/TkDodo/status/1491451513264574501
|
|
||||||
type Props<
|
|
||||||
TQueryKey extends QueryKey,
|
|
||||||
TQueryFnData extends Record<string, unknown>,
|
|
||||||
TError,
|
|
||||||
TData extends Record<string, unknown> = TQueryFnData,
|
|
||||||
> = {
|
|
||||||
title: string;
|
|
||||||
createButton?: string | true | React.ReactNode;
|
|
||||||
modals?: React.ReactNode[];
|
|
||||||
queryOptions: (
|
|
||||||
page: number,
|
|
||||||
limit: number,
|
|
||||||
q?: string
|
|
||||||
) => UseQueryOptions<
|
|
||||||
PaginatedResponse<TQueryFnData>,
|
|
||||||
TError,
|
|
||||||
PaginatedResponse<TData>,
|
|
||||||
TQueryKey
|
|
||||||
>;
|
|
||||||
columnDefs: ColumnDef<any>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a "Create New" button or returns the provided React node.
|
|
||||||
*
|
|
||||||
* @param property - The property that determines the type of button to create. It can be a boolean, string, or React node.
|
|
||||||
* @returns The create button element.
|
|
||||||
*/
|
|
||||||
const createCreateButton = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
property: Props<any, any, any>["createButton"] = true
|
|
||||||
) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const addQuery = () => {
|
|
||||||
navigate({ to: `${window.location.pathname}`, search: { create: true } });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (property === true) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="gap-2"
|
|
||||||
variant={"outline"}
|
|
||||||
onClick={addQuery}
|
|
||||||
>
|
|
||||||
Tambah Data
|
|
||||||
<TbPlus />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else if (typeof property === "string") {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="gap-2"
|
|
||||||
variant={"outline"}
|
|
||||||
onClick={addQuery}
|
|
||||||
>
|
|
||||||
{property}
|
|
||||||
<TbPlus />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return property;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pagination component for handling page navigation.
|
|
||||||
*
|
|
||||||
* @param props - The properties object.
|
|
||||||
* @returns The rendered Pagination component.
|
|
||||||
*/
|
|
||||||
const CustomPagination = ({
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
onChange: (page: number) => void;
|
|
||||||
}) => {
|
|
||||||
const getPaginationItems = () => {
|
|
||||||
let items = [];
|
|
||||||
|
|
||||||
// Determine start and end pages
|
|
||||||
let startPage =
|
|
||||||
currentPage == totalPages && currentPage > 3 ?
|
|
||||||
Math.max(1, currentPage - 2) :
|
|
||||||
Math.max(1, currentPage - 1);
|
|
||||||
let endPage =
|
|
||||||
currentPage == 1 ?
|
|
||||||
Math.min(totalPages, currentPage + 2) :
|
|
||||||
Math.min(totalPages, currentPage + 1);
|
|
||||||
|
|
||||||
// Add ellipsis if needed
|
|
||||||
if (startPage > 2) {
|
|
||||||
items.push(<PaginationEllipsis key="start-ellipsis" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add page numbers
|
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
|
||||||
items.push(
|
|
||||||
<Button className='cursor-pointer' key={i} onClick={() => onChange(i)} variant={currentPage == i ? "outline" : "ghost"}>
|
|
||||||
{i}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ellipsis after
|
|
||||||
if (endPage < totalPages - 1) {
|
|
||||||
items.push(<PaginationEllipsis key="end-ellipsis" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add last page
|
|
||||||
if (endPage < totalPages) {
|
|
||||||
items.push(
|
|
||||||
<Button className='cursor-pointer' key={totalPages} onClick={() => onChange(totalPages)} variant={"ghost"}>
|
|
||||||
{totalPages}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (currentPage > 2) {
|
|
||||||
items.unshift(
|
|
||||||
<Button className='cursor-pointer' key={1} onClick={() => onChange(1)} variant={"ghost"}>
|
|
||||||
1
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pagination>
|
|
||||||
<PaginationContent className="flex flex-col items-center gap-4 md:flex-row">
|
|
||||||
<PaginationItem className="w-full md:w-auto">
|
|
||||||
<Button
|
|
||||||
onClick={() => onChange(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage - 1 == 0 ? true : false}
|
|
||||||
className="w-full gap-2 md:w-auto"
|
|
||||||
variant={"ghost"}
|
|
||||||
>
|
|
||||||
<HiChevronLeft />
|
|
||||||
Sebelumnya
|
|
||||||
</Button>
|
|
||||||
</PaginationItem>
|
|
||||||
<div className="flex flex-wrap justify-center gap-2">
|
|
||||||
{getPaginationItems().map((item) => (
|
|
||||||
<PaginationItem key={item.key}>
|
|
||||||
{item}
|
|
||||||
</PaginationItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<PaginationItem className="w-full md:w-auto">
|
|
||||||
<Button
|
|
||||||
onClick={() => onChange(Math.min(totalPages, currentPage + 1))}
|
|
||||||
disabled={currentPage == totalPages ? true : false}
|
|
||||||
className="w-full gap-2 md:w-auto"
|
|
||||||
variant={"ghost"}
|
|
||||||
>
|
|
||||||
Selanjutnya
|
|
||||||
<HiChevronRight />
|
|
||||||
</Button>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PageTemplate component for displaying a paginated table with search and filter functionality.
|
|
||||||
|
|
||||||
* @param props - The properties object.
|
|
||||||
* @returns The rendered PageTemplate component.
|
|
||||||
*/
|
|
||||||
export default function PageTemplate<
|
|
||||||
TQueryKey extends QueryKey,
|
|
||||||
TQueryFnData extends Record<string, unknown>,
|
|
||||||
TError,
|
|
||||||
TData extends Record<string, unknown> = TQueryFnData,
|
|
||||||
>(props: Props<TQueryKey, TQueryFnData, TError, TData>) {
|
|
||||||
const [filterOptions, setFilterOptions] = useState({
|
|
||||||
page: 0,
|
|
||||||
limit: 10,
|
|
||||||
q: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [debouncedSearchQuery] = useDebouncedValue(filterOptions.q, 500);
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
...(typeof props.queryOptions === "function"
|
|
||||||
? props.queryOptions(
|
|
||||||
filterOptions.page,
|
|
||||||
filterOptions.limit,
|
|
||||||
debouncedSearchQuery
|
|
||||||
)
|
|
||||||
: props.queryOptions),
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: query.data?.data ?? [],
|
|
||||||
columns: props.columnDefs,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
defaultColumn: {
|
|
||||||
cell: (props) => (
|
|
||||||
<span>
|
|
||||||
{props.getValue() as ReactNode}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the change in search query input with debounce.
|
|
||||||
*
|
|
||||||
* @param value - The new search query value.
|
|
||||||
*/
|
|
||||||
const handleSearchQueryChange = (value: string) => {
|
|
||||||
setFilterOptions((prev) => ({
|
|
||||||
page: 0,
|
|
||||||
limit: prev.limit,
|
|
||||||
q: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the change in page number.
|
|
||||||
*
|
|
||||||
* @param page - The new page number.
|
|
||||||
*/
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
setFilterOptions((prev) => ({
|
|
||||||
page: page - 1, // Adjust for zero-based index
|
|
||||||
limit: prev.limit,
|
|
||||||
q: prev.q,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<p className="text-2xl font-bold">{props.title}</p>
|
|
||||||
<Card className="p-4 border-hidden">
|
|
||||||
{/* Table Functionality */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{/* Search and Create Button */}
|
|
||||||
<div className="flex flex-col md:flex-row lg:flex-row pb-4 justify-between gap-4">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<TbSearch className="absolute top-1/2 left-3 transform -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
|
||||||
<Input
|
|
||||||
id="search"
|
|
||||||
name="search"
|
|
||||||
className="w-full max-w-xs pl-10"
|
|
||||||
value={filterOptions.q}
|
|
||||||
onChange={(e) => handleSearchQueryChange(e.target.value)}
|
|
||||||
placeholder="Pencarian..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
{createCreateButton(props.createButton)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DashboardTable table={table} />
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{query.data && (
|
|
||||||
<div className="pt-4 flex flex-col md:flex-row lg:flex-row items-center justify-between gap-2">
|
|
||||||
<div className="flex flex-row lg:flex-col items-center w-fit gap-2">
|
|
||||||
<span className="block text-sm font-medium text-muted-foreground whitespace-nowrap">Per Halaman</span>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setFilterOptions((prev) => ({
|
|
||||||
page: prev.page,
|
|
||||||
limit: parseInt(value ?? "10"),
|
|
||||||
q: prev.q,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
defaultValue="10"
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-fit p-4 gap-4">
|
|
||||||
<SelectValue placeholder="Per Page" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
<SelectItem value="100">100</SelectItem>
|
|
||||||
<SelectItem value="500">500</SelectItem>
|
|
||||||
<SelectItem value="1000">1000</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<CustomPagination
|
|
||||||
currentPage={filterOptions.page + 1}
|
|
||||||
totalPages={query.data._metadata.totalPages}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
Menampilkan {query.data.data.length} dari {query.data._metadata.totalItems}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{props.modals?.map((modal, index) => (
|
|
||||||
<React.Fragment key={index}>{modal}</React.Fragment>
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user