Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
151 lines
5.8 KiB
TypeScript
151 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import type { DatePickerRef } from 'react-multi-date-picker'
|
|
|
|
import { useRef, type ReactNode } from 'react'
|
|
import DatePicker from 'react-multi-date-picker'
|
|
import TimePicker from 'react-multi-date-picker/plugins/time_picker'
|
|
import persian from 'react-date-object/calendars/persian'
|
|
import persianFa from 'react-date-object/locales/persian_fa'
|
|
import { Controller, type Control } from 'react-hook-form'
|
|
|
|
import { cn } from '@/lib/cn'
|
|
import { buildOutsideLabel } from '@/components/formElements/input/inputUtils'
|
|
import CalendarIcon from '@/components/icons/CalendarIcon'
|
|
import { convertMinutesToTime, convertTimeToMinutes } from '@/helpers'
|
|
import { convertToISOFormat } from '@/helpers/dates'
|
|
|
|
type Variant = 'flat' | 'bordered' | 'faded' | 'underlined'
|
|
interface Props {
|
|
autoFocus?: boolean
|
|
className?: string
|
|
control: Control
|
|
description?: ReactNode
|
|
disabled?: boolean
|
|
finalInputWrapper: string
|
|
kind: 'datePicker' | 'datePickerPro' | 'timePicker'
|
|
label?: string
|
|
labelIcon?: ReactNode
|
|
maxDate?: string
|
|
minDate: string
|
|
name: string
|
|
onlyYearPicker?: boolean
|
|
placeholder?: string
|
|
radius: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
|
readonly?: boolean
|
|
required?: boolean
|
|
size?: 'sm' | 'md' | 'lg'
|
|
variant: Variant
|
|
viewportWidth: number
|
|
}
|
|
|
|
function pickerStyles(size: Props['size'], variant: Variant, invalid = false) {
|
|
const height = size === 'sm' ? '36px' : size === 'lg' ? '48px' : '40px'
|
|
const error = 'var(--color-fourth)'
|
|
const border = invalid ? error : '#e4e4e7'
|
|
const base = { width: '100%', boxSizing: 'border-box' as const, height, borderRadius: '12px', color: 'black', padding: '0 12px' }
|
|
|
|
if (variant === 'bordered') return { ...base, background: 'transparent', border: `1px solid ${border}`, borderColor: border }
|
|
if (variant === 'faded') return { ...base, background: '#f4f4f5', border: `1px solid ${border}`, borderColor: border }
|
|
if (variant === 'underlined')
|
|
return {
|
|
...base,
|
|
background: 'transparent',
|
|
border: 'none',
|
|
borderBottom: `1px solid ${border}`,
|
|
borderRadius: '0',
|
|
borderColor: border,
|
|
}
|
|
|
|
return { ...base, background: '#f4f4f5', border: invalid ? `1px solid ${error}` : 'none', borderColor: invalid ? error : '#f4f4f5' }
|
|
}
|
|
|
|
export default function DateFieldControl(props: Props) {
|
|
const pickerRef = useRef<DatePickerRef | null>(null)
|
|
|
|
return (
|
|
<Controller
|
|
control={props.control}
|
|
name={props.name}
|
|
render={({ field, fieldState: { error } }) => {
|
|
if (props.kind === 'timePicker') {
|
|
const time = convertMinutesToTime(field.value)
|
|
|
|
return (
|
|
<div className="relative flex flex-col">
|
|
<div className="labelClass mb-1">{buildOutsideLabel(props.label, null, props.required)}</div>
|
|
<DatePicker
|
|
ref={pickerRef}
|
|
disableDayPicker
|
|
portal
|
|
calendar={persian}
|
|
calendarPosition="bottom-right"
|
|
className="date-picker-input"
|
|
containerStyle={{ width: '100%' }}
|
|
disabled={props.disabled}
|
|
format="HH:mm"
|
|
locale={persianFa}
|
|
placeholder={props.placeholder}
|
|
plugins={[
|
|
<TimePicker
|
|
key="timePicker"
|
|
hideSeconds
|
|
/>,
|
|
]}
|
|
readOnly={props.readonly}
|
|
style={pickerStyles(props.size, props.variant)}
|
|
value={new Date().setHours(Number(time.hours), Number(time.minutes), 0, 0)}
|
|
onChange={(value) => {
|
|
field.onChange(convertTimeToMinutes(`${value?.hour}:${value?.minute}`))
|
|
pickerRef.current?.closeCalendar()
|
|
}}
|
|
/>
|
|
{error && <span className="text-red-500">{error.message}</span>}
|
|
{props.description && <p>{props.description}</p>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const raw = field.value
|
|
const valid = typeof raw === 'string' && raw.trim() !== '' && !Number.isNaN(new Date(raw).getTime())
|
|
|
|
return (
|
|
<div className="relative flex flex-col gap-1">
|
|
<div className={cn('labelClass flex flex-row flex-nowrap items-center', error && '!text-[var(--color-fourth)]')}>
|
|
{buildOutsideLabel(props.label, props.labelIcon ?? <CalendarIcon className="size-4" />, props.required, 0)}
|
|
</div>
|
|
<div className="rounded-[12px]">
|
|
<DatePicker
|
|
ref={pickerRef}
|
|
portal
|
|
showOtherDays
|
|
calendar={persian}
|
|
calendarPosition="bottom-right"
|
|
className={cn('date-picker-input', error && 'date-picker-input--invalid')}
|
|
containerStyle={{ width: '100%' }}
|
|
disabled={props.disabled}
|
|
format={props.onlyYearPicker ? 'YYYY' : 'YYYY/MM/DD'}
|
|
locale={persianFa}
|
|
mapDays={({ date }) => (date.weekDay.index === 6 ? { className: 'highlight highlight-red' } : {})}
|
|
maxDate={props.maxDate}
|
|
minDate={props.minDate}
|
|
onlyYearPicker={props.onlyYearPicker}
|
|
placeholder={props.placeholder}
|
|
readOnly={props.readonly}
|
|
style={pickerStyles(props.size, props.variant, !!error)}
|
|
value={valid ? new Date(raw) : null}
|
|
onChange={(value) => {
|
|
field.onChange(value ? convertToISOFormat(value) : '')
|
|
pickerRef.current?.closeCalendar()
|
|
}}
|
|
/>
|
|
</div>
|
|
{error && <p className="text-tiny text-fourth-900">{error.message}</p>}
|
|
{props.description && <p>{props.description}</p>}
|
|
</div>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
}
|