mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-21 05:45:46 +00:00
init
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import react, { JSX } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Color } from '@/types/theme';
|
||||
|
||||
interface ButtonProps extends react.ComponentProps<'button'> {
|
||||
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
||||
color?: Color;
|
||||
href?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
type,
|
||||
href,
|
||||
variant,
|
||||
color,
|
||||
isLoading,
|
||||
className,
|
||||
disabled,
|
||||
onClick,
|
||||
}: ButtonProps) => {
|
||||
const btnBaseClassName = cn(
|
||||
'btn',
|
||||
{
|
||||
'btn-soft': variant === 'soft',
|
||||
'btn-outline': variant === 'outline',
|
||||
'btn-dash': variant === 'dash',
|
||||
'btn-ghost': variant === 'ghost',
|
||||
'btn-link': variant === 'link',
|
||||
'btn-active': variant === 'active',
|
||||
|
||||
'btn-primary': color === 'primary',
|
||||
'btn-secondary': color === 'secondary',
|
||||
'btn-accent': color === 'accent',
|
||||
'btn-neutral': color === 'neutral',
|
||||
'btn-info': color === 'info',
|
||||
'btn-success': color === 'success',
|
||||
'btn-warning': color === 'warning',
|
||||
'btn-error': color === 'error',
|
||||
},
|
||||
'h-fit justify-center items-center gap-2 rounded-xl p-2 text-base transition-all'
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!href && (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
btnBaseClassName,
|
||||
'disabled:pointer-events-auto! disabled:cursor-not-allowed!',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!isLoading && children}
|
||||
{isLoading && <span className='loading loading-dots loading-md' />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{href && (
|
||||
<Link
|
||||
href={disabled ? '#' : href}
|
||||
aria-disabled={disabled}
|
||||
className={cn(
|
||||
btnBaseClassName,
|
||||
{ 'pointer-events-auto cursor-not-allowed': disabled },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!isLoading && children}
|
||||
{isLoading && <span className='loading loading-dots loading-xl' />}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
interface DrawerProps {
|
||||
children?: ReactNode;
|
||||
sidebarContent?: ReactNode;
|
||||
open: boolean;
|
||||
setOpen: (newOpenState: boolean) => void;
|
||||
openOnLarge?: boolean;
|
||||
}
|
||||
|
||||
const Drawer = ({
|
||||
children,
|
||||
sidebarContent,
|
||||
open,
|
||||
setOpen,
|
||||
openOnLarge,
|
||||
}: DrawerProps) => {
|
||||
const toggleDrawer = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('drawer', {
|
||||
'lg:drawer-open': openOnLarge,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={open}
|
||||
onChange={toggleDrawer}
|
||||
className='drawer-toggle'
|
||||
/>
|
||||
|
||||
<div className='drawer-content'>{children}</div>
|
||||
|
||||
<div className='drawer-side border-r border-solid border-gray-200 z-20'>
|
||||
<label
|
||||
aria-label='close sidebar'
|
||||
className='drawer-overlay'
|
||||
onClick={closeDrawer}
|
||||
/>
|
||||
|
||||
<div className='min-h-full w-full max-w-[300px] lg:w-[300px] bg-base-100'>
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface NavbarProps {
|
||||
title: string;
|
||||
toggleSidebar?: () => void;
|
||||
}
|
||||
|
||||
const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
return (
|
||||
<div className='navbar px-4 bg-base-100 shadow-sm'>
|
||||
<div className='flex-1'>
|
||||
<div className='flex flex-row items-center gap-4'>
|
||||
{toggleSidebar && (
|
||||
<Button onClick={toggleSidebar} className='block lg:hidden'>
|
||||
<Icon
|
||||
icon='material-symbols:menu-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<span className='font-bold text-xl text-primary'>{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
className='btn btn-ghost btn-circle avatar'
|
||||
>
|
||||
<div className='w-10 rounded-full border grid place-items-center'>
|
||||
<Icon icon='uil:user' width={40} height={40} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Menu className='dropdown-content w-52 mt-3 p-2 bg-base-100 shadow rounded-box menu-sm'>
|
||||
<MenuItem title='Settings' href='#' />
|
||||
<MenuItem title='Logout' href='#' />
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,324 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
const range = (start: number, end: number) =>
|
||||
Array.from({ length: end - start + 1 }, (_, i) => i + start);
|
||||
|
||||
const PaginationButton = ({
|
||||
content = '',
|
||||
disabled = false,
|
||||
onClick = () => {},
|
||||
}: {
|
||||
content?: ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<button
|
||||
className={cn(
|
||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square',
|
||||
'disabled:text-gray-700 disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-gray-50 disabled:active:translate-y-0'
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
|
||||
const EtcPaginationButton = ({
|
||||
startPage = 0,
|
||||
endPage = 0,
|
||||
onPageItemClick = (pageNumber: number) => {},
|
||||
}) => {
|
||||
const pages = range(startPage, endPage);
|
||||
|
||||
return (
|
||||
<>
|
||||
{startPage > 0 && endPage >= startPage && (
|
||||
<div className='dropdown dropdown-top dropdown-center'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
className={cn(
|
||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
|
||||
)}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
|
||||
<div className='dropdown-content'>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='menu bg-base-100 rounded-lg z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
|
||||
>
|
||||
{pages.map((pageNumber) => (
|
||||
<li key={pageNumber}>
|
||||
<PaginationButton
|
||||
content={pageNumber}
|
||||
onClick={() => onPageItemClick(pageNumber)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(startPage === 0 || endPage < startPage) && (
|
||||
<button
|
||||
disabled
|
||||
className={cn(
|
||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
|
||||
)}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Pagination = ({
|
||||
currentPage = 1,
|
||||
totalItems = 0,
|
||||
itemsPerPage = 10,
|
||||
onPageChange = (pageNumber: number) => {},
|
||||
onPrevPage = () => {},
|
||||
onNextPage = () => {},
|
||||
}) => {
|
||||
const totalPages =
|
||||
Math.ceil(totalItems / itemsPerPage) === 0
|
||||
? 1
|
||||
: Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='join w-full justify-between items-center gap-3'>
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={onPrevPage}
|
||||
className={cn(
|
||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
|
||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon='uil:arrow-left'
|
||||
width={20}
|
||||
height={20}
|
||||
className='text-gray-400 group-disabled:text-gray-300'
|
||||
/>{' '}
|
||||
Previous
|
||||
</button>
|
||||
|
||||
{totalPages <= 7 && (
|
||||
<div className='join-item join gap-0.5'>
|
||||
{range(1, totalPages).map((pageNumber) => (
|
||||
<PaginationButton
|
||||
key={pageNumber}
|
||||
content={pageNumber}
|
||||
disabled={currentPage === pageNumber}
|
||||
onClick={() => pageChangeHandler(pageNumber)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalPages > 7 && (
|
||||
<div className='join-item join gap-0.5'>
|
||||
<PaginationButton
|
||||
content={1}
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => pageChangeHandler(1)}
|
||||
/>
|
||||
|
||||
{totalPages >= 2 &&
|
||||
(currentPage <= 3 || currentPage >= totalPages - 2) && (
|
||||
<PaginationButton
|
||||
content={2}
|
||||
disabled={currentPage === 2}
|
||||
onClick={() => pageChangeHandler(2)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 2 &&
|
||||
currentPage > 3 &&
|
||||
currentPage < totalPages - 2 && (
|
||||
<EtcPaginationButton
|
||||
startPage={2}
|
||||
endPage={currentPage - 2}
|
||||
onPageItemClick={pageChangeHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 3 &&
|
||||
(currentPage <= 4 || currentPage >= totalPages - 2) &&
|
||||
currentPage !== totalPages - 2 && (
|
||||
<PaginationButton
|
||||
content={3}
|
||||
disabled={currentPage === 3}
|
||||
onClick={() => pageChangeHandler(3)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 7 &&
|
||||
(currentPage <= 2 || currentPage >= totalPages - 2) && (
|
||||
<EtcPaginationButton
|
||||
startPage={
|
||||
currentPage <= 2
|
||||
? currentPage + 2
|
||||
: currentPage === totalPages - 2
|
||||
? 3
|
||||
: currentPage >= totalPages - 1
|
||||
? 4
|
||||
: 1
|
||||
}
|
||||
endPage={
|
||||
currentPage <= 2 || currentPage >= totalPages - 1
|
||||
? totalPages - 3
|
||||
: currentPage === totalPages - 2
|
||||
? totalPages - 4
|
||||
: 2
|
||||
}
|
||||
onPageItemClick={pageChangeHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 3 &&
|
||||
currentPage > 4 &&
|
||||
currentPage < totalPages - 1 && (
|
||||
<PaginationButton
|
||||
content={currentPage - 1}
|
||||
onClick={() => pageChangeHandler(currentPage - 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 7 &&
|
||||
currentPage > 3 &&
|
||||
currentPage < totalPages - 2 && (
|
||||
<PaginationButton content={currentPage} disabled />
|
||||
)}
|
||||
|
||||
{totalPages >= 5 &&
|
||||
currentPage > 2 &&
|
||||
currentPage < totalPages - 2 && (
|
||||
<PaginationButton
|
||||
content={currentPage + 1}
|
||||
onClick={() => pageChangeHandler(currentPage + 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 5 &&
|
||||
(currentPage <= 2 || currentPage >= totalPages - 2) && (
|
||||
<PaginationButton
|
||||
content={totalPages - 2}
|
||||
disabled={currentPage === totalPages - 2}
|
||||
onClick={() => pageChangeHandler(totalPages - 2)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 6 &&
|
||||
currentPage > 2 &&
|
||||
currentPage < totalPages - 3 && (
|
||||
<EtcPaginationButton
|
||||
startPage={
|
||||
currentPage <= 3
|
||||
? currentPage + 2
|
||||
: currentPage >= 4
|
||||
? currentPage + 2
|
||||
: 1
|
||||
}
|
||||
endPage={
|
||||
currentPage <= 3
|
||||
? totalPages - 2
|
||||
: currentPage >= 4
|
||||
? totalPages - 1
|
||||
: 0
|
||||
}
|
||||
onPageItemClick={pageChangeHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 6 &&
|
||||
(currentPage <= 3 || currentPage >= totalPages - 3) && (
|
||||
<PaginationButton
|
||||
content={totalPages - 1}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
onClick={() => pageChangeHandler(totalPages - 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalPages >= 7 && (
|
||||
<PaginationButton
|
||||
content={totalPages}
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => pageChangeHandler(totalPages)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={onNextPage}
|
||||
className={cn(
|
||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
|
||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||
)}
|
||||
>
|
||||
Next{' '}
|
||||
<Icon
|
||||
icon='uil:arrow-right'
|
||||
width={20}
|
||||
height={20}
|
||||
className='text-gray-400 group-disabled:text-gray-300'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2 mt-2 sm:hidden'>
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={onPrevPage}
|
||||
className={cn(
|
||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
|
||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon='uil:arrow-left'
|
||||
width={20}
|
||||
height={20}
|
||||
className='text-gray-400 group-disabled:text-gray-300'
|
||||
/>{' '}
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<button
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={onNextPage}
|
||||
className={cn(
|
||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
|
||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||
)}
|
||||
>
|
||||
Next{' '}
|
||||
<Icon
|
||||
icon='uil:arrow-right'
|
||||
width={20}
|
||||
height={20}
|
||||
className='text-gray-400 group-disabled:text-gray-300'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
TableOptions,
|
||||
useReactTable,
|
||||
ColumnDef,
|
||||
} from '@tanstack/react-table';
|
||||
import { rankItem } from '@tanstack/match-sorter-utils';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
import Pagination from '@/components/Pagination';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
interface TableClassNames {
|
||||
containerClassName?: string;
|
||||
tableWrapperClassName?: string;
|
||||
tableClassName?: string;
|
||||
tableHeaderClassName?: string;
|
||||
headerRowClassName?: string;
|
||||
headerColumnClassName?: string;
|
||||
tableBodyClassName?: string;
|
||||
bodyRowClassName?: string;
|
||||
bodyColumnClassName?: string;
|
||||
paginationClassName?: string;
|
||||
}
|
||||
|
||||
// Type for the Table component props
|
||||
interface TableProps<TData extends object> {
|
||||
data: TData[];
|
||||
columns: ColumnDef<TData, any>[];
|
||||
pageSize?: number;
|
||||
isLoading?: boolean;
|
||||
fuzzySearchValue?: string | null;
|
||||
onFuzzySearchValueChange?: (value: string) => void;
|
||||
className?: TableClassNames;
|
||||
}
|
||||
|
||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||
|
||||
const fuzzyFilter = (
|
||||
row: any,
|
||||
columnId: string,
|
||||
value: string,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
addMeta({ itemRank });
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
const Table = <TData extends object>({
|
||||
data = [],
|
||||
columns = [],
|
||||
pageSize = 10,
|
||||
isLoading = false,
|
||||
fuzzySearchValue = null,
|
||||
onFuzzySearchValueChange = () => {},
|
||||
className = {
|
||||
containerClassName: '',
|
||||
tableWrapperClassName: '',
|
||||
tableClassName: '',
|
||||
tableHeaderClassName: '',
|
||||
headerRowClassName: '',
|
||||
headerColumnClassName: '',
|
||||
tableBodyClassName: '',
|
||||
bodyRowClassName: '',
|
||||
bodyColumnClassName: '',
|
||||
paginationClassName: '',
|
||||
},
|
||||
}: TableProps<TData>) => {
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: pageSize,
|
||||
});
|
||||
|
||||
const tableOptions: TableOptions<TData> = {
|
||||
columns,
|
||||
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
pagination,
|
||||
globalFilter: fuzzySearchValue,
|
||||
},
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
globalFilterFn: fuzzyFilter,
|
||||
};
|
||||
|
||||
if (fuzzySearchValue !== null) {
|
||||
tableOptions.onGlobalFilterChange = onFuzzySearchValueChange;
|
||||
tableOptions.getFilteredRowModel = getFilteredRowModel();
|
||||
}
|
||||
|
||||
const table = useReactTable(tableOptions);
|
||||
|
||||
return (
|
||||
<div className={className.containerClassName}>
|
||||
<div className={className.tableWrapperClassName}>
|
||||
<table className={className.tableClassName}>
|
||||
<thead className={className.tableHeaderClassName}>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id} className={className.headerRowClassName}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
className={cn(
|
||||
header.column.getCanSort()
|
||||
? 'cursor-pointer select-none'
|
||||
: '',
|
||||
className.headerColumnClassName
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-1'>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
|
||||
{header.column.getCanSort() && (
|
||||
<div className='flex items-center'>
|
||||
<Icon
|
||||
icon='lucide:arrow-up'
|
||||
width={12}
|
||||
height={12}
|
||||
className={cn(
|
||||
'transition-all ease-in-out duration-200',
|
||||
header.column.getIsSorted() === 'asc'
|
||||
? 'text-black'
|
||||
: 'text-black/30'
|
||||
)}
|
||||
/>
|
||||
<Icon
|
||||
icon='lucide:arrow-down'
|
||||
width={12}
|
||||
height={12}
|
||||
className={cn(
|
||||
'transition-all ease-in-out duration-200',
|
||||
header.column.getIsSorted() === 'desc'
|
||||
? 'text-black'
|
||||
: 'text-black/30'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
|
||||
<tbody className={className.tableBodyClassName}>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className={className.bodyRowClassName}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className={className.bodyColumnClassName}>
|
||||
{!isLoading &&
|
||||
flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
||||
{isLoading && <div className='skeleton w-full h-4' />}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{data.length > 0 && !isLoading && (
|
||||
<div className={cn('mt-5', className.paginationClassName)}>
|
||||
<Pagination
|
||||
totalItems={table.getRowCount()}
|
||||
itemsPerPage={table.getState().pagination.pageSize}
|
||||
currentPage={table.getState().pagination.pageIndex + 1}
|
||||
onPrevPage={() => table.previousPage()}
|
||||
onNextPage={() => table.nextPage()}
|
||||
onPageChange={(pageNumber) =>
|
||||
table.setPageIndex(pageNumber ? pageNumber - 1 : 0)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Ref } from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
import { TextInputProps } from '@/components/input/TextInput';
|
||||
|
||||
interface FileInputProps
|
||||
extends Omit<
|
||||
TextInputProps,
|
||||
| 'type'
|
||||
| 'value'
|
||||
| 'isValid'
|
||||
| 'startAdornment'
|
||||
| 'endAdornment'
|
||||
| 'isLoading'
|
||||
> {
|
||||
ref?: Ref<HTMLInputElement>;
|
||||
accept?: string;
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
label?: string;
|
||||
input?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const FileInput = ({
|
||||
ref,
|
||||
label,
|
||||
bottomLabel,
|
||||
name,
|
||||
placeholder,
|
||||
accept = '*',
|
||||
className,
|
||||
isError,
|
||||
errorMessage,
|
||||
disabled = false,
|
||||
onChange,
|
||||
onBlur,
|
||||
readOnly = false,
|
||||
}: FileInputProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex flex-col gap-2 text-start',
|
||||
className?.wrapper
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={cn(
|
||||
'w-full text-sm font-normal leading-5',
|
||||
{
|
||||
'text-error': isError,
|
||||
},
|
||||
className?.label
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={ref}
|
||||
type='file'
|
||||
accept={accept}
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'grow file-input w-full h-12 rounded-lg!',
|
||||
className?.input
|
||||
)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
{bottomLabel && (
|
||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||
)}
|
||||
|
||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileInput;
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface PasswordInputProps
|
||||
extends Omit<TextInputProps, 'type' | 'startAdornment' | 'endAdornment'> {}
|
||||
|
||||
const PasswordInput = (props: PasswordInputProps) => {
|
||||
const [type, setType] = useState('password');
|
||||
|
||||
const showPasswordHandler = () => {
|
||||
setType((prevType) => {
|
||||
if (prevType === 'password') return 'text';
|
||||
|
||||
return 'password';
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type={type}
|
||||
endAdornment={
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
type='button'
|
||||
variant='ghost'
|
||||
onClick={showPasswordHandler}
|
||||
className='btn btn-ghost w-fit h-fit p-2 rounded-full'
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<Icon
|
||||
icon={type === 'password' ? 'mdi:eye' : 'mdi:eye-off'}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import { ComponentType, ReactNode, useMemo } from 'react';
|
||||
import Select, { OptionProps, GroupBase } from 'react-select';
|
||||
import makeAnimated from 'react-select/animated';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export interface OptionType {
|
||||
value: string | number;
|
||||
label: string;
|
||||
className?: string; // for multi select
|
||||
labelClassName?: string; // for multi select
|
||||
}
|
||||
|
||||
export type OptionComponent<T = OptionType> = ComponentType<
|
||||
OptionProps<T, boolean, GroupBase<T>>
|
||||
>;
|
||||
|
||||
interface SelectInputProps<T = OptionType> {
|
||||
label?: ReactNode;
|
||||
bottomLabel?: ReactNode;
|
||||
value?: T | T[];
|
||||
onChange?: (val: T | T[] | null) => void;
|
||||
options: T[];
|
||||
optionComponent?: OptionComponent<T>;
|
||||
isDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
isClearable?: boolean;
|
||||
isRtl?: boolean;
|
||||
isSearchable?: boolean;
|
||||
isMulti?: boolean;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
label?: string;
|
||||
select?: string;
|
||||
};
|
||||
isError?: boolean;
|
||||
errorMessage?: string;
|
||||
isAnimated?: boolean;
|
||||
openMenu?: boolean;
|
||||
}
|
||||
|
||||
const animatedComponents = makeAnimated();
|
||||
|
||||
const SelectInput = <T extends OptionType>({
|
||||
label,
|
||||
bottomLabel,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
optionComponent,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isClearable,
|
||||
isRtl,
|
||||
isSearchable = true,
|
||||
isMulti,
|
||||
placeholder,
|
||||
required,
|
||||
className,
|
||||
isError,
|
||||
errorMessage,
|
||||
isAnimated = true,
|
||||
openMenu,
|
||||
}: SelectInputProps) => {
|
||||
const components = useMemo(() => {
|
||||
const base = isAnimated ? animatedComponents : {};
|
||||
|
||||
return {
|
||||
...base,
|
||||
IndicatorSeparator: () => null,
|
||||
};
|
||||
}, [isAnimated]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex flex-col gap-2 text-start',
|
||||
className?.wrapper
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
<span
|
||||
className={cn(
|
||||
'w-full text-sm font-normal leading-5',
|
||||
{
|
||||
'text-error': isError,
|
||||
},
|
||||
className?.label
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{required && (
|
||||
<>
|
||||
{' '}
|
||||
<span className='tooltip tooltip-error' data-tip='required'>
|
||||
<span className='text-error'> *</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Select
|
||||
instanceId='select'
|
||||
value={value}
|
||||
onChange={(val) => onChange?.(val as T)}
|
||||
options={options}
|
||||
menuIsOpen={openMenu}
|
||||
isMulti={isMulti}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
isRtl={isRtl}
|
||||
isSearchable={isSearchable}
|
||||
placeholder={placeholder}
|
||||
className={cn('w-full', className)}
|
||||
classNames={{
|
||||
control: ({ isFocused, isDisabled }) =>
|
||||
cn(
|
||||
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!',
|
||||
{
|
||||
'border-red-500! focus-within:border-red-500 focus-within:ring-2 focus-within:ring-red-200':
|
||||
isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
||||
'border-gray-300': !isError && !isFocused,
|
||||
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
|
||||
}
|
||||
),
|
||||
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
|
||||
placeholder: () =>
|
||||
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
|
||||
singleValue: () =>
|
||||
cn({ 'text-gray-900': !isError, 'text-error!': isError }),
|
||||
input: () => cn('text-gray-900'),
|
||||
indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
|
||||
indicatorSeparator: () => cn('mx-1 h-4 w-px bg-gray-200'),
|
||||
clearIndicator: () => cn('p-1 rounded-md hover:bg-gray-100'),
|
||||
dropdownIndicator: ({ isFocused }) =>
|
||||
cn('p-1 rounded-md hover:bg-gray-100', {
|
||||
'text-gray-900': isFocused,
|
||||
'text-gray-500': !isFocused,
|
||||
'text-error!': isError,
|
||||
}),
|
||||
menu: () =>
|
||||
cn(
|
||||
'border border-gray-200 rounded-lg bg-white shadow-lg rounded-lg!'
|
||||
),
|
||||
menuList: () => cn('p-2! max-h-60 overflow-auto'),
|
||||
groupHeading: () =>
|
||||
cn('ml-2 mt-2 mb-1 text-xs font-medium text-gray-500'),
|
||||
option: ({ isFocused, isSelected, isDisabled }) =>
|
||||
cn('mt-1 px-3 py-2 rounded-md cursor-pointer! select-none', {
|
||||
'text-gray-300': isDisabled,
|
||||
'bg-indigo-600 text-white': isFocused,
|
||||
'text-gray-700': !isDisabled && !isFocused,
|
||||
'active:bg-indigo-50': !isDisabled,
|
||||
'bg-blue-500!': isSelected,
|
||||
}),
|
||||
noOptionsMessage: () => cn('px-3 py-2 text-gray-500'),
|
||||
loadingMessage: () => cn('px-3 py-2 text-gray-500'),
|
||||
multiValue: ({ getValue, index }) => {
|
||||
const selectedValues = getValue();
|
||||
|
||||
return cn(
|
||||
'bg-indigo-50 rounded-md py-0.5 pl-2 pr-1 flex items-center gap-1 rounded-md!',
|
||||
selectedValues[index]?.className
|
||||
);
|
||||
},
|
||||
multiValueLabel: ({ getValue, index }) => {
|
||||
const selectedValues = getValue();
|
||||
|
||||
return cn('text-indigo-700', selectedValues[index]?.labelClassName);
|
||||
},
|
||||
multiValueRemove: () =>
|
||||
cn('p-1 rounded-sm! hover:bg-indigo-100 hover:text-indigo-800'),
|
||||
}}
|
||||
components={{
|
||||
...components,
|
||||
...(optionComponent ? { Option: optionComponent } : {}),
|
||||
}}
|
||||
// make the menu float above modals/etc.
|
||||
menuPortalTarget={
|
||||
typeof document !== 'undefined' ? document.body : undefined
|
||||
}
|
||||
styles={{
|
||||
// Tailwind can't set inline z-index on a portal; use styles here:
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
}}
|
||||
/>
|
||||
|
||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||
{!isError && bottomLabel && (
|
||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectInput;
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FocusEventHandler,
|
||||
HTMLInputTypeAttribute,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export interface TextInputProps {
|
||||
type?: HTMLInputTypeAttribute;
|
||||
label?: string;
|
||||
bottomLabel?: string;
|
||||
name: string;
|
||||
value?: string | number;
|
||||
placeholder?: string;
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
label?: string;
|
||||
inputWrapper?: string;
|
||||
input?: string;
|
||||
};
|
||||
isError?: boolean;
|
||||
isValid?: boolean;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
isLoading?: boolean;
|
||||
errorMessage?: string;
|
||||
startAdornment?: ReactNode;
|
||||
endAdornment?: ReactNode;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const TextInput = ({
|
||||
type = 'text',
|
||||
label,
|
||||
bottomLabel,
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
className,
|
||||
isError,
|
||||
isValid,
|
||||
errorMessage,
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
disabled = false,
|
||||
required = false,
|
||||
onChange,
|
||||
onBlur,
|
||||
readOnly = false,
|
||||
isLoading = false,
|
||||
}: TextInputProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex flex-col gap-2 text-start',
|
||||
className?.wrapper
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={cn(
|
||||
'w-full text-sm font-normal leading-5',
|
||||
{
|
||||
'text-error': isError,
|
||||
},
|
||||
className?.label
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{required && (
|
||||
<>
|
||||
{' '}
|
||||
<span className='tooltip tooltip-error' data-tip='required'>
|
||||
<span className='text-error'> *</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
>
|
||||
{startAdornment && startAdornment}
|
||||
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
className={cn('grow', className?.input)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
{(isLoading || endAdornment) && (
|
||||
<div className='flex flex-row gap-2'>
|
||||
{isLoading && <span className='loading loading-spinner' />}
|
||||
|
||||
{endAdornment && endAdornment}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isError && bottomLabel && (
|
||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||
)}
|
||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInput;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
interface MenuProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Menu = ({ children, className }: MenuProps) => {
|
||||
return (
|
||||
<ul className={cn('menu w-full p-0 gap-0.5', className)}>{children}</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,64 @@
|
||||
import Link from 'next/link';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
interface MenuItemProps {
|
||||
title: string;
|
||||
href?: string;
|
||||
icon?: string;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MenuItem = ({
|
||||
title,
|
||||
href,
|
||||
icon,
|
||||
active = false,
|
||||
className,
|
||||
onClick,
|
||||
}: MenuItemProps) => {
|
||||
const menuItemBaseClassName = cn(
|
||||
'group px-3 py-2 text-base text-black font-semibold flex flex-row items-center rounded-md',
|
||||
{ 'bg-gray-100 border-l-2 border-l-primary': active },
|
||||
className
|
||||
);
|
||||
|
||||
const menuItemContent = (
|
||||
<>
|
||||
{icon && (
|
||||
<Icon
|
||||
icon={icon}
|
||||
width={20}
|
||||
height={20}
|
||||
className={cn({
|
||||
'text-gray-400': !active,
|
||||
'text-black': active,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cn({ 'opacity-40': !active }, 'group-active:opacity-100')}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<li onClick={onClick}>
|
||||
{href && (
|
||||
<Link href={href} className={menuItemBaseClassName}>
|
||||
{menuItemContent}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!href && <a className={menuItemBaseClassName}>{menuItemContent}</a>}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuItem;
|
||||
Reference in New Issue
Block a user