admin/components/consumer/input/ConsumerChoiceField.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

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>
)
}}
/>
)
}