- Introduced a new PATCH endpoint `/api/v1/notifications/me/read-all` to mark all unread notifications as read for the authenticated user.
- Added a new GET endpoint `/api/v1/events/{eventId}/faqs/page` for retrieving paginated FAQs related to a specific event, with support for pagination, sorting, and filtering.
- Updated the existing GET endpoint `/api/v1/events/{eventId}/guest-list-links/invited-guests/page` to include pagination parameters.
222 lines
7.0 KiB
TypeScript
222 lines
7.0 KiB
TypeScript
'use client'
|
|
|
|
import type { ComponentProps, CSSProperties, 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 DropdownTriggerProps {
|
|
children?: ReactNode
|
|
'aria-label'?: string
|
|
className?: string
|
|
isDisabled?: boolean
|
|
style?: CSSProperties
|
|
}
|
|
|
|
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 }: DropdownTriggerProps) {
|
|
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)
|
|
})
|
|
}
|
|
|
|
function isButtonLikeTrigger(child: ReactElement): boolean {
|
|
if (typeof child.type === 'string') return child.type === 'button'
|
|
|
|
const displayName =
|
|
typeof child.type === 'function' || typeof child.type === 'object'
|
|
? (child.type as { displayName?: string; name?: string }).displayName || (child.type as { name?: string }).name
|
|
: undefined
|
|
|
|
return displayName === 'ConsumerButton' || displayName === 'Button'
|
|
}
|
|
|
|
/** Button/`button` داخل MenuTrigger reliably باز نمیشود؛ محتوا را به Dropdown.Trigger واقعی منتقل میکنیم. */
|
|
function resolveTrigger(triggerElement: ReactElement<DropdownTriggerProps>): {
|
|
'aria-label'?: string
|
|
className?: string
|
|
content: ReactNode
|
|
isDisabled?: boolean
|
|
style?: CSSProperties
|
|
} {
|
|
const { children, className, isDisabled, style } = triggerElement.props
|
|
const ariaLabel = triggerElement.props['aria-label']
|
|
const onlyChild = Children.count(children) === 1 ? Children.toArray(children)[0] : null
|
|
|
|
if (isValidElement(onlyChild) && isButtonLikeTrigger(onlyChild)) {
|
|
const nested = onlyChild.props as {
|
|
'aria-label'?: string
|
|
children?: ReactNode
|
|
className?: string
|
|
disabled?: boolean
|
|
style?: CSSProperties
|
|
}
|
|
|
|
return {
|
|
'aria-label': nested['aria-label'] ?? ariaLabel,
|
|
className: nested.className ?? className,
|
|
content: nested.children,
|
|
isDisabled: nested.disabled ?? isDisabled,
|
|
style: nested.style ?? style,
|
|
}
|
|
}
|
|
|
|
return {
|
|
'aria-label': ariaLabel,
|
|
className,
|
|
content: children,
|
|
isDisabled,
|
|
style,
|
|
}
|
|
}
|
|
|
|
export function Dropdown({ children, placement, trigger }: DropdownProps) {
|
|
const parts = Children.toArray(children).filter(isValidElement)
|
|
const triggerElement = parts.find((part) => part.type === DropdownTrigger) as ReactElement<DropdownTriggerProps> | undefined
|
|
const menuElement = parts.find((part) => part.type === DropdownMenu) as ReactElement<DropdownMenuProps> | undefined
|
|
|
|
if (!triggerElement || !menuElement) return null
|
|
|
|
const resolvedTrigger = resolveTrigger(triggerElement)
|
|
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}>
|
|
<HeroDropdown.Trigger
|
|
aria-label={resolvedTrigger['aria-label']}
|
|
className={resolvedTrigger.className}
|
|
isDisabled={resolvedTrigger.isDisabled}
|
|
style={resolvedTrigger.style}
|
|
>
|
|
{resolvedTrigger.content}
|
|
</HeroDropdown.Trigger>
|
|
<HeroDropdown.Popover placement={normalizedPlacement}>
|
|
<HeroDropdown.Menu {...menuProps}>{renderMenuChildren(menuChildren, showIndicator)}</HeroDropdown.Menu>
|
|
</HeroDropdown.Popover>
|
|
</HeroDropdown>
|
|
)
|
|
}
|