Delete apps/frontend/src/components/PageTemplate.tsx

This commit is contained in:
Abiyasa Putra Prasetya 2024-10-04 08:19:49 +07:00 committed by GitHub
parent 7d9cd51d83
commit d883af0f6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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>
);
}