From d46652cb68560db1900c8239402e60bd6a7a809e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 23 Jan 2026 23:00:15 +0700 Subject: [PATCH] feat: add Breadcrumb component --- src/components/Breadcrumb.tsx | 263 ++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/components/Breadcrumb.tsx 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;