admin/components/consumer/ConsumerInput.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

244 lines
7.1 KiB
TypeScript

'use client'
/**
* ConsumerInput — consumer-only form fields (direct heroui + RHF).
*
* **Design props**
* - `size`: `sm` | `md` | `lg`
* - `radius`: `full` (pill) | `control` (10px)
* - `tone`: `muted` (#E2E2E280 fill) | `bordered` (white + border). Select is always `muted`.
*
* Use in `app/(consumer)/**` and `components/consumer/**` instead of
* `components/formElements/Input`.
*
* Guidelines: `frontend/docs/consumer-ui-guidelines.md`
*/
import { useEffect } from 'react'
import { FormProvider, useForm, useFormContext, type UseFormReturn } from 'react-hook-form'
import ConsumerChoiceField from '@/components/consumer/input/ConsumerChoiceField'
import ConsumerSelectField from '@/components/consumer/input/ConsumerSelectField'
import ConsumerTextareaField from '@/components/consumer/input/ConsumerTextareaField'
import { ConsumerNumberField, ConsumerTextField } from '@/components/consumer/input/ConsumerTextField'
import { defaultValueForConsumerField, type ConsumerInputProps } from '@/components/consumer/input/consumerInputTypes'
import { texts, format } from '@/texts'
export type { ConsumerInputProps } from '@/components/consumer/input/consumerInputTypes'
function useOptionalFormContext(): UseFormReturn | null {
const context = useFormContext() as UseFormReturn | null
return context?.control ? context : null
}
function ConsumerInputFields({
radius = 'control',
size = 'md',
tone,
isClearable = false,
inputWrapper,
orientation = 'vertical',
direction = 'rtl',
englishDigitsOnly = false,
otpLength = 4,
otpClassNames,
selectKey = 'code',
selectValue = 'name',
textAreaMinRows = 5,
...props
}: ConsumerInputProps) {
const { control } = useFormContext()
const placeholder =
props.placeholder ??
(props.generalType === 'select'
? undefined
: typeof props.label === 'string'
? format(texts.common.enterField, { label: props.label })
: '')
switch (props.generalType) {
case 'numberInput':
return (
<ConsumerNumberField
ariaLabel={props.ariaLabel}
autoFocus={props.autoFocus}
className={props.className}
control={control}
description={props.description}
direction={direction}
disabled={props.disabled}
englishDigitsOnly={englishDigitsOnly}
formatOptions={props.formatOptions}
iconEnd={props.iconEnd}
iconStart={props.iconStart}
inputWrapper={inputWrapper}
isClearable={isClearable}
label={props.label}
labelIcon={props.labelIcon}
maxValue={props.maxValue}
minValue={props.minValue}
name={props.name}
placeholder={placeholder}
radius={radius}
readonly={props.readonly}
required={props.required}
size={size}
tone={tone}
onBlur={props.onBlur}
onClear={props.onClear}
/>
)
case 'input':
return (
<ConsumerTextField
ariaLabel={props.ariaLabel}
autoFocus={props.autoFocus}
className={props.className}
control={control}
description={props.description}
direction={direction}
disabled={props.disabled}
englishDigitsOnly={englishDigitsOnly}
iconEnd={props.iconEnd}
iconStart={props.iconStart}
inputType={props.inputType}
inputWrapper={inputWrapper}
isClearable={isClearable}
label={props.label}
labelIcon={props.labelIcon}
maxValue={props.maxValue}
minValue={props.minValue}
name={props.name}
placeholder={placeholder}
radius={radius}
readonly={props.readonly}
required={props.required}
size={size}
tone={tone}
onBlur={props.onBlur}
onClear={props.onClear}
onKeyDown={props.onKeyDown}
/>
)
case 'textarea':
return (
<ConsumerTextareaField
autoFocus={props.autoFocus}
className={props.className}
control={control}
description={props.description}
disabled={props.disabled}
inputWrapper={inputWrapper}
isClearable={isClearable}
label={props.label}
name={props.name}
placeholder={placeholder}
radius={radius}
readonly={props.readonly}
required={props.required}
textAreaMinRows={textAreaMinRows}
tone={tone}
onBlur={props.onBlur}
/>
)
case 'select':
return (
<ConsumerSelectField
ariaLabel={props.ariaLabel}
autoFocus={props.autoFocus}
className={props.className}
control={control}
description={props.description}
disabled={props.disabled}
iconEnd={props.iconEnd}
iconStart={props.iconStart}
inputWrapper={inputWrapper}
label={props.label}
multiple={props.multiple}
name={props.name}
options={props.selectOptions ?? []}
placeholder={placeholder}
radius={radius}
readonly={props.readonly}
required={props.required}
selectKey={selectKey}
selectValue={selectValue}
size={size}
tone={tone}
/>
)
case 'radio':
case 'checkbox':
case 'otp':
case 'switch':
return (
<ConsumerChoiceField
autoFocus={props.autoFocus}
className={props.className}
control={control}
description={props.description}
disabled={props.disabled}
kind={props.generalType}
label={props.label}
name={props.name}
options={props.radioOptions ?? []}
orientation={orientation}
otpClassNames={otpClassNames}
otpLength={otpLength}
placeholder={placeholder}
radius={radius}
readonly={props.readonly}
required={props.required}
selectKey={selectKey}
size={size}
/>
)
default:
return null
}
}
function ConsumerInputStandalone(props: ConsumerInputProps) {
const { name, generalType, value, onValueChange } = props
const methods = useForm({
defaultValues: { [name]: value ?? defaultValueForConsumerField(generalType) },
values: value !== undefined ? { [name]: value } : undefined,
})
useEffect(() => {
if (!onValueChange) return
const subscription = methods.watch((values, info) => {
if (info.name && info.name !== name) return
onValueChange(values[name])
})
return () => {
subscription.unsubscribe()
}
}, [methods, name, onValueChange])
return (
<FormProvider {...methods}>
<ConsumerInputFields {...props} />
</FormProvider>
)
}
export default function ConsumerInput(props: ConsumerInputProps) {
const form = useOptionalFormContext()
const isControlled = props.value !== undefined || props.onValueChange !== undefined
if (!form || isControlled) {
return <ConsumerInputStandalone {...props} />
}
return <ConsumerInputFields {...props} />
}