Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
'use client'
|
|
|
|
import type { ComponentProps, ReactElement, ReactNode } from 'react'
|
|
|
|
import { Children, Fragment, isValidElement } from 'react'
|
|
import { Description, Dropdown as HeroDropdown, Header, Label, Separator } from '@heroui/react'
|
|
|
|
type DropdownKey = string | number
|
|
|
|
export interface DropdownItemProps {
|
|
children?: ReactNode
|
|
className?: string
|
|
color?: 'default' | 'danger'
|
|
description?: ReactNode
|
|
key?: DropdownKey
|
|
startContent?: ReactNode
|
|
textValue?: string
|
|
onPress?: () => void
|
|
}
|
|
|
|
interface DropdownMenuProps {
|
|
children?: ReactNode
|
|
'aria-label'?: string
|
|
closeOnSelect?: boolean
|
|
disabledKeys?: Iterable<DropdownKey>
|
|
disallowEmptySelection?: boolean
|
|
selectedKeys?: 'all' | Iterable<DropdownKey>
|
|
selectionMode?: 'none' | 'single' | 'multiple'
|
|
variant?: string
|
|
onAction?: (key: DropdownKey) => void
|
|
onSelectionChange?: (keys: 'all' | Set<DropdownKey>) => void
|
|
}
|
|
|
|
interface DropdownSectionProps {
|
|
children?: ReactNode
|
|
showDivider?: boolean
|
|
title?: ReactNode
|
|
}
|
|
|
|
interface DropdownProps {
|
|
children?: ReactNode
|
|
placement?: ComponentProps<typeof HeroDropdown.Popover>['placement'] | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'
|
|
trigger?: 'press' | 'longPress'
|
|
}
|
|
|
|
export function DropdownTrigger({ children }: { children?: ReactNode }) {
|
|
return children
|
|
}
|
|
|
|
export function DropdownMenu({ children }: DropdownMenuProps) {
|
|
return children
|
|
}
|
|
|
|
export function DropdownItem({ children }: DropdownItemProps) {
|
|
return children
|
|
}
|
|
|
|
export function DropdownSection({ children }: DropdownSectionProps) {
|
|
return children
|
|
}
|
|
|
|
function itemTextValue(item: ReactElement<DropdownItemProps>, id: string): string {
|
|
if (item.props.textValue) return item.props.textValue
|
|
if (typeof item.props.children === 'string' || typeof item.props.children === 'number') return String(item.props.children)
|
|
|
|
return id
|
|
}
|
|
|
|
function renderItem(item: ReactElement<DropdownItemProps>, index: number, showIndicator: boolean) {
|
|
const id = normalizeElementKey(item.key, index)
|
|
const label = <Label>{item.props.children}</Label>
|
|
const description = item.props.description ? <Description>{item.props.description}</Description> : null
|
|
|
|
return (
|
|
<HeroDropdown.Item
|
|
key={id}
|
|
className={item.props.className}
|
|
id={id}
|
|
textValue={itemTextValue(item, id)}
|
|
variant={item.props.color === 'danger' ? 'danger' : undefined}
|
|
onAction={item.props.onPress}
|
|
>
|
|
{showIndicator ? <HeroDropdown.ItemIndicator /> : null}
|
|
{item.props.startContent}
|
|
{description ? (
|
|
<div className="flex min-w-0 flex-col">
|
|
{label}
|
|
{description}
|
|
</div>
|
|
) : (
|
|
label
|
|
)}
|
|
</HeroDropdown.Item>
|
|
)
|
|
}
|
|
|
|
function normalizeElementKey(key: ReactElement['key'], index: number): string {
|
|
if (key == null) return String(index)
|
|
|
|
const value = String(key)
|
|
|
|
return value.startsWith('.$') ? value.slice(2) : value
|
|
}
|
|
|
|
function renderMenuChildren(children: ReactNode, showIndicator: boolean): ReactNode {
|
|
return Children.toArray(children).map((child, index) => {
|
|
if (!isValidElement(child)) return null
|
|
|
|
if (child.type === DropdownSection) {
|
|
const section = child as ReactElement<DropdownSectionProps>
|
|
|
|
return (
|
|
<Fragment key={normalizeElementKey(section.key, index)}>
|
|
<HeroDropdown.Section>
|
|
{section.props.title ? <Header>{section.props.title}</Header> : null}
|
|
{renderMenuChildren(section.props.children, showIndicator)}
|
|
</HeroDropdown.Section>
|
|
{section.props.showDivider ? <Separator /> : null}
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
return renderItem(child as ReactElement<DropdownItemProps>, index, showIndicator)
|
|
})
|
|
}
|
|
|
|
export function Dropdown({ children, placement, trigger }: DropdownProps) {
|
|
const parts = Children.toArray(children).filter(isValidElement)
|
|
const triggerElement = parts.find((part) => part.type === DropdownTrigger) as ReactElement<{ children?: ReactNode }> | undefined
|
|
const menuElement = parts.find((part) => part.type === DropdownMenu) as ReactElement<DropdownMenuProps> | undefined
|
|
|
|
if (!triggerElement || !menuElement) return null
|
|
|
|
const menuChildren = menuElement.props.children
|
|
const menuProps = {
|
|
'aria-label': menuElement.props['aria-label'],
|
|
disabledKeys: menuElement.props.disabledKeys,
|
|
disallowEmptySelection: menuElement.props.disallowEmptySelection,
|
|
selectedKeys: menuElement.props.selectedKeys,
|
|
selectionMode: menuElement.props.selectionMode,
|
|
onAction: menuElement.props.onAction,
|
|
onSelectionChange: menuElement.props.onSelectionChange,
|
|
}
|
|
const showIndicator = menuProps.selectionMode === 'single' || menuProps.selectionMode === 'multiple'
|
|
const normalizedPlacement = placement?.replace('-', ' ') as ComponentProps<typeof HeroDropdown.Popover>['placement']
|
|
|
|
return (
|
|
<HeroDropdown trigger={trigger}>
|
|
{triggerElement.props.children}
|
|
<HeroDropdown.Popover placement={normalizedPlacement}>
|
|
<HeroDropdown.Menu {...menuProps}>{renderMenuChildren(menuChildren, showIndicator)}</HeroDropdown.Menu>
|
|
</HeroDropdown.Popover>
|
|
</HeroDropdown>
|
|
)
|
|
}
|