This commit is contained in:
ValdiANS
2025-09-26 11:06:31 +07:00
parent a5524686a6
commit 2e1b0fef2b
36 changed files with 8716 additions and 79 deletions
+84
View File
@@ -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;
+60
View File
@@ -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;
+54
View File
@@ -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;
+324
View File
@@ -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;
+199
View File
@@ -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;
+88
View File
@@ -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;
+47
View File
@@ -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;
+203
View File
@@ -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;
+130
View File
@@ -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;
+16
View File
@@ -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;
+64
View File
@@ -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;