Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
'use client'
|
||
|
||
import type { ChangeEvent, ComponentProps, ReactNode } from 'react'
|
||
|
||
import { forwardRef } from 'react'
|
||
import { Description, FieldError, type Input as HeroInput, InputGroup, Label, TextArea as HeroTextarea, TextField } from '@heroui/react'
|
||
|
||
import { cn } from '@/lib/cn'
|
||
|
||
interface LegacyClassNames {
|
||
base?: string
|
||
input?: string
|
||
inputWrapper?: string
|
||
innerWrapper?: string
|
||
label?: string
|
||
}
|
||
|
||
interface SharedProps {
|
||
'aria-label'?: string
|
||
autoFocus?: boolean
|
||
className?: string
|
||
classNames?: LegacyClassNames
|
||
defaultValue?: string
|
||
description?: ReactNode
|
||
descriptionClassName?: string
|
||
disabled?: boolean
|
||
endContent?: ReactNode
|
||
errorMessage?: ReactNode
|
||
isClearable?: boolean
|
||
isDisabled?: boolean
|
||
isInvalid?: boolean
|
||
isReadOnly?: boolean
|
||
isRequired?: boolean
|
||
label?: ReactNode
|
||
labelPlacement?: 'inside' | 'outside' | 'outside-left'
|
||
name?: string
|
||
placeholder?: string
|
||
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
||
size?: 'sm' | 'md' | 'lg'
|
||
startContent?: ReactNode
|
||
validationBehavior?: 'native' | 'aria'
|
||
value?: string
|
||
variant?: 'flat' | 'bordered' | 'faded' | 'underlined'
|
||
onBlur?: ComponentProps<typeof HeroInput>['onBlur']
|
||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void
|
||
onClear?: () => void
|
||
onValueChange?: (value: string) => void
|
||
}
|
||
|
||
export type InputProps = SharedProps &
|
||
Omit<
|
||
ComponentProps<typeof HeroInput>,
|
||
'children' | 'className' | 'defaultValue' | 'disabled' | 'onChange' | 'readOnly' | 'required' | 'size' | 'value' | 'variant'
|
||
>
|
||
|
||
function radiusClass(radius: SharedProps['radius']): string {
|
||
if (!radius) return ''
|
||
if (radius === 'none') return 'rounded-none'
|
||
if (radius === 'full') return 'rounded-full'
|
||
if (radius === 'lg') return 'rounded-2xl'
|
||
if (radius === 'sm') return 'rounded-md'
|
||
|
||
return 'rounded-lg'
|
||
}
|
||
|
||
function sizeClass(size: SharedProps['size']): string {
|
||
if (size === 'sm') return 'min-h-8'
|
||
if (size === 'lg') return 'min-h-12'
|
||
|
||
return 'min-h-10'
|
||
}
|
||
|
||
function variantClass(variant: SharedProps['variant'], invalid?: boolean): string {
|
||
if (invalid) return 'border-fourth'
|
||
if (variant === 'underlined') return 'rounded-none border-x-0 border-t-0'
|
||
if (variant === 'flat' || variant === 'faded') return 'border-transparent bg-default-100'
|
||
|
||
return 'border-divider bg-surface'
|
||
}
|
||
|
||
const clearButtonClassName =
|
||
'flex size-5 shrink-0 items-center justify-center rounded-full bg-default-200 text-sm leading-none text-default-500 transition-colors hover:bg-default-300'
|
||
|
||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||
{
|
||
className,
|
||
classNames,
|
||
description,
|
||
descriptionClassName,
|
||
disabled,
|
||
endContent,
|
||
errorMessage,
|
||
isClearable,
|
||
isDisabled,
|
||
isInvalid,
|
||
isReadOnly,
|
||
isRequired,
|
||
label,
|
||
labelPlacement: _labelPlacement,
|
||
radius,
|
||
size,
|
||
startContent,
|
||
validationBehavior,
|
||
variant,
|
||
onChange,
|
||
onClear,
|
||
onValueChange,
|
||
...props
|
||
},
|
||
ref
|
||
) {
|
||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||
onChange?.(event)
|
||
onValueChange?.(event.target.value)
|
||
}
|
||
|
||
return (
|
||
<TextField
|
||
className={cn('flex min-w-0 w-full flex-col gap-1', classNames?.base, className)}
|
||
isDisabled={isDisabled || disabled}
|
||
isInvalid={isInvalid}
|
||
isReadOnly={isReadOnly}
|
||
isRequired={isRequired}
|
||
validationBehavior={validationBehavior}
|
||
>
|
||
{label ? <Label className={cn('labelClass', classNames?.label)}>{label}</Label> : null}
|
||
<InputGroup
|
||
fullWidth
|
||
className={cn(
|
||
'relative',
|
||
!classNames?.inputWrapper && cn('border', radiusClass(radius), sizeClass(size), variantClass(variant, isInvalid)),
|
||
classNames?.inputWrapper,
|
||
classNames?.innerWrapper
|
||
)}
|
||
variant={variant === 'bordered' || variant === 'underlined' ? 'secondary' : 'primary'}
|
||
>
|
||
{startContent ? <InputGroup.Prefix>{startContent}</InputGroup.Prefix> : null}
|
||
<InputGroup.Input
|
||
{...props}
|
||
ref={ref}
|
||
className={cn(isClearable && props.value && 'pl-8', classNames?.input)}
|
||
onChange={handleChange}
|
||
/>
|
||
{isClearable && props.value ? (
|
||
<button
|
||
aria-label="پاک کردن"
|
||
className={cn(clearButtonClassName, 'absolute left-2 top-1/2 z-10 -translate-y-1/2')}
|
||
type="button"
|
||
onClick={onClear}
|
||
>
|
||
×
|
||
</button>
|
||
) : endContent ? (
|
||
<InputGroup.Suffix>{endContent}</InputGroup.Suffix>
|
||
) : null}
|
||
</InputGroup>
|
||
{description ? <Description className={descriptionClassName}>{description}</Description> : null}
|
||
{isInvalid && errorMessage ? <FieldError>{errorMessage}</FieldError> : null}
|
||
</TextField>
|
||
)
|
||
})
|
||
|
||
export type TextareaProps = Omit<SharedProps, 'onBlur' | 'onChange'> &
|
||
Omit<
|
||
ComponentProps<typeof HeroTextarea>,
|
||
'children' | 'className' | 'defaultValue' | 'disabled' | 'onChange' | 'readOnly' | 'required' | 'size' | 'value' | 'variant'
|
||
> & {
|
||
minRows?: number
|
||
onBlur?: ComponentProps<typeof HeroTextarea>['onBlur']
|
||
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void
|
||
}
|
||
|
||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
|
||
{
|
||
className,
|
||
classNames,
|
||
description,
|
||
descriptionClassName,
|
||
disabled,
|
||
errorMessage,
|
||
isClearable,
|
||
isDisabled,
|
||
isInvalid,
|
||
isReadOnly,
|
||
isRequired,
|
||
label,
|
||
labelPlacement: _labelPlacement,
|
||
minRows,
|
||
radius,
|
||
size,
|
||
validationBehavior,
|
||
variant,
|
||
onClear,
|
||
onValueChange,
|
||
...props
|
||
},
|
||
ref
|
||
) {
|
||
return (
|
||
<TextField
|
||
className={cn('flex min-w-0 w-full flex-col gap-1', classNames?.base, className)}
|
||
isDisabled={isDisabled || disabled}
|
||
isInvalid={isInvalid}
|
||
isReadOnly={isReadOnly}
|
||
isRequired={isRequired}
|
||
validationBehavior={validationBehavior}
|
||
>
|
||
{label ? <Label className={cn('labelClass', classNames?.label)}>{label}</Label> : null}
|
||
<div className="relative">
|
||
<HeroTextarea
|
||
{...props}
|
||
ref={ref}
|
||
fullWidth
|
||
className={cn(
|
||
!classNames?.inputWrapper && cn('border p-3', radiusClass(radius), sizeClass(size), variantClass(variant, isInvalid)),
|
||
classNames?.inputWrapper,
|
||
classNames?.input
|
||
)}
|
||
rows={minRows}
|
||
variant={variant === 'bordered' || variant === 'underlined' ? 'secondary' : 'primary'}
|
||
onChange={(event) => {
|
||
props.onChange?.(event)
|
||
onValueChange?.(event.target.value)
|
||
}}
|
||
/>
|
||
{isClearable && props.value ? (
|
||
<button
|
||
aria-label="پاک کردن"
|
||
className={cn(clearButtonClassName, 'absolute left-2 top-2')}
|
||
type="button"
|
||
onClick={onClear}
|
||
>
|
||
×
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
{description ? <Description className={descriptionClassName}>{description}</Description> : null}
|
||
{isInvalid && errorMessage ? <FieldError>{errorMessage}</FieldError> : null}
|
||
</TextField>
|
||
)
|
||
})
|