Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
171 lines
5.2 KiB
TypeScript
171 lines
5.2 KiB
TypeScript
'use client'
|
||
|
||
import type { ReactNode } from 'react'
|
||
|
||
import { Controller, type Control } from 'react-hook-form'
|
||
|
||
import ConsumerFieldShell from '@/components/consumer/input/ConsumerFieldShell'
|
||
import type { ConsumerInputRadius, ConsumerInputSize, ConsumerInputTone } from '@/components/consumer/input/consumerInputTypes'
|
||
import {
|
||
CONSUMER_FIELD_SELECT_PLACEHOLDER_CLASS,
|
||
CONSUMER_FIELD_SELECT_POPOVER_CLASS,
|
||
CONSUMER_FIELD_SELECT_VALUE_CLASS,
|
||
buildConsumerFieldWrapper,
|
||
resolveConsumerTone,
|
||
withConsumerFieldError,
|
||
} from '@/components/consumer/input/consumerInputStyles'
|
||
import { Select, SelectItem } from '@/components/heroui/Select'
|
||
import { getOptionValue, type SelectOptionItem } from '@/components/formElements/input/inputUtils'
|
||
import { useDialogPortalContainer } from '@/components/formElements/input/useDialogPortalContainer'
|
||
import { coerceToString } from '@/helpers'
|
||
import { cn } from '@/lib/cn'
|
||
|
||
interface Props {
|
||
ariaLabel?: string
|
||
autoFocus?: boolean
|
||
className?: string
|
||
radius: ConsumerInputRadius
|
||
size: ConsumerInputSize
|
||
control: Control
|
||
description?: ReactNode
|
||
disabled?: boolean
|
||
iconEnd?: ReactNode
|
||
iconStart?: ReactNode
|
||
inputWrapper?: string
|
||
label?: string
|
||
multiple?: boolean
|
||
name: string
|
||
options: SelectOptionItem[]
|
||
placeholder?: string
|
||
readonly?: boolean
|
||
required?: boolean
|
||
selectKey: string
|
||
selectValue: string
|
||
tone?: ConsumerInputTone
|
||
}
|
||
|
||
export default function ConsumerSelectField({
|
||
ariaLabel,
|
||
autoFocus,
|
||
className,
|
||
radius,
|
||
size,
|
||
control,
|
||
description,
|
||
disabled,
|
||
iconEnd,
|
||
iconStart,
|
||
label,
|
||
multiple,
|
||
name,
|
||
options,
|
||
placeholder,
|
||
readonly: _readonly,
|
||
required,
|
||
selectKey,
|
||
selectValue,
|
||
tone,
|
||
inputWrapper,
|
||
}: Props) {
|
||
const { portalContainer, setHostEl } = useDialogPortalContainer()
|
||
const resolvedTone = resolveConsumerTone('select', tone)
|
||
const wrapper = cn(
|
||
buildConsumerFieldWrapper({
|
||
radius: radius,
|
||
size: size,
|
||
tone: resolvedTone,
|
||
}),
|
||
inputWrapper
|
||
)
|
||
|
||
return (
|
||
<Controller
|
||
control={control}
|
||
name={name}
|
||
render={({ field, fieldState: { error } }) => {
|
||
const invalid = !!error
|
||
const triggerWrapper = invalid ? withConsumerFieldError(wrapper) : wrapper
|
||
const selectedKeys = multiple
|
||
? field.value
|
||
: field.value !== undefined && field.value !== null && field.value !== ''
|
||
? [String(field.value)]
|
||
: []
|
||
|
||
return (
|
||
<div
|
||
ref={setHostEl}
|
||
className={cn('flex w-full flex-col', className)}
|
||
>
|
||
<ConsumerFieldShell
|
||
description={description}
|
||
errorMessage={error?.message}
|
||
invalid={invalid}
|
||
label={label}
|
||
required={required}
|
||
>
|
||
<Select
|
||
ref={field.ref}
|
||
aria-label={ariaLabel ?? label ?? name}
|
||
autoFocus={autoFocus}
|
||
classNames={{
|
||
placeholder: CONSUMER_FIELD_SELECT_PLACEHOLDER_CLASS,
|
||
trigger: triggerWrapper,
|
||
value: CONSUMER_FIELD_SELECT_VALUE_CLASS,
|
||
}}
|
||
endContent={iconEnd}
|
||
isDisabled={disabled}
|
||
isInvalid={invalid}
|
||
name={field.name}
|
||
placeholder={placeholder}
|
||
popoverProps={{
|
||
placement: 'bottom-start',
|
||
portalContainer,
|
||
shouldBlockScroll: false,
|
||
classNames: { content: CONSUMER_FIELD_SELECT_POPOVER_CLASS },
|
||
}}
|
||
selectedKeys={selectedKeys}
|
||
selectionMode={multiple ? 'multiple' : 'single'}
|
||
size={size}
|
||
startContent={iconStart}
|
||
validationBehavior="aria"
|
||
variant="bordered"
|
||
onBlur={field.onBlur}
|
||
onSelectionChange={(keys) => {
|
||
if (multiple) field.onChange(keys)
|
||
else if (keys === 'all') field.onChange('')
|
||
else field.onChange(String(Array.from(keys as Set<React.Key>)[0] ?? ''))
|
||
}}
|
||
>
|
||
{options.map((item, index) => {
|
||
const optionKey = coerceToString(getOptionValue(item, selectKey)) || `select-item-${index}`
|
||
const optionLabel = coerceToString(getOptionValue(item, selectValue))
|
||
const depth = Number(getOptionValue(item, 'depth') ?? 0)
|
||
|
||
return (
|
||
<SelectItem
|
||
key={optionKey}
|
||
textValue={optionLabel}
|
||
>
|
||
{depth > 0 ? (
|
||
<div
|
||
className="flex items-center gap-2"
|
||
style={{ paddingRight: depth * 20 }}
|
||
>
|
||
<span className="select-none text-tertiary-300">–</span>
|
||
<span className="truncate">{optionLabel}</span>
|
||
</div>
|
||
) : (
|
||
optionLabel
|
||
)}
|
||
</SelectItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</ConsumerFieldShell>
|
||
</div>
|
||
)
|
||
}}
|
||
/>
|
||
)
|
||
}
|