Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
184 lines
5.6 KiB
TypeScript
184 lines
5.6 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 } from '@/components/consumer/input/consumerInputTypes'
|
|
import {
|
|
CONSUMER_CHECKBOX_CLASS_NAMES,
|
|
CONSUMER_FIELD_DESCRIPTION_CLASS,
|
|
CONSUMER_FIELD_LABEL_CLASS,
|
|
CONSUMER_RADIO_CLASS_NAMES,
|
|
buildConsumerOtpSegmentClass,
|
|
withConsumerFieldError,
|
|
} from '@/components/consumer/input/consumerInputStyles'
|
|
import { Checkbox, Radio, RadioGroup, Switch } from '@/components/heroui/SelectionControls'
|
|
import { InputOtp } from '@/components/heroui/InputOtp'
|
|
import { getOptionValue, type SelectOptionItem } from '@/components/formElements/input/inputUtils'
|
|
import { coerceToString } from '@/helpers'
|
|
import { cn } from '@/lib/cn'
|
|
|
|
interface Props {
|
|
autoFocus?: boolean
|
|
className?: string
|
|
control: Control
|
|
description?: ReactNode
|
|
disabled?: boolean
|
|
kind: 'radio' | 'checkbox' | 'otp' | 'switch'
|
|
label?: string
|
|
name: string
|
|
options: SelectOptionItem[]
|
|
orientation: 'horizontal' | 'vertical'
|
|
otpClassNames?: { segment?: string; segmentWrapper?: string }
|
|
otpLength: number
|
|
placeholder?: string
|
|
radius: ConsumerInputRadius
|
|
readonly?: boolean
|
|
required?: boolean
|
|
selectKey: string
|
|
size: ConsumerInputSize
|
|
}
|
|
|
|
export default function ConsumerChoiceField({
|
|
autoFocus,
|
|
className,
|
|
control,
|
|
description,
|
|
disabled,
|
|
kind,
|
|
label,
|
|
name,
|
|
options,
|
|
orientation,
|
|
otpClassNames,
|
|
otpLength,
|
|
placeholder,
|
|
radius,
|
|
readonly,
|
|
required,
|
|
selectKey,
|
|
size,
|
|
}: Props) {
|
|
const otpSegmentClass = otpClassNames?.segment ?? buildConsumerOtpSegmentClass({ radius, size })
|
|
|
|
return (
|
|
<Controller
|
|
control={control}
|
|
name={name}
|
|
render={({ field, fieldState: { error } }) => {
|
|
const invalid = !!error
|
|
const otpSegment = invalid ? withConsumerFieldError(otpSegmentClass) : otpSegmentClass
|
|
|
|
if (kind === 'otp') {
|
|
return (
|
|
<ConsumerFieldShell
|
|
className={className}
|
|
description={description}
|
|
errorMessage={error?.message}
|
|
invalid={invalid}
|
|
label={label}
|
|
required={required}
|
|
>
|
|
<InputOtp
|
|
{...field}
|
|
fullWidth
|
|
autoComplete="one-time-code"
|
|
autoFocus={autoFocus}
|
|
classNames={{
|
|
segment: otpSegment,
|
|
segmentWrapper: otpClassNames?.segmentWrapper ?? 'flex-row-reverse justify-center gap-2',
|
|
}}
|
|
inputMode="numeric"
|
|
isDisabled={disabled}
|
|
isInvalid={invalid}
|
|
isReadOnly={readonly}
|
|
isRequired={required}
|
|
length={otpLength}
|
|
placeholder={placeholder}
|
|
size={size}
|
|
validationBehavior="aria"
|
|
variant="bordered"
|
|
/>
|
|
</ConsumerFieldShell>
|
|
)
|
|
}
|
|
|
|
if (kind === 'radio') {
|
|
return (
|
|
<RadioGroup
|
|
{...field}
|
|
className={className}
|
|
classNames={{
|
|
label: cn(CONSUMER_FIELD_LABEL_CLASS, invalid && '!text-fourth'),
|
|
}}
|
|
description={description}
|
|
descriptionClassName={CONSUMER_FIELD_DESCRIPTION_CLASS}
|
|
errorMessage={error?.message}
|
|
isDisabled={disabled}
|
|
isInvalid={invalid}
|
|
isReadOnly={readonly}
|
|
isRequired={required}
|
|
label={label}
|
|
orientation={orientation}
|
|
size={size}
|
|
validationBehavior="aria"
|
|
value={field.value}
|
|
onValueChange={field.onChange}
|
|
>
|
|
{options.map((item) => (
|
|
<Radio
|
|
key={coerceToString(getOptionValue(item, selectKey) ?? getOptionValue(item, 'name'))}
|
|
classNames={CONSUMER_RADIO_CLASS_NAMES}
|
|
value={coerceToString(getOptionValue(item, selectKey))}
|
|
>
|
|
{coerceToString(getOptionValue(item, 'name'))}
|
|
</Radio>
|
|
))}
|
|
</RadioGroup>
|
|
)
|
|
}
|
|
|
|
if (kind === 'checkbox') {
|
|
return (
|
|
<div className={cn('flex flex-col gap-1', className)}>
|
|
<Checkbox
|
|
{...field}
|
|
classNames={CONSUMER_CHECKBOX_CLASS_NAMES}
|
|
isDisabled={disabled}
|
|
isInvalid={invalid}
|
|
isReadOnly={readonly}
|
|
isSelected={field.value}
|
|
size={size}
|
|
onValueChange={field.onChange}
|
|
>
|
|
{label}
|
|
</Checkbox>
|
|
{description ? <p className={CONSUMER_FIELD_DESCRIPTION_CLASS}>{description}</p> : null}
|
|
{invalid && error?.message ? <p className="text-sm text-fourth">{error.message}</p> : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={cn('flex flex-col gap-1', className)}>
|
|
<Switch
|
|
classNames={{ label: CONSUMER_FIELD_LABEL_CLASS }}
|
|
isDisabled={disabled}
|
|
isSelected={field.value}
|
|
name={field.name}
|
|
size={size}
|
|
onBlur={field.onBlur}
|
|
onValueChange={field.onChange}
|
|
>
|
|
{label}
|
|
</Switch>
|
|
{description ? <p className={CONSUMER_FIELD_DESCRIPTION_CLASS}>{description}</p> : null}
|
|
</div>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
}
|