Compare commits

..

1 Commits

Author SHA1 Message Date
ValdiANS 7d8a6ff852 fix: update next.js version 2025-12-09 10:52:49 +07:00
13 changed files with 483 additions and 559 deletions
+2
View File
@@ -140,6 +140,7 @@ deploy:dev:
environment: environment:
name: development name: development
url: https://dev-lti-erp.mbugroup.id url: https://dev-lti-erp.mbugroup.id
# ====== PRODUCTION ====== # ====== PRODUCTION ======
# build:production: # build:production:
# <<: *build_template # <<: *build_template
@@ -162,3 +163,4 @@ deploy:dev:
# environment: # environment:
# name: production # name: production
+13 -13
View File
@@ -15,7 +15,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "^15.5.7", "next": "15.5.7",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@@ -36,9 +36,9 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"daisyui": "^5.5.5", "daisyui": "^5.1.12",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "15.5.3",
"husky": "^9.1.7", "husky": "^9.1.7",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tailwindcss": "^4", "tailwindcss": "^4",
@@ -1088,9 +1088,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
"version": "15.5.7", "version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz",
"integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", "integrity": "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3063,9 +3063,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "5.5.5", "version": "5.3.10",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz",
"integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==", "integrity": "sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -3571,13 +3571,13 @@
} }
}, },
"node_modules/eslint-config-next": { "node_modules/eslint-config-next": {
"version": "15.5.7", "version": "15.5.3",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.7.tgz", "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.3.tgz",
"integrity": "sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==", "integrity": "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "15.5.7", "@next/eslint-plugin-next": "15.5.3",
"@rushstack/eslint-patch": "^1.10.3", "@rushstack/eslint-patch": "^1.10.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+3 -3
View File
@@ -18,7 +18,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "^15.5.7", "next": "15.5.7",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@@ -39,9 +39,9 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"daisyui": "^5.5.5", "daisyui": "^5.1.12",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "15.5.3",
"husky": "^9.1.7", "husky": "^9.1.7",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tailwindcss": "^4", "tailwindcss": "^4",
+147 -7
View File
@@ -1,21 +1,161 @@
'use client'; 'use client';
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Image from 'next/image'; import Image from 'next/image';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Drawer from '@/components/Drawer'; import Drawer from '@/components/Drawer';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Navbar from '@/components/Navbar'; import Navbar from '@/components/Navbar';
import Collapse from '@/components/Collapse';
import Button from '@/components/Button'; import Button from '@/components/Button';
import SidebarMenu from '@/components/molecules/SidebarMenu';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import { MAIN_DRAWER_LINKS } from '@/config/constant'; import { MAIN_DRAWER_LINKS } from '@/config/constant';
import { isPathActive } from '@/lib/helper'; import { cn } from '@/lib/helper';
type CollapseMenuProps = {
title: string;
link: string;
icon: string;
submenu?: CollapseMenuProps[];
depth?: number;
};
const isPathActive = (pathname: string, link?: string) => {
if (!link) return false;
const splittedPathname = pathname.split('/');
const splittedLink = link.split('/');
const isActiveLinkValid = splittedLink.every((linkChunk, idx) => {
return linkChunk === splittedPathname[idx];
});
return pathname.startsWith(link) && isActiveLinkValid;
};
const CollapseMenu = ({
title,
link,
icon,
submenu,
depth = 0,
}: CollapseMenuProps) => {
const pathname = usePathname();
const isActive = isPathActive(pathname, link);
const [open, setOpen] = useState(isActive);
const menuCollapseTitle = (
<div
className={cn(
'w-full px-3 py-2 rounded-md text-base font-semibold transition-colors flex flex-row justify-between items-center gap-2 hover:bg-primary/10 opacity-40',
{
'bg-primary/10 opacity-100': open || isActive,
}
)}
>
<div className='flex flex-row items-center gap-2'>
<Icon icon={icon} width={20} height={20} />
<span>{title}</span>
</div>
<Icon
icon='cuida:caret-up-outline'
width={20}
height={20}
className={cn('transition-transform', {
'rotate-90': !open,
'rotate-180': open,
})}
/>
</div>
);
return (
<Collapse
open={open}
title={menuCollapseTitle}
onOpenChange={setOpen}
className='w-full'
titleClassName='w-full p-0!'
>
<Menu>
<div
className='w-full py-0.5 flex flex-col gap-0.5'
style={{
paddingLeft: `${0.5 * (depth + 1)}rem`,
}}
>
{submenu?.map((item, idx) => {
const hasSubmenu = item.submenu && item.submenu.length > 0;
if (!hasSubmenu) {
return (
<MenuItem
key={idx}
title={item.title}
href={item.link}
icon={item.icon}
active={isPathActive(pathname, item.link)}
/>
);
}
return (
<CollapseMenu
key={idx}
title={item.title}
link={item.link}
icon={item.icon}
submenu={item.submenu}
depth={depth + 1}
/>
);
})}
</div>
</Menu>
</Collapse>
);
};
const MainDrawerMenu = () => {
const pathname = usePathname();
return (
<Menu>
{MAIN_DRAWER_LINKS.map((item, idx) => {
const hasSubmenu = item.submenu && item.submenu.length > 0;
if (!hasSubmenu) {
return (
<MenuItem
key={idx}
title={item.title}
href={item.link}
icon={item.icon}
active={pathname.startsWith(item.link)}
/>
);
}
return (
<CollapseMenu
key={idx}
title={item.title}
link={item.link}
icon={item.icon}
submenu={item.submenu}
/>
);
})}
</Menu>
);
};
const MainDrawerContent = () => { const MainDrawerContent = () => {
const pathname = usePathname();
const { setMainDrawerOpen } = useUiStore(); const { setMainDrawerOpen } = useUiStore();
const closeMainDrawerHandler = () => { const closeMainDrawerHandler = () => {
@@ -51,7 +191,7 @@ const MainDrawerContent = () => {
</div> </div>
</div> </div>
<SidebarMenu menu={MAIN_DRAWER_LINKS} activeLink={pathname} /> <MainDrawerMenu />
</div> </div>
); );
}; };
@@ -76,9 +216,9 @@ const MainDrawer = ({
const hasSubmenu = menu?.submenu && menu?.submenu.length > 0; const hasSubmenu = menu?.submenu && menu?.submenu.length > 0;
if (!title) { if (!title) {
title += menu?.text; title += menu?.title;
} else { } else {
title += ' - ' + menu?.text; title += ' - ' + menu?.title;
} }
if (!hasSubmenu || !menu.submenu) return; if (!hasSubmenu || !menu.submenu) return;
+208 -298
View File
@@ -1,9 +1,7 @@
'use client'; 'use client';
import { ChangeEventHandler, ReactNode } from 'react'; import { ReactNode } from 'react';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
@@ -19,18 +17,16 @@ const PaginationButton = ({
disabled?: boolean; disabled?: boolean;
onClick?: () => void; onClick?: () => void;
}) => ( }) => (
<Button <button
variant='ghost' className={cn(
color='none' '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} disabled={disabled}
onClick={onClick} onClick={onClick}
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
'disabled:text-primary disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-primary/10 disabled:active:translate-y-0'
)}
> >
{content} {content}
</Button> </button>
); );
const EtcPaginationButton = ({ const EtcPaginationButton = ({
@@ -94,20 +90,16 @@ const Pagination = ({
currentPage = 1, currentPage = 1,
totalItems = 0, totalItems = 0,
itemsPerPage = 10, itemsPerPage = 10,
rowOptions = [10, 20, 50, 100],
onPageChange, onPageChange,
onPrevPage = () => {}, onPrevPage = () => {},
onNextPage = () => {}, onNextPage = () => {},
onRowChange,
}: { }: {
currentPage: number; currentPage: number;
totalItems: number; totalItems: number;
itemsPerPage: number; itemsPerPage: number;
rowOptions?: number[];
onPageChange: (pageNumber: number) => void; onPageChange: (pageNumber: number) => void;
onPrevPage: () => void; onPrevPage: () => void;
onNextPage: () => void; onNextPage: () => void;
onRowChange?: (row: number) => void;
}) => { }) => {
const totalPages = const totalPages =
Math.ceil(totalItems / itemsPerPage) === 0 Math.ceil(totalItems / itemsPerPage) === 0
@@ -115,139 +107,30 @@ const Pagination = ({
: Math.ceil(totalItems / itemsPerPage); : Math.ceil(totalItems / itemsPerPage);
const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber); const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber);
const firstPageClickHandler = () => onPageChange(1);
const lastPageClickHandler = () => onPageChange(totalPages);
const rowChangeHandler: ChangeEventHandler<HTMLSelectElement> = (e) => {
onRowChange?.(Number(e.target.value));
};
const DisplayedRowCountSelect = () => (
<div className='flex flex-row items-center gap-4'>
<span className='text-sm font-medium text-base-content/50'>Showing</span>
<select
defaultValue={itemsPerPage}
onChange={rowChangeHandler}
className='select select-xs w-fit text-base-content/50'
>
{rowOptions.map((rowOption, rowOptionIdx) => (
<option
key={rowOptionIdx}
value={rowOption}
className='text-base-content active:text-neutral-content'
>
{rowOption} Per page
</option>
))}
</select>
</div>
);
const GoToFirstPageButton = () => (
<Button
disabled={currentPage === 1}
onClick={firstPageClickHandler}
variant='ghost'
color='none'
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-double-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const PrevPageButton = () => (
<Button
disabled={currentPage === 1}
onClick={onPrevPage}
variant='ghost'
color='none'
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const GoToLastPageButton = () => (
<Button
variant='ghost'
color='none'
disabled={currentPage === totalPages}
onClick={lastPageClickHandler}
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-double-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const NextPageButton = () => (
<Button
variant='ghost'
color='none'
disabled={currentPage === totalPages}
onClick={onNextPage}
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const PageInfo = () => (
<span className='text-nowrap text-sm font-medium text-base-content/50'>
Page {currentPage} of {totalPages}
</span>
);
return ( return (
<div className='@container'> <div>
<div className='flex flex-row justify-center items-center'> <div className='join w-full justify-between items-center gap-3'>
<div className='hidden @lg:block'> <button
<DisplayedRowCountSelect /> disabled={currentPage === 1}
</div> 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>
<div className='join w-full justify-end @lg:justify-center items-center gap-0.5'> {totalPages <= 7 && (
<div className='hidden @lg:block'> <div className='join-item join gap-0.5'>
<GoToFirstPageButton /> {range(1, totalPages).map((pageNumber) => (
</div>
<div className='hidden @lg:block'>
<PrevPageButton />
</div>
{totalPages <= 7 &&
range(1, totalPages).map((pageNumber) => (
<PaginationButton <PaginationButton
key={pageNumber} key={pageNumber}
content={pageNumber} content={pageNumber}
@@ -255,168 +138,195 @@ const Pagination = ({
onClick={() => pageChangeHandler(pageNumber)} onClick={() => pageChangeHandler(pageNumber)}
/> />
))} ))}
</div>
)}
{totalPages > 7 && ( {totalPages > 7 && (
<> <div className='join-item join gap-0.5'>
<PaginationButton <PaginationButton
content={1} content={1}
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => pageChangeHandler(1)} onClick={() => pageChangeHandler(1)}
/> />
{totalPages >= 2 && {totalPages >= 2 &&
(currentPage <= 3 || currentPage >= 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 <PaginationButton
content={totalPages} content={2}
disabled={currentPage === totalPages} disabled={currentPage === 2}
onClick={() => pageChangeHandler(totalPages)} 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'
)} )}
>
<div className='hidden @lg:block'> Next{' '}
<NextPageButton /> <Icon
</div> icon='uil:arrow-right'
width={20}
<div className='hidden @lg:block'> height={20}
<GoToLastPageButton /> className='text-gray-400 group-disabled:text-gray-300'
</div> />
</div> </button>
<div className='hidden @lg:block'>
<PageInfo />
</div>
</div> </div>
<div className='flex @lg:hidden flex-col justify-center items-end gap-2'> <div className='flex gap-2 mt-2 sm:hidden'>
<div className='flex flex-row items-center gap-0.5'> <button
<GoToFirstPageButton /> disabled={currentPage === 1}
<PrevPageButton /> onClick={onPrevPage}
<NextPageButton /> className={cn(
<GoToLastPageButton /> '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',
</div> '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>
<div className='flex flex-row items-center gap-4'> <button
<DisplayedRowCountSelect /> disabled={currentPage === totalPages}
onClick={onNextPage}
<PageInfo /> className={cn(
</div> '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>
</div> </div>
); );
+29 -59
View File
@@ -38,7 +38,6 @@ export interface TableProps<TData extends object> {
data: TData[]; data: TData[];
columns: ColumnDef<TData, unknown>[]; columns: ColumnDef<TData, unknown>[];
pageSize?: number; pageSize?: number;
onPageSizeChange?: (pageSize: number) => void;
totalItems?: number; totalItems?: number;
page?: number; page?: number;
onPageChange?: (page: number) => void; onPageChange?: (page: number) => void;
@@ -53,8 +52,6 @@ export interface TableProps<TData extends object> {
rowSelection?: Record<string, boolean>; rowSelection?: Record<string, boolean>;
setRowSelection?: OnChangeFn<Record<string, boolean>>; setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean); enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
withCheckbox?: boolean;
rowOptions?: number[];
} }
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -67,32 +64,28 @@ const emptyContentDefaultValue = (
</div> </div>
); );
const TABLE_DEFAULT_STYLING = {
containerClassName: 'w-full mb-20',
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: 'px-4 py-3 text-base-content/50',
tableBodyClassName: '',
bodyRowClassName: 'border-t border-t-base-content/10',
bodyColumnClassName: 'px-4 py-3 text-base-content',
paginationClassName: '',
};
const Table = <TData extends object>({ const Table = <TData extends object>({
data = [], data = [],
columns = [], columns = [],
pageSize = 10, pageSize = 10,
onPageSizeChange,
totalItems, totalItems,
page, page,
onPageChange, onPageChange,
isLoading = false, isLoading = false,
fuzzySearchValue, fuzzySearchValue,
onFuzzySearchValueChange, onFuzzySearchValueChange,
className = TABLE_DEFAULT_STYLING, className = {
containerClassName: '',
tableWrapperClassName: '',
tableClassName: '',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: '',
tableBodyClassName: '',
bodyRowClassName: '',
bodyColumnClassName: '',
paginationClassName: '',
},
emptyContent = emptyContentDefaultValue, emptyContent = emptyContentDefaultValue,
sorting, sorting,
setSorting, setSorting,
@@ -100,19 +93,12 @@ const Table = <TData extends object>({
rowSelection, rowSelection,
setRowSelection, setRowSelection,
enableRowSelection, enableRowSelection,
withCheckbox = false,
rowOptions = [10, 20, 50, 100],
}: TableProps<TData>) => { }: TableProps<TData>) => {
const isServerSideTable = const isServerSideTable =
totalItems !== undefined && totalItems !== undefined &&
page !== undefined && page !== undefined &&
onPageChange !== undefined; onPageChange !== undefined;
const tableClassNames = {
...TABLE_DEFAULT_STYLING,
...className,
};
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
pageIndex: 0, pageIndex: 0,
pageSize: pageSize, pageSize: pageSize,
@@ -205,15 +191,12 @@ const Table = <TData extends object>({
}, [pageSize, setPageSize]); }, [pageSize, setPageSize]);
return ( return (
<div className={tableClassNames.containerClassName}> <div className={className.containerClassName}>
<div className={tableClassNames.tableWrapperClassName}> <div className={className.tableWrapperClassName}>
<table className={tableClassNames.tableClassName}> <table className={className.tableClassName}>
<thead className={tableClassNames.tableHeaderClassName}> <thead className={className.tableHeaderClassName}>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr <tr key={headerGroup.id} className={className.headerRowClassName}>
key={headerGroup.id}
className={tableClassNames.headerRowClassName}
>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th <th
key={header.id} key={header.id}
@@ -223,10 +206,7 @@ const Table = <TData extends object>({
header.column.getCanSort() header.column.getCanSort()
? 'cursor-pointer select-none' ? 'cursor-pointer select-none'
: '', : '',
{ className.headerColumnClassName
'first:w-9 first:pr-0': withCheckbox,
},
tableClassNames.headerColumnClassName
)} )}
> >
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
@@ -236,13 +216,12 @@ const Table = <TData extends object>({
)} )}
{header.column.getCanSort() && ( {header.column.getCanSort() && (
<div className='w-4 h-4 relative flex flex-col items-center'> <div className='flex items-center'>
<Icon <Icon
icon='heroicons:chevron-up-16-solid' icon='lucide:arrow-up'
width={18} width={12}
height={18} height={12}
className={cn( className={cn(
'absolute -top-1',
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc' header.column.getIsSorted() === 'asc'
? 'text-black' ? 'text-black'
@@ -250,11 +229,10 @@ const Table = <TData extends object>({
)} )}
/> />
<Icon <Icon
icon='heroicons:chevron-down-16-solid' icon='lucide:arrow-down'
width={18} width={12}
height={18} height={12}
className={cn( className={cn(
'absolute -bottom-1.5',
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc' header.column.getIsSorted() === 'desc'
? 'text-black' ? 'text-black'
@@ -270,17 +248,11 @@ const Table = <TData extends object>({
))} ))}
</thead> </thead>
<tbody className={tableClassNames.tableBodyClassName}> <tbody className={className.tableBodyClassName}>
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row) => (
<tr key={row.id} className={tableClassNames.bodyRowClassName}> <tr key={row.id} className={className.bodyRowClassName}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td <td key={cell.id} className={className.bodyColumnClassName}>
key={cell.id}
className={cn(
{ 'first:w-9 first:pr-0': withCheckbox },
tableClassNames.bodyColumnClassName
)}
>
{!isLoading && {!isLoading &&
flexRender(cell.column.columnDef.cell, cell.getContext())} flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -298,7 +270,7 @@ const Table = <TData extends object>({
emptyContent} emptyContent}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
<div className={cn('mt-5', tableClassNames.paginationClassName)}> <div className={cn('mt-5', className.paginationClassName)}>
<Pagination <Pagination
totalItems={isServerSideTable ? totalItems : table.getRowCount()} totalItems={isServerSideTable ? totalItems : table.getRowCount()}
itemsPerPage={table.getState().pagination.pageSize} itemsPerPage={table.getState().pagination.pageSize}
@@ -310,8 +282,6 @@ const Table = <TData extends object>({
onPrevPage={prevPageClickHandler} onPrevPage={prevPageClickHandler}
onNextPage={nextPageClickHandler} onNextPage={nextPageClickHandler}
onPageChange={pageChangeHandler} onPageChange={pageChangeHandler}
rowOptions={rowOptions}
onRowChange={onPageSizeChange}
/> />
</div> </div>
)} )}
+2 -13
View File
@@ -2,9 +2,8 @@
import { HTMLProps, useEffect, useRef } from 'react'; import { HTMLProps, useEffect, useRef } from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Size } from '@/types/theme';
interface CheckboxInputProps extends Omit<HTMLProps<HTMLInputElement>, 'size'> { interface CheckboxInputProps extends HTMLProps<HTMLInputElement> {
name: string; name: string;
label?: string; label?: string;
indeterminate?: boolean; indeterminate?: boolean;
@@ -17,7 +16,6 @@ interface CheckboxInputProps extends Omit<HTMLProps<HTMLInputElement>, 'size'> {
isError?: boolean; isError?: boolean;
isValid?: boolean; isValid?: boolean;
errorMessage?: string; errorMessage?: string;
size?: Size;
} }
const CheckboxInput = ({ const CheckboxInput = ({
@@ -29,19 +27,10 @@ const CheckboxInput = ({
isValid, isValid,
isError, isError,
errorMessage, errorMessage,
size = 'sm',
...rest ...rest
}: CheckboxInputProps) => { }: CheckboxInputProps) => {
const ref = useRef<HTMLInputElement>(null!); const ref = useRef<HTMLInputElement>(null!);
const checkboxBaseClassName = cn('checkbox cursor-pointer rounded-md', {
'checkbox-xs': size === 'xs',
'checkbox-sm': size === 'sm',
'checkbox-md': size === 'md',
'checkbox-lg': size === 'lg',
'checkbox-xl': size === 'xl',
});
useEffect(() => { useEffect(() => {
if (typeof indeterminate === 'boolean') { if (typeof indeterminate === 'boolean') {
ref.current.indeterminate = !rest.checked && indeterminate; ref.current.indeterminate = !rest.checked && indeterminate;
@@ -64,7 +53,7 @@ const CheckboxInput = ({
id={name} id={name}
name={name} name={name}
className={cn( className={cn(
checkboxBaseClassName, 'checkbox cursor-pointer',
{ {
'border-error': isError, 'border-error': isError,
'border-success': isValid, 'border-success': isValid,
+4 -20
View File
@@ -1,32 +1,16 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Size } from '@/types/theme';
interface MenuProps { interface MenuProps {
children?: ReactNode; children?: ReactNode;
size?: Size;
direction?: 'vertical' | 'horizontal';
className?: string; className?: string;
} }
const Menu = ({ const Menu = ({ children, className }: MenuProps) => {
children, return (
size = 'md', <ul className={cn('menu w-full p-0 gap-0.5', className)}>{children}</ul>
direction = 'vertical', );
className,
}: MenuProps) => {
const menuBaseClassName = cn('menu w-full', {
'menu-xs': size === 'xs',
'menu-sm': size === 'sm',
'menu-md': size === 'md',
'menu-lg': size === 'lg',
'menu-xl': size === 'xl',
'menu-vertical': direction === 'vertical',
'menu-horizontal': direction === 'horizontal',
});
return <ul className={cn(menuBaseClassName, className)}>{children}</ul>;
}; };
export default Menu; export default Menu;
-92
View File
@@ -1,92 +0,0 @@
import Link from 'next/link';
import Menu from '@/components/menu/Menu';
import { Icon } from '@iconify/react';
import { cn, isPathActive } from '@/lib/helper';
export interface SidebarMenuItem {
type?: 'item' | 'title';
text: string;
link: string;
icon?: string;
submenu?: SidebarMenuItem[];
}
interface SidebarMenuItemProps {
item: SidebarMenuItem;
activeLink: string;
}
interface SidebarMenuProps {
menu: SidebarMenuItem[];
activeLink: string;
}
const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
const isItemActive = isPathActive(activeLink, item.link);
const menuItemWithoutSubmenu = (
<li>
<Link
href={item.link}
className={cn(
{
'menu-active border-2 border-solid border-base-300': isItemActive,
},
'px-3 py-1.5'
)}
>
{item.icon && <Icon icon={item.icon} width={20} height={20} />}
<span className='text-base'>{item.text}</span>
</Link>
</li>
);
if (!item.submenu || item.submenu.length === 0) {
return menuItemWithoutSubmenu;
}
const menuItemWithSubmenu = (
<li>
<details open={isItemActive}>
<summary
className={cn({
'text-primary': isItemActive,
})}
>
{item.icon && <Icon icon={item.icon} width={20} height={20} />}
<span className='text-base'>{item.text}</span>
</summary>
<ul>
{item.submenu.map((submenuItem, submenuIdx) => (
<SidebarMenuItem
key={`submenu#${submenuIdx}`}
item={submenuItem}
activeLink={activeLink}
/>
))}
</ul>
</details>
</li>
);
return menuItemWithSubmenu;
};
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
return (
<Menu>
{menu.map((menuItem, menuIdx) => (
<SidebarMenuItem
key={menuIdx}
item={menuItem}
activeLink={activeLink}
/>
))}
</Menu>
);
};
export default SidebarMenu;
+73 -34
View File
@@ -1,116 +1,155 @@
import { SidebarMenuItem } from '@/components/molecules/SidebarMenu'; type MAIN_DRAWER_MENU = {
title: string;
link: string;
icon: string;
submenu?: MAIN_DRAWER_MENU[];
};
export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
{ {
text: 'Dashboard', title: 'Dashboard',
link: '/dashboard', link: '/dashboard',
icon: 'heroicons-outline:chart-bar-square', icon: 'gg:chart',
}, },
{ {
text: 'Produksi', title: 'Produksi',
link: '/production', link: '/production',
icon: 'heroicons-outline:wrench-screwdriver', icon: 'material-symbols:conveyor-belt-outline-rounded',
submenu: [ submenu: [
{ {
text: 'Daftar Flock', title: 'List Flock',
link: '/production/project-flock', link: '/production/project-flock',
icon: 'material-symbols:list-alt-add-outline-rounded',
}, },
// { // DI HILANGKAN PADA VERSI REFACTORING
// title: 'Chick In',
// link: '/production/chickin',
// icon: 'mdi:home-import-outline',
// },
{ {
text: 'Recording', title: 'Recording',
link: '/production/recording', link: '/production/recording',
icon: 'mdi:clipboard-text',
}, },
{ {
text: 'Transfer to Laying', title: 'Transfer ke Laying',
link: '/production/transfer-to-laying', link: '/production/transfer-to-laying',
icon: 'streamline:transfer-van',
}, },
], ],
}, },
{ {
text: 'Pembelian', title: 'Pembelian',
link: '/purchase', link: '/purchase',
icon: 'heroicons-outline:shopping-cart', icon: 'gg:shopping-cart',
}, },
{ {
text: 'Penjualan', title: 'Penjualan',
link: '/marketing', link: '/marketing',
icon: 'heroicons-outline:currency-dollar', icon: 'mdi:attach-money',
}, },
{ {
text: 'Biaya Operasional', title: 'Biaya Operasional',
link: '/expense', link: '/expense',
icon: 'heroicons:wallet', icon: 'uil:wallet',
}, },
{ {
text: 'Persediaan', title: 'Persediaan',
link: '/inventory', link: '/inventory',
icon: 'heroicons-outline:folder', icon: 'mdi:warehouse',
submenu: [ submenu: [
// {
// title: 'Product',
// link: '/inventory/product',
// icon: 'mdi:package-variant-closed',
// },
{ {
text: 'Penyesuaian Stok', title: 'Penyesuaian Stok',
link: '/inventory/adjustment', link: '/inventory/adjustment',
icon: 'mdi:database-edit',
}, },
{ {
text: 'Transfer Stok', title: 'Transfer Stok',
link: '/inventory/movement', link: '/inventory/movement',
icon: 'mdi:swap-horizontal',
}, },
], ],
}, },
{ {
text: 'Master Data', title: 'Master Data',
link: '/master-data', link: '/master-data',
icon: 'heroicons-outline:circle-stack', icon: 'majesticons:data-line',
submenu: [ submenu: [
{ {
text: 'Produk', title: 'Product',
link: '/master-data/product', link: '/master-data/product',
icon: 'fluent-mdl2:product-variant',
}, },
{ {
text: 'Kategori Produk', title: 'Product Category',
link: '/master-data/product-category', link: '/master-data/product-category',
icon: 'carbon:categories',
}, },
{ {
text: 'Bank', title: 'Bank',
link: '/master-data/bank', link: '/master-data/bank',
icon: 'mdi:bank-outline',
}, },
{ {
text: 'Area', title: 'Area',
link: '/master-data/area', link: '/master-data/area',
icon: 'majesticons:map-marker-area-line',
}, },
{ {
text: 'Lokasi', title: 'Location',
link: '/master-data/location', link: '/master-data/location',
icon: 'mingcute:location-line',
}, },
{ {
text: 'Kandang', title: 'Kandang',
link: '/master-data/kandang', link: '/master-data/kandang',
icon: 'mdi:farm-home-outline',
}, },
{ {
text: 'Warehouse', title: 'Warehouse',
link: '/master-data/warehouse', link: '/master-data/warehouse',
icon: 'hugeicons:warehouse',
}, },
{ {
text: 'Customer', title: 'Customer',
link: '/master-data/customer', link: '/master-data/customer',
icon: 'ix:customer',
}, },
{ {
text: 'UOM', title: 'UOM',
link: '/master-data/uom', link: '/master-data/uom',
icon: 'lsicon:measure-outline',
}, },
{ {
text: 'Non-Stock', title: 'Non-Stock',
link: '/master-data/nonstock', link: '/master-data/nonstock',
icon: 'fluent:box-32-regular',
}, },
{ {
text: 'FCR', title: 'FCR',
link: '/master-data/fcr', link: '/master-data/fcr',
icon: 'fluent:food-chicken-leg-16-regular',
}, },
{ {
text: 'Supplier', title: 'Supplier',
link: '/master-data/supplier', link: '/master-data/supplier',
icon: 'material-symbols:add-business-outline-rounded',
}, },
{ {
text: 'Flock', title: 'Flock',
link: '/master-data/flock', link: '/master-data/flock',
icon: 'material-symbols:raven-outline-rounded',
}, },
], ],
}, },
-13
View File
@@ -119,16 +119,3 @@ export const convertRowSelectionObjToArr = (
return result; return result;
}; };
export const isPathActive = (pathname: string, link?: string) => {
if (!link) return false;
const splittedPathname = pathname.split('/');
const splittedLink = link.split('/');
const isActiveLinkValid = splittedLink.every((linkChunk, idx) => {
return linkChunk === splittedPathname[idx];
});
return pathname.startsWith(link) && isActiveLinkValid;
};
-5
View File
@@ -1,9 +1,4 @@
@layer utilities { @layer utilities {
.menu {
--menu-active-fg: var(--color-primary);
--menu-active-bg: transparent;
}
.step.step-success::before { .step.step-success::before {
--step-bg: var(--color-success); --step-bg: var(--color-success);
--step-fg: var(--color-success-content); --step-fg: var(--color-success-content);
+2 -2
View File
@@ -1,4 +1,4 @@
export type Color = type Color =
| 'primary' | 'primary'
| 'secondary' | 'secondary'
| 'accent' | 'accent'
@@ -9,4 +9,4 @@ export type Color =
| 'error' | 'error'
| 'none'; | 'none';
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export { Color };