150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
|
|
import {
|
||
|
|
Button,
|
||
|
|
Calendar,
|
||
|
|
Field,
|
||
|
|
FieldDescription,
|
||
|
|
FieldError,
|
||
|
|
FormControl,
|
||
|
|
FormField,
|
||
|
|
Popover,
|
||
|
|
PopoverContent,
|
||
|
|
PopoverTrigger,
|
||
|
|
} from "@repo/shadcn-ui/components";
|
||
|
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||
|
|
import { format, isValid, parseISO } from "date-fns";
|
||
|
|
import { CalendarIcon, LockIcon } from "lucide-react";
|
||
|
|
import React from "react";
|
||
|
|
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
||
|
|
|
||
|
|
import { FormFieldLabel } from "./form-field-label.tsx";
|
||
|
|
|
||
|
|
type DatePickerFieldProps<TFormValues extends FieldValues> = {
|
||
|
|
name: FieldPath<TFormValues>;
|
||
|
|
|
||
|
|
label?: string;
|
||
|
|
description?: string;
|
||
|
|
|
||
|
|
disabled?: boolean;
|
||
|
|
required?: boolean;
|
||
|
|
readOnly?: boolean;
|
||
|
|
|
||
|
|
placeholder?: string;
|
||
|
|
|
||
|
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||
|
|
|
||
|
|
className?: string;
|
||
|
|
inputClassName?: string;
|
||
|
|
formatDateFn?: (value: string) => string;
|
||
|
|
};
|
||
|
|
|
||
|
|
const parseFieldDate = (value?: string): Date | undefined => {
|
||
|
|
if (!value) return undefined;
|
||
|
|
|
||
|
|
const parsed = parseISO(value);
|
||
|
|
if (!isValid(parsed)) return undefined;
|
||
|
|
|
||
|
|
return parsed;
|
||
|
|
};
|
||
|
|
|
||
|
|
const toDateOnlyString = (date: Date): string => {
|
||
|
|
return format(date, "yyyy-MM-dd");
|
||
|
|
};
|
||
|
|
|
||
|
|
export function DatePickerField<TFormValues extends FieldValues>({
|
||
|
|
name,
|
||
|
|
label,
|
||
|
|
placeholder,
|
||
|
|
description,
|
||
|
|
disabled = false,
|
||
|
|
required = false,
|
||
|
|
readOnly = false,
|
||
|
|
orientation = "vertical",
|
||
|
|
className,
|
||
|
|
inputClassName,
|
||
|
|
formatDateFn = (value) => {
|
||
|
|
const parsed = parseFieldDate(value);
|
||
|
|
return parsed ? format(parsed, "dd/MM/yyyy") : value;
|
||
|
|
},
|
||
|
|
}: DatePickerFieldProps<TFormValues>) {
|
||
|
|
const triggerId = React.useId();
|
||
|
|
const { control, formState } = useFormContext<TFormValues>();
|
||
|
|
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<FormField
|
||
|
|
control={control}
|
||
|
|
name={name}
|
||
|
|
render={({ field, fieldState }) => {
|
||
|
|
const selectedDate = parseFieldDate(field.value);
|
||
|
|
const displayValue = field.value ? formatDateFn(field.value) : null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Field
|
||
|
|
className={cn("gap-1", className)}
|
||
|
|
data-invalid={fieldState.invalid}
|
||
|
|
orientation={orientation}
|
||
|
|
>
|
||
|
|
{label ? (
|
||
|
|
<FormFieldLabel htmlFor={triggerId} required={required}>
|
||
|
|
{label}
|
||
|
|
</FormFieldLabel>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
<Popover>
|
||
|
|
<PopoverTrigger asChild>
|
||
|
|
<FormControl>
|
||
|
|
<Button
|
||
|
|
aria-invalid={fieldState.invalid}
|
||
|
|
aria-required={required || undefined}
|
||
|
|
className={cn(
|
||
|
|
"h-9 w-full justify-start bg-muted/50 text-left font-medium",
|
||
|
|
"hover:border-ring/60",
|
||
|
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||
|
|
!displayValue && "text-muted-foreground",
|
||
|
|
inputClassName
|
||
|
|
)}
|
||
|
|
disabled={isDisabled}
|
||
|
|
id={triggerId}
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
{readOnly ? (
|
||
|
|
<LockIcon aria-hidden="true" className="mr-2 h-4 w-4 opacity-70" />
|
||
|
|
) : (
|
||
|
|
<CalendarIcon aria-hidden="true" className="mr-2 h-4 w-4" />
|
||
|
|
)}
|
||
|
|
|
||
|
|
<span className="truncate">{displayValue ?? placeholder ?? "Select date"}</span>
|
||
|
|
</Button>
|
||
|
|
</FormControl>
|
||
|
|
</PopoverTrigger>
|
||
|
|
|
||
|
|
{isDisabled ? null : (
|
||
|
|
<PopoverContent align="start" className="w-auto p-0">
|
||
|
|
<Calendar
|
||
|
|
initialFocus
|
||
|
|
mode="single"
|
||
|
|
onSelect={(date) => {
|
||
|
|
field.onChange(date ? toDateOnlyString(date) : "");
|
||
|
|
field.onBlur();
|
||
|
|
}}
|
||
|
|
selected={selectedDate}
|
||
|
|
/>
|
||
|
|
</PopoverContent>
|
||
|
|
)}
|
||
|
|
</Popover>
|
||
|
|
|
||
|
|
{description ? (
|
||
|
|
<FieldDescription>{description}</FieldDescription>
|
||
|
|
) : (
|
||
|
|
<div aria-hidden="true" className="min-h-5" />
|
||
|
|
)}
|
||
|
|
|
||
|
|
<FieldError errors={[fieldState.error]} />
|
||
|
|
</Field>
|
||
|
|
);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|