Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
'use client'
|
||
|
||
import type { ComponentProps, ReactNode } from 'react'
|
||
|
||
import { Description, FieldError, Label, NumberField } from '@heroui/react'
|
||
|
||
import { cn } from '@/lib/cn'
|
||
|
||
type NumberInputProps = Omit<ComponentProps<typeof NumberField>, 'children' | 'className' | 'onChange' | 'variant'> & {
|
||
className?: string
|
||
classNames?: { inputWrapper?: string; label?: string; input?: string }
|
||
description?: ReactNode
|
||
descriptionClassName?: string
|
||
endContent?: ReactNode
|
||
errorMessage?: ReactNode
|
||
formatOptions?: Intl.NumberFormatOptions
|
||
isClearable?: boolean
|
||
label?: ReactNode
|
||
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
||
size?: 'sm' | 'md' | 'lg'
|
||
startContent?: ReactNode
|
||
variant?: 'flat' | 'bordered' | 'faded' | 'underlined'
|
||
onClear?: () => void
|
||
onValueChange?: (value: number) => void
|
||
}
|
||
|
||
function radiusClass(radius: NumberInputProps['radius'], variant: NumberInputProps['variant']): string {
|
||
if (variant === 'underlined') return 'rounded-none'
|
||
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'
|
||
}
|
||
|
||
export function NumberInput({
|
||
className,
|
||
classNames,
|
||
description,
|
||
descriptionClassName,
|
||
endContent,
|
||
errorMessage,
|
||
isClearable,
|
||
isInvalid,
|
||
label,
|
||
radius,
|
||
size,
|
||
startContent,
|
||
variant,
|
||
value,
|
||
onClear,
|
||
onValueChange,
|
||
...props
|
||
}: NumberInputProps) {
|
||
return (
|
||
<NumberField
|
||
{...props}
|
||
className={cn('flex w-full flex-col gap-1', className)}
|
||
isInvalid={isInvalid}
|
||
value={value}
|
||
variant={variant === 'bordered' || variant === 'underlined' ? 'secondary' : 'primary'}
|
||
onChange={onValueChange}
|
||
>
|
||
{label ? <Label className={cn('labelClass', classNames?.label)}>{label}</Label> : null}
|
||
<NumberField.Group
|
||
className={cn(
|
||
'relative flex w-full items-center',
|
||
!classNames?.inputWrapper &&
|
||
cn(
|
||
'border border-divider bg-surface px-3',
|
||
size === 'sm' ? 'min-h-8' : size === 'lg' ? 'min-h-12' : 'min-h-10',
|
||
radiusClass(radius, variant)
|
||
),
|
||
classNames?.inputWrapper,
|
||
// Always keep a single red border on error (never HeroUI invalid-field-ring outline).
|
||
isInvalid && '!border-fourth hover:!border-fourth focus-within:!border-fourth data-[invalid=true]:!border-fourth'
|
||
)}
|
||
>
|
||
{startContent}
|
||
<NumberField.Input
|
||
className={cn(
|
||
'min-w-0 flex-1 bg-transparent text-left outline-none',
|
||
classNames?.input,
|
||
isClearable && value !== undefined && value !== null && 'pl-8'
|
||
)}
|
||
dir="ltr"
|
||
/>
|
||
{isClearable && value !== undefined && value !== null ? (
|
||
<button
|
||
aria-label="پاک کردن"
|
||
className="absolute left-2 top-1/2 z-10 flex size-5 -translate-y-1/2 shrink-0 items-center justify-center rounded-full bg-default-200 text-sm leading-none text-default-500 transition-colors hover:bg-default-300"
|
||
type="button"
|
||
onClick={onClear}
|
||
>
|
||
×
|
||
</button>
|
||
) : (
|
||
endContent
|
||
)}
|
||
</NumberField.Group>
|
||
{description ? <Description className={descriptionClassName}>{description}</Description> : null}
|
||
{isInvalid && errorMessage ? <FieldError>{errorMessage}</FieldError> : null}
|
||
</NumberField>
|
||
)
|
||
}
|