Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import type { ReactElement, ReactNode } from 'react'
|
|
|
|
import { Children, isValidElement, useId, useMemo } from 'react'
|
|
|
|
import { Input } from '@/components/heroui/Input'
|
|
|
|
type AutocompleteKey = string | number
|
|
|
|
interface AutocompleteItemProps {
|
|
children?: ReactNode
|
|
key?: AutocompleteKey
|
|
textValue?: string
|
|
}
|
|
|
|
export function AutocompleteItem(_props: AutocompleteItemProps) {
|
|
return null
|
|
}
|
|
|
|
interface AutocompleteProps<T extends object> {
|
|
'aria-label'?: string
|
|
autoFocus?: boolean
|
|
children: ReactNode | ((item: T) => ReactElement<AutocompleteItemProps>)
|
|
className?: string
|
|
defaultItems?: Iterable<T>
|
|
description?: ReactNode
|
|
descriptionClassName?: string
|
|
endContent?: ReactNode
|
|
errorMessage?: ReactNode
|
|
isDisabled?: boolean
|
|
isInvalid?: boolean
|
|
isLoading?: boolean
|
|
isReadOnly?: boolean
|
|
listboxProps?: object
|
|
placeholder?: string
|
|
popoverProps?: object
|
|
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
|
selectedKey?: AutocompleteKey | null
|
|
size?: 'sm' | 'md' | 'lg'
|
|
startContent?: ReactNode
|
|
type?: string
|
|
validationBehavior?: 'native' | 'aria'
|
|
variant?: 'flat' | 'bordered' | 'faded' | 'underlined'
|
|
onBlur?: () => void
|
|
onInputChange?: (value: string) => void
|
|
onSelectionChange?: (key: AutocompleteKey | null) => void
|
|
}
|
|
|
|
function normalizeKey(key: ReactElement['key'], index: number): string {
|
|
if (key == null) return String(index)
|
|
const value = String(key)
|
|
|
|
return value.startsWith('.$') ? value.slice(2) : value
|
|
}
|
|
|
|
export function Autocomplete<T extends object>({
|
|
children,
|
|
defaultItems,
|
|
selectedKey,
|
|
onInputChange,
|
|
onSelectionChange,
|
|
...props
|
|
}: AutocompleteProps<T>) {
|
|
const listId = useId()
|
|
const options = useMemo(() => {
|
|
const nodes = typeof children === 'function' ? Array.from(defaultItems ?? [], (item) => children(item)) : Children.toArray(children)
|
|
|
|
return Children.toArray(nodes)
|
|
.filter((child): child is ReactElement<AutocompleteItemProps> => isValidElement<AutocompleteItemProps>(child))
|
|
.map((child, index) => {
|
|
const id = normalizeKey(child.key, index)
|
|
const label = child.props.textValue ?? (typeof child.props.children === 'string' ? child.props.children : id)
|
|
|
|
return { id, label }
|
|
})
|
|
}, [children, defaultItems])
|
|
const selectedLabel = options.find((option) => option.id === String(selectedKey ?? ''))?.label ?? ''
|
|
|
|
return (
|
|
<>
|
|
<Input
|
|
{...props}
|
|
defaultValue={selectedLabel}
|
|
list={listId}
|
|
onValueChange={(value) => {
|
|
onInputChange?.(value)
|
|
const match = options.find((option) => option.label === value || option.id === value)
|
|
|
|
onSelectionChange?.(match?.id ?? null)
|
|
}}
|
|
/>
|
|
<datalist id={listId}>
|
|
{options.map((option) => (
|
|
<option
|
|
key={option.id}
|
|
value={option.label}
|
|
/>
|
|
))}
|
|
</datalist>
|
|
</>
|
|
)
|
|
}
|