diff --git a/src/app/globals.css b/src/app/globals.css index eda1deab..0eb04a09 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,6 @@ @import 'tailwindcss'; @plugin "daisyui"; +@import '../styles/tailwind.css'; @import '../styles/daisyui.css'; @import '../figma-make/styles/theme.css'; @@ -34,11 +35,11 @@ /* Status/Utility Colors */ --color-info: oklch(67.4% 0.176 238.9); --color-info-content: oklch(0% 0 0); /* #000000 */ - --color-success: oklch(62.3% 0.147 149); + --color-success: #00d390; --color-success-content: oklch(100% 0 0); /* #ffffff */ --color-warning: oklch(82.2% 0.165 91.9); --color-warning-content: oklch(0% 0 0); /* #000000 */ - --color-error: oklch(61.8% 0.203 27.8); + --color-error: #ff3a3a; --color-error-content: oklch(100% 0 0); /* #fffffff */ --radius-selector: 0rem; @@ -52,7 +53,7 @@ } :root { - --color-primary: #1f74bf; + --color-primary: #0069e0; } @theme { @@ -64,6 +65,9 @@ --container-lg: 64rem; --container-xl: 80rem; --container-2xl: 96rem; + + --shadow-button-soft: + 0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200); } html { diff --git a/src/app/production/transfer-to-laying/page.tsx b/src/app/production/transfer-to-laying/page.tsx index 84513542..048ae005 100644 --- a/src/app/production/transfer-to-laying/page.tsx +++ b/src/app/production/transfer-to-laying/page.tsx @@ -1,9 +1,12 @@ import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable'; +import TransferToLayingFormModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFormModal'; const TransferToLaying = () => { return ( -
+
+ +
); }; diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx new file mode 100644 index 00000000..e5a4ef63 --- /dev/null +++ b/src/components/Breadcrumb.tsx @@ -0,0 +1,263 @@ +import React, { useId } from 'react'; +import Link from 'next/link'; +import { Icon } from '@iconify/react'; +import { cn, findMenuPath } from '@/lib/helper'; +import { Size } from '@/types/theme'; +import Button from '@/components/Button'; +import { MAIN_DRAWER_LINKS } from '@/config/constant'; + +interface BreadcrumbItem { + label: string; + href?: string; + icon?: React.ReactNode; + isActive?: boolean; + isDisabled?: boolean; +} + +interface BreadcrumbsProps extends React.HTMLAttributes { + items: BreadcrumbItem[]; + size?: Size; + maxVisibleItems?: number; + showEllipsisDropdown?: boolean; +} + +export function buildBreadcrumbs(pathname: string): BreadcrumbItem[] { + const menuPath = findMenuPath(MAIN_DRAWER_LINKS, pathname); + + if (!menuPath) return []; + + return menuPath.map((menu, index) => { + const isLast = index === menuPath.length - 1; + + return { + label: menu.text, + href: isLast ? menu.link : undefined, + isActive: isLast, + icon: menu.icon ? ( + + ) : undefined, + }; + }); +} + +const EllipsisDropdown = ({ + hiddenItems, +}: { + hiddenItems: BreadcrumbItem[]; +}) => { + const dropdownId = useId(); + const anchorId = useId(); + + return ( +
  • + {/* Ellipsis Button */} + + + {/* Dropdown Menu using popover API */} +
      + {hiddenItems.map((item, index) => { + const itemStyles = cn( + '[&]:flex [&]:items-center [&]:justify-start py-1 text-sm', + // Disabled state + item.isDisabled && 'text-base-content/40 opacity-50', + // Active/Last state + (item.isActive || item.isDisabled) && 'text-primary', + // Regular clickable state + !item.isDisabled && 'text-base-content/50' + ); + + const itemContent = ( +
      + {item.icon && ( + {item.icon} + )} + {item.label} +
      + ); + + return ( +
    • + {item.href && !item.isDisabled ? ( + e.stopPropagation()} + > + {itemContent} + + ) : ( +
      + {itemContent} +
      + )} +
    • + ); + })} +
    +
  • + ); +}; + +const Breadcrumb = ({ + items, + size = 'md', + maxVisibleItems = 3, + showEllipsisDropdown = true, + className, + ...props +}: BreadcrumbsProps) => { + const sizeClasses = { + xs: 'text-xs', + sm: 'text-sm', + md: 'text-base', + lg: 'text-lg', + xl: 'text-xl', + }; + + const getItemStyles = ( + item: BreadcrumbItem, + position: 'first' | 'middle' | 'last' = 'middle' + ) => { + const baseClasses = 'inline-flex items-center gap-2'; + + // Disabled state + if (item.isDisabled) { + return `${baseClasses} text-base-content/40 !cursor-default opacity-50 hover:!no-underline`; + } + + // Active/Last state (no underline) + if (item.isActive || position === 'last') { + return `${baseClasses} text-primary !cursor-pointer hover:!no-underline`; + } + + // Regular clickable state + return `${baseClasses} text-base-content/60`; + }; + + const renderItem = ( + item: BreadcrumbItem, + position: 'first' | 'middle' | 'last' = 'middle' + ) => { + const styles = getItemStyles(item, position); + + // Disabled items + if (item.isDisabled) { + return ( + + {item.icon && item.icon} + {item.label} + + ); + } + + // Active/Last items + if (item.isActive || position === 'last') { + if (item.href) { + return ( + + {item.icon && ( + {item.icon} + )} + {item.label} + + ); + } + return ( + + {item.icon && item.icon} + {item.label} + + ); + } + + // Regular items + if (item.href) { + return ( + + {item.icon && {item.icon}} + {item.label} + + ); + } + + return ( + + {item.icon && item.icon} + {item.label} + + ); + }; + + const renderBreadcrumbList = () => { + // Show all items if within limit + if (items.length <= maxVisibleItems) { + return items.map((item, index) => { + const position = + index === 0 + ? 'first' + : index === items.length - 1 + ? 'last' + : 'middle'; + return
  • {renderItem(item, position)}
  • ; + }); + } + + // Collapsed items indexing when exceeding limit + const firstItem = items[0]; + const lastItem = items[items.length - 1]; + const visibleMiddleItems = items.slice(1, -1).slice(-(maxVisibleItems - 2)); + const hiddenItems = items.slice(1, -1).slice(0, -(maxVisibleItems - 2)); + const showEllipsis = showEllipsisDropdown && hiddenItems.length > 0; + + return ( + <> +
  • {renderItem(firstItem, 'first')}
  • + + {/* Ellipsis for hidden items with dropdown */} + {showEllipsis && } + + {/* Middle items */} + {visibleMiddleItems.map((item, index) => ( +
  • {renderItem(item, 'middle')}
  • + ))} + +
  • {renderItem(lastItem, 'last')}
  • + + ); + }; + + return ( + + ); +}; + +export default Breadcrumb; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 2f209ece..b55e5e24 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -2,11 +2,12 @@ import react from 'react'; import Link from 'next/link'; import { cn } from '@/lib/helper'; import { Color } from '@/types/theme'; +import { UrlObject } from 'url'; export interface ButtonProps extends react.ComponentProps<'button'> { variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active'; color?: Color; - href?: string; + href?: string | UrlObject; isLoading?: boolean; target?: string; rel?: string; diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 056d67a4..fdb65c38 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -78,40 +78,6 @@ const MainDrawer = ({ permissionCheck(permission) ); - const getPageTitle = useCallback(() => { - let title = ''; - - const activeMenu = MAIN_DRAWER_LINKS.find((item) => - isPathActive(pathname, item.link) - ); - - const traverseMenuTitle = (menu: typeof activeMenu) => { - if (!menu) return; - - const hasSubmenu = menu?.submenu && menu?.submenu.length > 0; - - if (!title) { - title += menu?.text; - } else { - title += ' - ' + menu?.text; - } - - if (!hasSubmenu || !menu.submenu) return; - - const activeSubmenu = menu.submenu?.find((item) => - isPathActive(pathname, item.link) - ); - - traverseMenuTitle(activeSubmenu); - }; - - traverseMenuTitle(activeMenu); - - return title; - }, [pathname]); - - const pageTitle = getPageTitle(); - const toggleSidebar = () => { setMainDrawerOpen(!mainDrawerOpen); }; @@ -132,7 +98,7 @@ const MainDrawer = ({ }} >
    - + {children}
    diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 5a1dc806..caa07870 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -53,15 +53,25 @@ interface ModalProps { ref: RefObject; children?: ReactNode; closeOnBackdrop?: boolean; + onBackdropClick?: () => void; + position?: 'top' | 'middle' | 'bottom' | 'start' | 'end'; className?: { modal?: string; modalBox?: string; }; } -const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => { +const Modal = ({ + ref, + children, + closeOnBackdrop, + onBackdropClick, + position = 'middle', + className, +}: ModalProps) => { const handleBackdropClick = (e: React.MouseEvent) => { if (closeOnBackdrop && e.target === ref.current) { + onBackdropClick?.(); ref.current?.close(); } }; @@ -69,7 +79,17 @@ const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => { return (
    {children}
    diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 280217a0..4998ca66 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,26 +1,26 @@ 'use client'; import toast from 'react-hot-toast'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; -import Dropdown from '@/components/Dropdown'; +import Breadcrumb, { buildBreadcrumbs } from '@/components/Breadcrumb'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; import { isResponseError } from '@/lib/api-helper'; interface NavbarProps { - title: string; toggleSidebar?: () => void; } -const Navbar = ({ title, toggleSidebar }: NavbarProps) => { +const Navbar = ({ toggleSidebar }: NavbarProps) => { const { setUser } = useAuth(); const router = useRouter(); + const pathname = usePathname(); const logoutClickHandler = async () => { const logoutRes = await AuthApi.logout(); @@ -35,42 +35,52 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => { }; return ( -
    +
    {toggleSidebar && ( - )} - {title} +
    - -
    - -
    -
    - } - className={{ - content: 'w-52 mt-3', - }} + - - - - + + + + + +
    ); diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 37bc118a..0e095c1f 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -31,6 +31,7 @@ interface TableClassNames { headerColumnClassName?: string; tableBodyClassName?: string; bodyRowClassName?: string; + selectedBodyRowClassName?: string; bodyColumnClassName?: string; tableFooterClassName?: string; footerRowClassName?: string; @@ -88,9 +89,11 @@ export const TABLE_DEFAULT_STYLING = { headerColumnClassName: 'px-4 py-3 border-base-content/10 text-base-content/50 text-sm font-medium', tableBodyClassName: '', - bodyRowClassName: 'border-t border-base-content/10', + bodyRowClassName: + 'transition-all duration-200 border-t border-base-content/10 bg-transparent', + selectedBodyRowClassName: 'bg-primary/5', bodyColumnClassName: 'px-4 py-3 text-base-content', - paginationClassName: '', + paginationClassName: 'px-3', tableFooterClassName: 'font-semibold border-base-content/10', footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', @@ -353,7 +356,11 @@ const Table = ({ key={row.id} className={cn( TABLE_DEFAULT_STYLING.bodyRowClassName, - tableClassNames.bodyRowClassName + tableClassNames.bodyRowClassName, + { + [tableClassNames.selectedBodyRowClassName]: + row.getIsSelected(), + } )} > {row.getVisibleCells().map((cell) => ( diff --git a/src/components/helper/StatusBadge.tsx b/src/components/helper/StatusBadge.tsx new file mode 100644 index 00000000..46b36559 --- /dev/null +++ b/src/components/helper/StatusBadge.tsx @@ -0,0 +1,56 @@ +import Badge from '@/components/Badge'; +import { cn } from '@/lib/helper'; +import { Color } from '@/types/theme'; + +interface StatusBadgeProps { + color: Color; + text: string; + className?: { + badge?: string; + status?: string; + }; +} + +const StatusBadge = ({ + color = 'neutral', + text, + className, +}: StatusBadgeProps) => { + return ( + + + + + + {text} + + ); +}; + +export default StatusBadge; diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx index a424d723..558779c7 100644 --- a/src/components/input/DateInput.tsx +++ b/src/components/input/DateInput.tsx @@ -204,17 +204,12 @@ const DateInput = ({ const finalErrorMessage = internalError || externalErrorMessage; return ( -
    +
    {label && (
    {!finalIsError && bottomLabel && ( -

    {bottomLabel}

    +

    {bottomLabel}

    )} {finalIsError && finalErrorMessage && ( -

    {finalErrorMessage}

    +

    {finalErrorMessage}

    )} -
    +
    {shouldShowAdornment && startAdornment} {children}
    @@ -118,7 +118,7 @@ const CustomMenuList = < {children} {options.length > 0 && isLoading && ( -
    +
    )} @@ -204,16 +204,11 @@ const SelectInput = (props: SelectInputProps) => { }; return ( -
    +
    {label && ( (props: SelectInputProps) => { ...(!startAdornment && { control: ({ isFocused, isDisabled }) => cn( - 'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!', + 'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!', { 'border-red-500! ring-2 ring-red-200': isError, 'border-indigo-500 ring-2 ring-indigo-200': isFocused, - 'border-gray-300': !isError && !isFocused, + 'border-base-content/10!': !isError && !isFocused, 'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled, } ), - valueContainer: () => cn('flex-1 px-4! py-2! gap-1'), + valueContainer: () => cn('flex-1 p-3! 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'), + input: () => cn('text-gray-900 m-0! p-0!'), indicatorsContainer: () => cn('flex items-center gap-1 pr-2'), dropdownIndicator: ({ isFocused }) => - cn('p-1 rounded hover:bg-gray-100', { + cn('p-1! rounded hover:bg-gray-100', { 'text-gray-900': isFocused, 'text-gray-500': !isFocused, 'text-error!': isError, }), + clearIndicator: () => cn('p-1! rounded hover:bg-gray-100'), menu: () => - cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'), - menuList: () => cn('p-2! max-h-60 overflow-auto'), + cn( + 'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!' + ), + menuList: () => cn('p-0! max-h-60 overflow-auto'), option: ({ isFocused, isSelected }) => - cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', { + cn('px-3 py-2 rounded-md cursor-pointer!', { 'bg-indigo-600 text-white': isFocused, 'bg-blue-500!': isSelected, 'text-gray-700': !isFocused && !isSelected, @@ -287,13 +285,17 @@ const SelectInput = (props: SelectInputProps) => { multiValue: ({ getValue, index }) => { const selectedValues = getValue() as T[]; return cn( - 'bg-indigo-50 rounded py-0.5 pl-2 pr-1 flex items-center gap-1!', + 'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!', selectedValues[index]?.className ); }, + multiValueRemove: () => cn('p-0! w-3 h-3'), multiValueLabel: ({ getValue, index }) => { const selectedValues = getValue() as T[]; - return cn('text-indigo-700', selectedValues[index]?.labelClassName); + return cn( + 'p-0! text-base-content! text-xs!', + selectedValues[index]?.labelClassName + ); }, }} components={{ diff --git a/src/components/input/SelectInputCheckbox.tsx b/src/components/input/SelectInputCheckbox.tsx index 24125204..a334d133 100644 --- a/src/components/input/SelectInputCheckbox.tsx +++ b/src/components/input/SelectInputCheckbox.tsx @@ -25,14 +25,18 @@ const CheckboxOption = < >( props: OptionProps ) => { - const { isSelected, label, innerRef, innerProps, className } = props; + const { isSelected, label, innerRef, innerProps, className, isFocused } = + props; return (
    @@ -40,9 +44,12 @@ const CheckboxOption = < type='checkbox' checked={isSelected} onChange={() => null} - className='checkbox checkbox-sm rounded checkbox-primary pointer-events-none' + className='checkbox checkbox-sm rounded-md checkbox-primary pointer-events-none border-base-content/10' /> - + +
    ); }; diff --git a/src/components/input/SelectInputRadio.tsx b/src/components/input/SelectInputRadio.tsx index 73608931..5b7b56ac 100644 --- a/src/components/input/SelectInputRadio.tsx +++ b/src/components/input/SelectInputRadio.tsx @@ -21,14 +21,18 @@ const RadioOption = < >( props: OptionProps ) => { - const { isSelected, label, innerRef, innerProps, className } = props; + const { isSelected, label, innerRef, innerProps, className, isFocused } = + props; return (
    @@ -36,9 +40,12 @@ const RadioOption = < type='radio' checked={isSelected} onChange={() => null} - className='radio radio-sm radio-primary pointer-events-none' + className='radio radio-md radio-primary pointer-events-none' /> - + +
    ); }; diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx index 550dbc6b..65bd57d3 100644 --- a/src/components/input/TextArea.tsx +++ b/src/components/input/TextArea.tsx @@ -53,7 +53,7 @@ const TextArea = ({ return (
    @@ -61,7 +61,7 @@ const TextArea = ({
    ); }; diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 5936c85a..62582781 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -21,6 +21,9 @@ export interface TextInputProps { label?: string; inputWrapper?: string; input?: string; + inputPrefix?: string; + inputSuffix?: string; + inputPrefixSuffixWrapper?: string; }; isError?: boolean; isValid?: boolean; @@ -62,7 +65,7 @@ const TextInput = ({ return (
    @@ -70,7 +73,7 @@ const TextInput = ({