Pull Request branch dev-clone to main #1

Merged
gitea merged 429 commits from dev-clone into main 2024-12-23 09:31:34 +00:00
5 changed files with 175 additions and 189 deletions
Showing only changes of commit 20553f6497 - Show all commits

View File

@ -30,7 +30,7 @@ interface User {
// image: "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png", // image: "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png",
// }; // };
export default function AppHeader({toggle}: Props) { export default function AppHeader({ toggle }: Props) {
const [userMenuOpened, setUserMenuOpened] = useState(false); const [userMenuOpened, setUserMenuOpened] = useState(false);
const { user }: { user: User | null } = useAuth(); const { user }: { user: User | null } = useAuth();
@ -40,7 +40,7 @@ export default function AppHeader({toggle}: Props) {
// )); // ));
return ( return (
<header className="fixed top-0 left-0 w-full h-[60px] bg-white z-50 border"> <header className="fixed top-0 left-0 w-full h-16 bg-white z-50 border">
<div className="flex h-full justify-between w-full items-center"> <div className="flex h-full justify-between w-full items-center">
<Button <Button
onClick={toggle} onClick={toggle}
@ -57,31 +57,30 @@ export default function AppHeader({toggle}: Props) {
onOpenChange={setUserMenuOpened} onOpenChange={setUserMenuOpened}
> >
<DropdownMenuTrigger asChild className="flex"> <DropdownMenuTrigger asChild className="flex">
<button <button
className={cx(classNames.user, { className={cx(classNames.user, {
[classNames.userActive]: userMenuOpened, [classNames.userActive]: userMenuOpened,
})} })}
> >
<div className="flex items-center"> <div className="flex items-center">
<Avatar> <Avatar>
{user?.photoProfile ? ( {user?.photoProfile ? (
<AvatarImage src={user.photoProfile} /> <AvatarImage src={user.photoProfile} />
) : ( ) : (
<AvatarFallback>{user?.name?.charAt(0) ?? "A"}</AvatarFallback> <AvatarFallback>{user?.name?.charAt(0) ?? "A"}</AvatarFallback>
)} )}
</Avatar> </Avatar>
</div> </div>
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="transition-all duration-200 z-50 border bg-white" className="transition-all duration-200 z-50 border bg-white w-64"
style={{ width: '260px' }}
> >
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/logout">Logout</Link> <Link to="/logout">Logout</Link>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -15,7 +15,7 @@ import AppHeader from "./AppHeader";
* *
* @returns A React element representing the application's navigation bar. * @returns A React element representing the application's navigation bar.
*/ */
export default function AppNavbar(){ export default function AppNavbar() {
// const {user} = useAuth(); // const {user} = useAuth();
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -28,68 +28,60 @@ export default function AppNavbar(){
const { data } = useQuery({ const { data } = useQuery({
queryKey: ["sidebarData"], queryKey: ["sidebarData"],
queryFn: async () => { queryFn: async () => {
const res = await client.dashboard.getSidebarItems.$get(); const res = await client.dashboard.getSidebarItems.$get();
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
return data; return data;
} }
console.error("Error:", res.status, res.statusText); console.error("Error:", res.status, res.statusText);
throw new Error("Error fetching sidebar data"); throw new Error("Error fetching sidebar data");
}, },
}); });
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (window.innerWidth < 768) { // Ganti 768 dengan breakpoint mobile Anda if (window.innerWidth < 768) { // Ganti 768 dengan breakpoint mobile Anda
setSidebarOpen(false); setSidebarOpen(false);
} else { } else {
setSidebarOpen(true); setSidebarOpen(true);
} }
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
handleResize(); // Initial check handleResize(); // Initial check
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, []); }, []);
const handleMenuItemClick = () => { const handleMenuItemClick = () => {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
setSidebarOpen(false); setSidebarOpen(false);
} }
}; };
return ( return (
<> <>
<div> <div>
{/* Header */} {/* Header */}
<AppHeader toggle={toggleSidebar} openNavbar={isSidebarOpen} /> <AppHeader toggle={toggleSidebar} openNavbar={isSidebarOpen} />
{/* Sidebar */} {/* Sidebar */}
<div
className={`fixed lg:relative w-64 bg-white top-[60px] left-0 h-full z-40 px-3 py-4 transition-transform border-x
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}
>
<ScrollArea className="flex flex-1 h-full">
{data?.map((menu, i) => (
<MenuItem
key={i}
menu={menu}
isActive={pathname === menu.link}
onClick={handleMenuItemClick}
/>
))}
</ScrollArea>
</div>
{/* Overlay to close sidebar on mobile */}
{isSidebarOpen && (
<div <div
className="fixed inset-0 bg-black bg-opacity-50 z-30 hidden lg:visible" className={`fixed lg:relative w-64 bg-white top-16 left-0 h-full z-40 px-3 py-4 transition-transform border-x
onClick={toggleSidebar} ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}
/> >
)} <ScrollArea className="flex flex-1 h-full">
</div> {data?.map((menu, i) => (
<MenuItem
key={i}
menu={menu}
isActive={pathname === menu.link}
onClick={handleMenuItemClick}
/>
))}
</ScrollArea>
</div>
</div>
</> </>
); );
} }

View File

@ -15,15 +15,15 @@ interface Props<TData> {
export default function DashboardTable<T>({ table }: Props<T>) { export default function DashboardTable<T>({ table }: Props<T>) {
return ( return (
<ScrollArea className="w-full max-w-full"> <div className="w-full max-w-full overflow-x-auto border rounded-lg">
<Table className="min-w-full divide-y divide-gray-200"> <Table className="min-w-full divide-y divide-muted-foreground bg-white">
<TableHeader className="bg-gray-50"> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<TableHead <TableHead
key={header.id} key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-sm font-medium text-muted-foreground"
style={{ style={{
maxWidth: `${header.column.columnDef.maxSize}px`, maxWidth: `${header.column.columnDef.maxSize}px`,
width: `${header.getSize()}`, width: `${header.getSize()}`,
@ -38,14 +38,14 @@ export default function DashboardTable<T>({ table }: Props<T>) {
))} ))}
</TableHeader> </TableHeader>
<TableBody className="bg-white divide-y divide-gray-200"> <TableBody>
{table.getRowModel().rows.length > 0 ? ( {table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow key={row.id}> <TableRow key={row.id}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
key={cell.id} key={cell.id}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900" className="px-6 py-4 whitespace-nowrap text-sm text-black"
style={{ style={{
maxWidth: `${cell.column.columnDef.maxSize}px`, maxWidth: `${cell.column.columnDef.maxSize}px`,
}} }}
@ -64,6 +64,6 @@ export default function DashboardTable<T>({ table }: Props<T>) {
)} )}
</TableBody> </TableBody>
</Table> </Table>
</ScrollArea> </div>
); );
} }

View File

@ -26,7 +26,7 @@ import {
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} from "@/shadcn/components/ui/pagination" } from "@/shadcn/components/ui/pagination"
import { import {
Select, Select,
SelectContent, SelectContent,
@ -96,15 +96,15 @@ const createCreateButton = (
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Button <Button
type="button" type="button"
className="flex bg-white w-full text-black border items-center justify-between hover:bg-muted-foreground hover:text-white" className="flex bg-white w-full text-black border items-center justify-between hover:bg-muted-foreground hover:text-white"
onClick={() => navigate({ to: `${window.location.pathname}`, search: { create: true } })} onClick={() => navigate({ to: `${window.location.pathname}`, search: { create: true } })}
> >
<span className="flex items-center justify-between gap-2"> <span className="flex items-center justify-between gap-2">
{property} {property}
<TbPlus /> <TbPlus />
</span> </span>
</Button> </Button>
); );
} else { } else {
return property; return property;
@ -122,64 +122,62 @@ const CustomPagination = ({
totalPages, totalPages,
onPageChange, onPageChange,
hasNextPage, hasNextPage,
}: { }: {
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
onPageChange: (page: number) => void; onPageChange: (page: number) => void;
hasNextPage: boolean; hasNextPage: boolean;
}) => { }) => {
return ( return (
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious <PaginationPrevious
onClick={() => onPageChange(currentPage - 1)} onClick={() => onPageChange(currentPage - 1)}
className={`${ className={`${currentPage === 1
currentPage === 1 ? 'bg-white text-muted-foreground cursor-not-allowed'
? 'bg-white text-muted-foreground cursor-not-allowed' : 'bg-white text-black hover:bg-muted hover:text-black'
: 'bg-white text-black hover:bg-muted hover:text-black' }`}
}`} aria-disabled={currentPage === 1}
aria-disabled={currentPage === 1} >
> Previous
Previous </PaginationPrevious>
</PaginationPrevious> </PaginationItem>
</PaginationItem>
{Array.from({ length: totalPages }, (_, index) => index + 1).map((page) => ( {Array.from({ length: totalPages }, (_, index) => index + 1).map((page) => (
<PaginationItem key={page}> <PaginationItem key={page}>
<PaginationLink <PaginationLink
href="#" href="#"
onClick={() => onPageChange(page)} onClick={() => onPageChange(page)}
className={page === currentPage ? 'border text-black' : ''} className={page === currentPage ? 'border text-black' : ''}
> >
{page} {page}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
))} ))}
{totalPages > 1 && currentPage < totalPages && ( {totalPages > 1 && currentPage < totalPages && (
<PaginationItem> <PaginationItem>
<PaginationEllipsis /> <PaginationEllipsis />
</PaginationItem> </PaginationItem>
)} )}
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext
onClick={() => onPageChange(currentPage + 1)} onClick={() => onPageChange(currentPage + 1)}
className={`${ className={`${!hasNextPage || currentPage === totalPages
!hasNextPage || currentPage === totalPages ? 'bg-white text-muted-foreground cursor-not-allowed'
? 'bg-white text-muted-foreground cursor-not-allowed' : 'bg-white text-black hover:bg-muted hover:text-black'
: 'bg-white text-black hover:bg-muted hover:text-black' }`}
}`} aria-disabled={!hasNextPage || currentPage === totalPages}
aria-disabled={!hasNextPage || currentPage === totalPages} >
> Next
Next </PaginationNext>
</PaginationNext> </PaginationItem>
</PaginationItem> </PaginationContent>
</PaginationContent> </Pagination>
</Pagination>
); );
}; };
/** /**
* PageTemplate component for displaying a paginated table with search and filter functionality. * PageTemplate component for displaying a paginated table with search and filter functionality.
@ -204,10 +202,10 @@ export default function PageTemplate<
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 filterOptions.q
) )
: props.queryOptions), : props.queryOptions),
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
}); });
@ -218,8 +216,8 @@ export default function PageTemplate<
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
defaultColumn: { defaultColumn: {
cell: (props) => ( cell: (props) => (
<span className="text-base font-medium text-gray-700"> <span className="text-base font-medium text-muted-foreground">
{props.getValue() as ReactNode} {props.getValue() as ReactNode}
</span> </span>
), ),
}, },
@ -245,44 +243,40 @@ export default function PageTemplate<
*/ */
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
if (page >= 1 && page <= (query.data?._metadata.totalPages ?? 1)) { 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 // Default values when query.data is undefined
const totalPages = query.data?._metadata.totalPages ?? 1; const totalPages = query.data?._metadata.totalPages ?? 1;
const hasNextPage = query.data const hasNextPage = query.data
? filterOptions.page < totalPages - 1 ? filterOptions.page < totalPages - 1
: false; : 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> <h1 className="text-2xl font-bold">{props.title}</h1>
<Card className="p-4 border-hidden"> <Card className="p-4 border-hidden">
{/* Top Section */}
{/* 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">
<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-gray-500" className="absolute top-1/2 left-3 transform -translate-y-1/2 text-muted-foreground pointer-events-none"
style={{ pointerEvents: 'none' }} // Ensure the icon doesn't capture click events />
/> <Input
<Input 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">
@ -300,27 +294,27 @@ export default function PageTemplate<
<label className="block text-sm font-medium text-muted-foreground">Per Page</label> <label className="block text-sm font-medium text-muted-foreground">Per Page</label>
<Select <Select
onValueChange={(value) => onValueChange={(value) =>
setFilterOptions((prev) => ({ setFilterOptions((prev) => ({
page: prev.page, page: prev.page,
limit: parseInt(value ?? "10"), limit: parseInt(value ?? "10"),
q: prev.q, q: prev.q,
})) }))
} }
defaultValue="10" defaultValue="10"
> >
<SelectTrigger className="w-fit p-4 gap-4"> <SelectTrigger className="w-fit p-4 gap-4">
<SelectValue placeholder="Per Page" /> <SelectValue placeholder="Per Page" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="5">5</SelectItem> <SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem> <SelectItem value="10">10</SelectItem>
<SelectItem value="50">50</SelectItem> <SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem> <SelectItem value="100">100</SelectItem>
<SelectItem value="500">500</SelectItem> <SelectItem value="500">500</SelectItem>
<SelectItem value="1000">1000</SelectItem> <SelectItem value="1000">1000</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<CustomPagination <CustomPagination
currentPage={filterOptions.page + 1} currentPage={filterOptions.page + 1}
totalPages={totalPages} totalPages={totalPages}

View File

@ -1,11 +1,11 @@
import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router"; import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router";
import { useDisclosure } from "@mantine/hooks";
import AppHeader from "../components/AppHeader"; import AppHeader from "../components/AppHeader";
import AppNavbar from "../components/AppNavbar"; import AppNavbar from "../components/AppNavbar";
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import fetchRPC from "@/utils/fetchRPC"; import fetchRPC from "@/utils/fetchRPC";
import client from "@/honoClient"; import client from "@/honoClient";
import { useState } from "react";
export const Route = createFileRoute("/_dashboardLayout")({ export const Route = createFileRoute("/_dashboardLayout")({
component: DashboardLayout, component: DashboardLayout,
@ -38,7 +38,10 @@ function DashboardLayout() {
enabled: isAuthenticated, enabled: isAuthenticated,
}); });
const [openNavbar, { toggle }] = useDisclosure(false); const [openNavbar, setNavbarOpen] = useState(true);
const toggle = () => {
setNavbarOpen(!openNavbar);
};
return isAuthenticated ? ( return isAuthenticated ? (
<div className="flex flex-col h-screen"> <div className="flex flex-col h-screen">
@ -51,9 +54,7 @@ function DashboardLayout() {
<AppNavbar /> <AppNavbar />
{/* Main Content */} {/* Main Content */}
<main className={`flex-1 mt-[60px] p-6 bg-white overflow-auto ${ <main className={"flex-1 mt-16 p-6 bg-white overflow-auto"}>
openNavbar ? 'lg:ml-64' : 'lg:ml-0'
}`}>
<Outlet /> <Outlet />
</main> </main>
</div> </div>