feat(backoffice): Backoffice for Hackathon - user management page (#67)
* feat(backoffice): create a boilerplate page for Hackathon dashboard - Create an empty page for Hackathon dashboard - Comment out and hide the existing backoffice sidebar * feat(backoffice): Create a nested/dropdown sidebar list - Create a dropdown sidebar list - Show back the old navigation and group them - Make the sidebar responsive for mobile view * test datatable with mock data * base UI for Hackathon backoffice TODO: - Organize table schema for users, teams, and submissions management - Create API Contract for additional back-end endpoint * feat(hackathon): draft data table column & API Contract * update endpoint * feat(backoffice): update page hackathon user management - update data table component - update filtering & pagination - add modal display to edit and add user - hide notification icon in backoffice wrapper * feat(backoffice): add API contract for hackathon users * feat(backoffice): little adjustment in hackathon users management UI and API contract
This commit is contained in:
@@ -3,64 +3,257 @@ import {
|
||||
AuditOutlined,
|
||||
BookOutlined,
|
||||
CommentOutlined,
|
||||
DownOutlined,
|
||||
InboxOutlined,
|
||||
LogoutOutlined,
|
||||
ReadOutlined,
|
||||
ReloadOutlined,
|
||||
RightOutlined,
|
||||
ScheduleOutlined,
|
||||
SettingOutlined,
|
||||
StockOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UserOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button } from '../../atoms';
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn, For, useSession } from '@imphnen-frontend-service/utils';
|
||||
|
||||
const MENUS = [
|
||||
{ label: 'Dashboard & Set Gacha', href: '/dashboard', icon: <AppstoreOutlined className="text-[20px]" /> },
|
||||
{ label: 'Dashboard - Dimentorin', href: '/dashboard-dimentorin', icon: <AppstoreOutlined className="text-[20px]" /> },
|
||||
{ label: 'Gacha Roll', href: '/gacha-roll', icon: <ReloadOutlined className="text-[20px]" /> },
|
||||
{ label: 'Permissions', href: '/permissions', icon: <UserSwitchOutlined className="text-[20px]" /> },
|
||||
{ label: 'Roles', href: '/roles', icon: <UsergroupAddOutlined className="text-[20px]" /> },
|
||||
{ label: 'Data Akun', href: '/accounts', icon: <UserOutlined className="text-[20px]" /> },
|
||||
{ label: 'Validasi Transaksi', href: '/transactions', icon: <AuditOutlined className="text-[20px]" /> },
|
||||
{ label: 'Data Pengiriman Hadiah', href: '/prizes', icon: <InboxOutlined className="text-[20px]" /> },
|
||||
{ label: 'User - Dimentorin', href: '/users-dimentorin', icon: <UserSwitchOutlined className="text-[20px]" /> },
|
||||
{ label: 'Session - Dimentorin', href: '/session-dimentorin', icon: <ScheduleOutlined className="text-[20px]" /> },
|
||||
{ label: 'Content & Roadmap', href: '/roadmap-dimentorin', icon: <BookOutlined className="text-[20px]" /> },
|
||||
{ label: 'Feedback & Review', href: '/feedback-review-dimentorin', icon: <CommentOutlined className="text-[20px]" /> },
|
||||
{ label: 'Settings - Dimentorin', href: '/settings-dimentorin', icon: <SettingOutlined className="text-[20px]" /> },
|
||||
]
|
||||
type MenuItem = {
|
||||
label: string;
|
||||
href?: string;
|
||||
icon?: ReactElement;
|
||||
children?: Array<{ label: string; href: string; icon?: ReactElement }>;
|
||||
};
|
||||
|
||||
export const BackofficeSidebar: FC = (): ReactElement => {
|
||||
const MENUS: MenuItem[] = [
|
||||
{
|
||||
label: 'Hackathon',
|
||||
icon: <StockOutlined className="text-p3" />,
|
||||
children: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/hackathon-dashboard',
|
||||
icon: <AppstoreOutlined className="text-p3" />,
|
||||
},
|
||||
{
|
||||
label: 'Users',
|
||||
href: '/hackathon-users',
|
||||
icon: <UserOutlined className="text-p3" />,
|
||||
},
|
||||
{
|
||||
label: 'Teams',
|
||||
href: '/hackathon-teams',
|
||||
icon: <UsergroupAddOutlined className="text-p3" />,
|
||||
},
|
||||
{
|
||||
label: 'Submissions',
|
||||
href: '/hackathon-submissions',
|
||||
icon: <AuditOutlined className="text-p3" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Dimentorin',
|
||||
icon: <ReadOutlined className="text-p3" />,
|
||||
children: [
|
||||
{
|
||||
label: 'Dashboard - Dimentorin',
|
||||
href: '/dashboard-dimentorin',
|
||||
icon: <AppstoreOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'User - Dimentorin',
|
||||
href: '/users-dimentorin',
|
||||
icon: <UserSwitchOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Session - Dimentorin',
|
||||
href: '/session-dimentorin',
|
||||
icon: <ScheduleOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Content & Roadmap',
|
||||
href: '/roadmap-dimentorin',
|
||||
icon: <BookOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Feedback & Review',
|
||||
href: '/feedback-review-dimentorin',
|
||||
icon: <CommentOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Settings - Dimentorin',
|
||||
href: '/settings-dimentorin',
|
||||
icon: <SettingOutlined className="text-[20px]" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Gacha',
|
||||
icon: <ReloadOutlined className="text-[20px]" />,
|
||||
children: [
|
||||
{
|
||||
label: 'Dashboard & Set Gacha',
|
||||
href: '/dashboard',
|
||||
icon: <AppstoreOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Gacha Roll',
|
||||
href: '/gacha-roll',
|
||||
icon: <ReloadOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Validasi Transaksi',
|
||||
href: '/transactions',
|
||||
icon: <AuditOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Data Pengiriman Hadiah',
|
||||
href: '/prizes',
|
||||
icon: <InboxOutlined className="text-[20px]" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Permissions',
|
||||
href: '/permissions',
|
||||
icon: <UserSwitchOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Roles',
|
||||
href: '/roles',
|
||||
icon: <UsergroupAddOutlined className="text-[20px]" />,
|
||||
},
|
||||
{
|
||||
label: 'Data Akun',
|
||||
href: '/accounts',
|
||||
icon: <UserOutlined className="text-[20px]" />,
|
||||
},
|
||||
];
|
||||
|
||||
interface SidebarProps {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const BackofficeSidebar: FC<SidebarProps> = ({
|
||||
isOpen = false,
|
||||
onClose,
|
||||
}): ReactElement => {
|
||||
const { signOut } = useSession();
|
||||
const location = useLocation();
|
||||
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
|
||||
const isActive = (path: string) => {
|
||||
if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin') return false
|
||||
return location.pathname.includes(path)
|
||||
if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin')
|
||||
return false;
|
||||
return location.pathname.includes(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="sticky top-0 left-0 w-[280px] bg-white h-svh py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
|
||||
<div className="flex flex-col gap-20 justify-between items-center">
|
||||
<img src="/logos/simple.svg" alt="IMPHNEN Logo" className="w-[150px]" />
|
||||
const toggleGroup = (groupLabel: string) => {
|
||||
setOpenGroups((prev) => ({ ...prev, [groupLabel]: !prev[groupLabel] }));
|
||||
};
|
||||
|
||||
const sidebarContent = (
|
||||
<div className="w-[280px] bg-white h-svh py-10 lg:py-[60px] px-7 shadow-xl flex flex-col justify-between">
|
||||
<div className="flex flex-col gap-10 lg:gap-20 justify-between items-center">
|
||||
<div className="flex justify-around lg:justify-center items-center w-full">
|
||||
<img
|
||||
src="/logos/simple.svg"
|
||||
alt="IMPHNEN Logo"
|
||||
className="w-[150px]"
|
||||
/>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
aria-label="Close sidebar"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col gap-4 w-full h-[calc(100svh-20rem)] overflow-y-auto">
|
||||
<For data={MENUS}>
|
||||
{({ label, href, icon }) => (
|
||||
<Link
|
||||
key={href}
|
||||
to={href}
|
||||
className={cn(
|
||||
"flex items-center justify-items-start gap-3 px-[8px] py-[10px]",
|
||||
isActive(href) ? "bg-primary-500 text-white rounded-md" : "text-gray-700 hover:bg-gray-100"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<span className="text-p3 font-medium">{label}</span>
|
||||
</Link>
|
||||
)}
|
||||
{(menu) =>
|
||||
menu.children && menu.children.length > 0 ? (
|
||||
<div key={menu.label} className="w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleGroup(menu.label)}
|
||||
className={cn(
|
||||
'flex items-center justify-between w-full gap-3 px-2 py-2.5 rounded-md cursor-pointer',
|
||||
openGroups[menu.label]
|
||||
? 'bg-primary-400 hover:bg-primary-500 text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{menu.icon}
|
||||
<span className="text-p3 font-medium">{menu.label}</span>
|
||||
</div>
|
||||
<span className="text-label2">
|
||||
{openGroups[menu.label] ? (
|
||||
<DownOutlined className="text-label1" />
|
||||
) : (
|
||||
<RightOutlined className="text-label1" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{openGroups[menu.label] && (
|
||||
<div className="mt-2 ml-6 flex flex-col gap-2">
|
||||
{menu.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
to={child.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-2 py-2.5 rounded-md',
|
||||
isActive(child.href)
|
||||
? 'bg-primary-100 text-primary-700 hover:bg-primary-200'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{child.icon}
|
||||
<span className="text-label1 font-medium">
|
||||
{child.label}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
key={menu.href ?? menu.label}
|
||||
to={menu.href ?? '#'}
|
||||
className={cn(
|
||||
'flex items-center justify-items-start gap-3 px-2 py-2.5',
|
||||
menu.href && isActive(menu.href)
|
||||
? 'bg-primary-500 text-white rounded-md'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{menu.icon}
|
||||
<span className="text-p3 font-medium">{menu.label}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</For>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -70,12 +263,36 @@ export const BackofficeSidebar: FC = (): ReactElement => {
|
||||
<Button
|
||||
onClick={signOut}
|
||||
variant="text"
|
||||
className="items-start justify-start gap-3 px-[8px] py-[10px] text-gray-700 hover:text-red-500 transition-colors w-full"
|
||||
className="items-start justify-start gap-3 px-2 py-2.5 text-gray-700 hover:text-red-500 transition-colors w-full"
|
||||
>
|
||||
<LogoutOutlined className="text-[20px]" />
|
||||
<LogoutOutlined className="text-p3" />
|
||||
<span className="text-p3 font-medium">Log Out</span>
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Sidebar - visible on lg+, sticky */}
|
||||
<div className="hidden lg:block sticky top-0 h-screen overflow-y-auto shadow">
|
||||
{sidebarContent}
|
||||
</div>
|
||||
|
||||
{/* Mobile Sidebar - overlay */}
|
||||
{isOpen && (
|
||||
<div className="lg:hidden fixed inset-0 z-50">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Sidebar */}
|
||||
<div className="fixed inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out">
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { Icon } from '@iconify/react'
|
||||
import { FC, ReactElement, ReactNode } from "react"
|
||||
import { Button } from "../../atoms"
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { FC, ReactElement, ReactNode } from 'react';
|
||||
import { Button } from '../../atoms';
|
||||
import { useAuthStore } from '@imphnen-frontend-service/service';
|
||||
|
||||
export type TBackofficeWrapperProps = {
|
||||
children: ReactNode
|
||||
title?: string
|
||||
className?: string
|
||||
classHeader?: string
|
||||
classTitle?: string
|
||||
}
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
className?: string;
|
||||
classHeader?: string;
|
||||
classTitle?: string;
|
||||
};
|
||||
|
||||
export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
||||
children,
|
||||
@@ -18,34 +19,52 @@ export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
||||
classHeader,
|
||||
classTitle,
|
||||
}): ReactElement => {
|
||||
const { session } = useAuthStore();
|
||||
const user = session?.user;
|
||||
|
||||
return (
|
||||
<main className={cn("w-full px-[48px] py-[40px] flex flex-col gap-8", className)}>
|
||||
<header className={cn("bg-white py-5 px-7 rounded-md shadow flex items-center justify-between", classHeader)}>
|
||||
<h1 className={cn("text-[19px] text-primary-500 font-semibold", classTitle)}>{title}</h1>
|
||||
<main
|
||||
className={cn(
|
||||
'w-full px-[48px] py-[40px] flex flex-col gap-8',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<header
|
||||
className={cn(
|
||||
'bg-white py-5 px-7 rounded-md shadow flex items-center justify-between',
|
||||
classHeader
|
||||
)}
|
||||
>
|
||||
<h1
|
||||
className={cn(
|
||||
'text-[19px] text-primary-500 font-semibold',
|
||||
classTitle
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-x-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="max-h-full p-3"
|
||||
>
|
||||
{/* <Button type="button" variant="secondary" className="max-h-full p-3">
|
||||
<Icon icon="mdi:bell-outline" className="size-6" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
<div className="flex items-center gap-x-6">
|
||||
<div className="text-neutral-600 font-medium">
|
||||
<p className="text-p3">Rizal Syaepulloh</p>
|
||||
<p className="text-label1">Super Admin</p>
|
||||
<p className="text-p3">{user?.fullname || 'Full Name'}</p>
|
||||
<p className="text-label1">Admin</p>
|
||||
</div>
|
||||
<div className="size-12 rounded-full overflow-hidden">
|
||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="size-full object-cover" />
|
||||
<img
|
||||
src={user?.avatar || '/images/asd687hwq6nds4dfjj2983.webp'}
|
||||
alt="Profile"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
{children}
|
||||
</section>
|
||||
<section>{children}</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,57 +1,113 @@
|
||||
import {
|
||||
PaginationState,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
flexRender,
|
||||
ColumnDef,
|
||||
Table,
|
||||
RowData,
|
||||
TableOptions,
|
||||
} from '@tanstack/react-table';
|
||||
import { Pagination } from '../../molecules';
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
|
||||
interface DataTableProps<T> {
|
||||
data: T[];
|
||||
columns: ColumnDef<T>[];
|
||||
table: Table<T>;
|
||||
interface DataTableProps<T extends RowData> {
|
||||
table?: Table<T>;
|
||||
data?: T[];
|
||||
columns?: ColumnDef<T, unknown>[];
|
||||
pageSize?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DataTable = <T extends RowData>({
|
||||
data,
|
||||
columns,
|
||||
table,
|
||||
data = [],
|
||||
columns = [],
|
||||
pageSize = 9,
|
||||
className,
|
||||
}: DataTableProps<T>) => {
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize,
|
||||
});
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
});
|
||||
// Update pagination state when pageSize prop changes
|
||||
React.useEffect(() => {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
pageSize,
|
||||
}));
|
||||
}, [pageSize]);
|
||||
|
||||
// Reset pagination when data changes to prevent out-of-bounds errors
|
||||
React.useEffect(() => {
|
||||
if (data.length > 0) {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
pageIndex: 0, // Reset to first page when data changes
|
||||
}));
|
||||
}
|
||||
}, [data.length]);
|
||||
|
||||
// Memoize data and columns to prevent unnecessary re-renders
|
||||
const memoizedData = React.useMemo(() => data, [data]);
|
||||
const memoizedColumns = React.useMemo(() => columns, [columns]);
|
||||
|
||||
// Memoize table configuration to prevent recreation on every render
|
||||
const tableConfig = React.useMemo(() => {
|
||||
const config: TableOptions<T> = {
|
||||
data: memoizedData,
|
||||
columns: memoizedColumns,
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
};
|
||||
|
||||
return config;
|
||||
}, [memoizedData, memoizedColumns, pagination, sorting]);
|
||||
|
||||
// Prefer external table instance if provided; otherwise create an internal one
|
||||
const internalTable = useReactTable(tableConfig);
|
||||
const t = table ?? internalTable;
|
||||
|
||||
// Handle empty data state
|
||||
const isEmpty = t.getRowModel().rows.length === 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className={cn('flex flex-col gap-8', className)}>
|
||||
<div className="w-full overflow-x-auto">
|
||||
<table className="w-full min-w-full text-base">
|
||||
<thead className="bg-primary-50 mb-3 text-left text-nowrap">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
{t.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
className={cn("py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg", header?.column?.columnDef?.meta?.headerClassName)}
|
||||
onClick={
|
||||
header.column.getCanSort()
|
||||
? header.column.getToggleSortingHandler()
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg',
|
||||
header.column.getCanSort() &&
|
||||
'cursor-pointer select-none hover:bg-primary-100 transition-colors',
|
||||
header?.column?.columnDef?.meta?.headerClassName
|
||||
)}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
@@ -59,28 +115,58 @@ export const DataTable = <T extends RowData>({
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{header.column.getCanSort() && (
|
||||
<span className="ml-2 text-xs text-gray-500">
|
||||
{header.column.getIsSorted() === 'asc' && '▲'}
|
||||
{header.column.getIsSorted() === 'desc' && '▼'}
|
||||
{!header.column.getIsSorted() && <span>⇅</span>}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className="bg-primary-100 odd:bg-white">
|
||||
{row.getVisibleCells().map((cell, index) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={cn("py-3 px-5 first:rounded-l-lg last:rounded-r-lg", cell?.column?.columnDef?.meta?.cellClassName)}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
{isEmpty ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={t.getAllColumns().length}
|
||||
className="py-8 px-5 text-center text-neutral-500"
|
||||
>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
) : (
|
||||
t.getRowModel().rows.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={cn(
|
||||
'hover:bg-primary-50 transition-colors',
|
||||
rowIndex % 2 === 0 ? 'bg-white' : 'bg-primary-100'
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={cn(
|
||||
'py-3 px-5 first:rounded-l-lg last:rounded-r-lg',
|
||||
cell?.column?.columnDef?.meta?.cellClassName
|
||||
)}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination table={table} />
|
||||
<Pagination table={t} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user