Pull Request branch dev-clone to main #1
|
|
@ -13,7 +13,7 @@ import {
|
||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { useDebouncedCallback } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { Button } from "@/shadcn/components/ui/button";
|
import { Button } from "@/shadcn/components/ui/button";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { Card } from "@/shadcn/components/ui/card";
|
import { Card } from "@/shadcn/components/ui/card";
|
||||||
|
|
@ -23,17 +23,16 @@ import {
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
PaginationEllipsis,
|
PaginationEllipsis,
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/shadcn/components/ui/pagination"
|
} from "@/shadcn/components/ui/pagination";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/shadcn/components/ui/select"
|
} from "@/shadcn/components/ui/select";
|
||||||
|
|
||||||
type PaginatedResponse<T extends Record<string, unknown>> = {
|
type PaginatedResponse<T extends Record<string, unknown>> = {
|
||||||
data: Array<T>;
|
data: Array<T>;
|
||||||
|
|
@ -78,32 +77,32 @@ const createCreateButton = (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
property: Props<any, any, any>["createButton"] = true
|
property: Props<any, any, any>["createButton"] = true
|
||||||
) => {
|
) => {
|
||||||
if (property === true) {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const addQuery = () => {
|
||||||
|
navigate({ to: `${window.location.pathname}`, search: { create: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property === true) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
className="gap-2"
|
||||||
className="flex bg-white w-full text-black border items-center justify-center hover:bg-muted-foreground hover:text-white"
|
variant={"outline"}
|
||||||
onClick={() => navigate({ to: `${window.location.pathname}`, search: { create: true } })}
|
onClick={addQuery}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-between gap-2">
|
|
||||||
Create New
|
Create New
|
||||||
<TbPlus />
|
<TbPlus />
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else if (typeof property === "string") {
|
} else if (typeof property === "string") {
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
className="gap-2"
|
||||||
className="flex bg-white w-full text-black border items-center justify-between hover:bg-muted-foreground hover:text-white"
|
variant={"outline"}
|
||||||
onClick={() => navigate({ to: `${window.location.pathname}`, search: { create: true } })}
|
onClick={addQuery}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-between gap-2">
|
|
||||||
{property}
|
{property}
|
||||||
<TbPlus />
|
<TbPlus />
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -120,56 +119,87 @@ const createCreateButton = (
|
||||||
const CustomPagination = ({
|
const CustomPagination = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
onPageChange,
|
onChange,
|
||||||
hasNextPage,
|
|
||||||
}: {
|
}: {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
onPageChange: (page: number) => void;
|
onChange: (page: number) => void;
|
||||||
hasNextPage: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
|
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 (
|
return (
|
||||||
<Pagination>
|
<Pagination>
|
||||||
<PaginationContent>
|
<PaginationContent className="flex flex-col items-center gap-4 md:flex-row">
|
||||||
<PaginationItem>
|
<PaginationItem className="w-full md:w-auto">
|
||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
onClick={() => onChange(Math.max(1, currentPage - 1))}
|
||||||
className={`${currentPage === 1
|
aria-disabled={currentPage - 1 == 0 ? true : false}
|
||||||
? 'bg-white text-muted-foreground cursor-not-allowed'
|
className="w-full gap-2 md:w-auto"
|
||||||
: 'bg-white text-black hover:bg-muted hover:text-black'
|
|
||||||
}`}
|
|
||||||
aria-disabled={currentPage === 1}
|
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</PaginationPrevious>
|
</PaginationPrevious>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
{Array.from({ length: totalPages }, (_, index) => index + 1).map((page) => (
|
{getPaginationItems().map((item) => (
|
||||||
<PaginationItem key={page}>
|
<PaginationItem key={item.key}>
|
||||||
<PaginationLink
|
{item}
|
||||||
href="#"
|
|
||||||
onClick={() => onPageChange(page)}
|
|
||||||
className={page === currentPage ? 'border text-black' : ''}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
{totalPages > 1 && currentPage < totalPages && (
|
<PaginationItem className="w-full md:w-auto">
|
||||||
<PaginationItem>
|
|
||||||
<PaginationEllipsis />
|
|
||||||
</PaginationItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext
|
<PaginationNext
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
onClick={() => onChange(Math.min(totalPages, currentPage + 1))}
|
||||||
className={`${!hasNextPage || currentPage === totalPages
|
aria-disabled={currentPage == totalPages ? true : false}
|
||||||
? 'bg-white text-muted-foreground cursor-not-allowed'
|
className="w-full gap-2 md:w-auto"
|
||||||
: 'bg-white text-black hover:bg-muted hover:text-black'
|
|
||||||
}`}
|
|
||||||
aria-disabled={!hasNextPage || currentPage === totalPages}
|
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</PaginationNext>
|
</PaginationNext>
|
||||||
|
|
@ -197,14 +227,14 @@ export default function PageTemplate<
|
||||||
q: "",
|
q: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// const [deboucedSearchQuery] = useDebouncedValue(filterOptions.q, 500);
|
const [debouncedSearchQuery] = useDebouncedValue(filterOptions.q, 500);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
...(typeof props.queryOptions === "function"
|
...(typeof props.queryOptions === "function"
|
||||||
? props.queryOptions(
|
? props.queryOptions(
|
||||||
filterOptions.page,
|
filterOptions.page,
|
||||||
filterOptions.limit,
|
filterOptions.limit,
|
||||||
filterOptions.q
|
debouncedSearchQuery
|
||||||
)
|
)
|
||||||
: props.queryOptions),
|
: props.queryOptions),
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
|
|
@ -216,7 +246,7 @@ export default function PageTemplate<
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
defaultColumn: {
|
defaultColumn: {
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<span className="text-base font-medium text-muted-foreground">
|
<span>
|
||||||
{props.getValue() as ReactNode}
|
{props.getValue() as ReactNode}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|
@ -228,13 +258,13 @@ export default function PageTemplate<
|
||||||
*
|
*
|
||||||
* @param value - The new search query value.
|
* @param value - The new search query value.
|
||||||
*/
|
*/
|
||||||
const handleSearchQueryChange = useDebouncedCallback((value: string) => {
|
const handleSearchQueryChange = (value: string) => {
|
||||||
setFilterOptions((prev) => ({
|
setFilterOptions((prev) => ({
|
||||||
page: 0,
|
page: 0,
|
||||||
limit: prev.limit,
|
limit: prev.limit,
|
||||||
q: value,
|
q: value,
|
||||||
}));
|
}));
|
||||||
}, 500);
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the change in page number.
|
* Handles the change in page number.
|
||||||
|
|
@ -242,42 +272,31 @@ export default function PageTemplate<
|
||||||
* @param page - The new page number.
|
* @param page - The new page number.
|
||||||
*/
|
*/
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
if (page >= 1 && page <= (query.data?._metadata.totalPages ?? 1)) {
|
|
||||||
setFilterOptions((prev) => ({
|
setFilterOptions((prev) => ({
|
||||||
page: page - 1, // Adjust for zero-based index
|
page: page - 1, // Adjust for zero-based index
|
||||||
limit: prev.limit,
|
limit: prev.limit,
|
||||||
q: prev.q,
|
q: prev.q,
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default values when query.data is undefined
|
|
||||||
const totalPages = query.data?._metadata.totalPages ?? 1;
|
|
||||||
const hasNextPage = query.data
|
|
||||||
? filterOptions.page < totalPages - 1
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<h1 className="text-2xl font-bold">{props.title}</h1>
|
<p className="text-2xl font-bold">{props.title}</p>
|
||||||
<Card className="p-4 border-hidden">
|
<Card className="p-4 border-hidden">
|
||||||
{/* Table Functionality */}
|
{/* Table Functionality */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* Search and Create Button */}
|
{/* Search and Create Button */}
|
||||||
<div className="flex flex-col md:flex-row lg:flex-row pb-4 justify-between">
|
<div className="flex flex-col md:flex-row lg:flex-row pb-4 justify-between gap-4">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<TbSearch
|
<TbSearch className="absolute top-1/2 left-3 transform -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
||||||
className="absolute top-1/2 left-3 transform -translate-y-1/2 text-muted-foreground pointer-events-none"
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
className="w-full max-w-xs pl-10"
|
className="w-full max-w-xs pl-10"
|
||||||
value={filterOptions.q}
|
value={filterOptions.q}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleSearchQueryChange(e.target.value)}
|
||||||
handleSearchQueryChange(e.target.value)
|
|
||||||
}
|
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{createCreateButton(props.createButton)}
|
{createCreateButton(props.createButton)}
|
||||||
|
|
@ -289,9 +308,9 @@ export default function PageTemplate<
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{query.data && (
|
{query.data && (
|
||||||
<div className="pt-4 flex flex-col md:flex-row lg:flex-row items-center justify-between">
|
<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">
|
<div className="flex flex-row lg:flex-col items-center w-fit gap-2">
|
||||||
<label className="block text-sm font-medium text-muted-foreground">Per Page</label>
|
<span className="block text-sm font-medium text-muted-foreground">Per Page</span>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setFilterOptions((prev) => ({
|
setFilterOptions((prev) => ({
|
||||||
|
|
@ -317,9 +336,8 @@ export default function PageTemplate<
|
||||||
</div>
|
</div>
|
||||||
<CustomPagination
|
<CustomPagination
|
||||||
currentPage={filterOptions.page + 1}
|
currentPage={filterOptions.page + 1}
|
||||||
totalPages={totalPages}
|
totalPages={query.data._metadata.totalPages}
|
||||||
onPageChange={handlePageChange}
|
onChange={handlePageChange}
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
|
|
@ -330,7 +348,6 @@ export default function PageTemplate<
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The Modals */}
|
|
||||||
{props.modals?.map((modal, index) => (
|
{props.modals?.map((modal, index) => (
|
||||||
<React.Fragment key={index}>{modal}</React.Fragment>
|
<React.Fragment key={index}>{modal}</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -44,17 +44,17 @@ function DashboardLayout() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return isAuthenticated ? (
|
return isAuthenticated ? (
|
||||||
<div className="flex flex-col h-screen">
|
<div className="flex flex-col w-full h-screen overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<AppHeader toggle={toggle} openNavbar={openNavbar} />
|
<AppHeader toggle={toggle} openNavbar={openNavbar} />
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex h-full w-screen overflow-hidden">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<AppNavbar />
|
<AppNavbar />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className={"flex-1 mt-16 p-6 bg-white overflow-auto"}>
|
<main className="relative w-full mt-16 p-6 bg-white overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user