5855ce33d4
- Introduced a clearable option for the DatePicker component, allowing users to easily reset the selected date. - Enhanced date handling logic to support conversion from Unix timestamps and improved value management for single and range selections. - Updated TenderListFilters to utilize the clearable feature in multiple DatePicker instances for better user experience. - Refactored useTenderListPresenter to manage date range filters more effectively, ensuring proper cleanup of cleared values.
203 lines
7.3 KiB
TypeScript
203 lines
7.3 KiB
TypeScript
"use client";
|
|
import { CalendarIcon } from "@/assets/icons";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Control,
|
|
Controller,
|
|
FieldValues,
|
|
Path,
|
|
RegisterOptions,
|
|
} from "react-hook-form";
|
|
import MultiDatePicker, { DateObject } from "react-multi-date-picker";
|
|
import TimePicker from "react-multi-date-picker/plugins/time_picker";
|
|
|
|
interface IProps<T extends FieldValues> {
|
|
control: Control<T>;
|
|
name: Path<T>;
|
|
label: string;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
multiple?: boolean;
|
|
range?: boolean;
|
|
onlyMonthPicker?: boolean;
|
|
onlyYearPicker?: boolean;
|
|
minDate?: number;
|
|
maxDate?: number;
|
|
timePicker?: boolean;
|
|
clearable?: boolean;
|
|
rules?: Omit<
|
|
RegisterOptions<T, Path<T>>,
|
|
"valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
|
|
>;
|
|
}
|
|
|
|
const DatePicker = <T extends FieldValues>({
|
|
control,
|
|
name,
|
|
label,
|
|
multiple = false,
|
|
placeholder,
|
|
required,
|
|
range = false,
|
|
timePicker = false,
|
|
onlyMonthPicker = false,
|
|
onlyYearPicker = false,
|
|
rules,
|
|
maxDate,
|
|
minDate,
|
|
clearable = false,
|
|
}: IProps<T>) => {
|
|
return (
|
|
<div className="space-y-3">
|
|
<label
|
|
htmlFor={name}
|
|
className="block text-body-sm font-semibold capitalize tracking-[0.01em] text-dark dark:text-white"
|
|
>
|
|
{label}
|
|
{required && <span className="ml-1 select-none text-error">*</span>}
|
|
</label>
|
|
|
|
<div>
|
|
<Controller
|
|
control={control}
|
|
name={name}
|
|
rules={rules}
|
|
render={({
|
|
field: { onChange, name, value },
|
|
formState: { errors },
|
|
}) => {
|
|
const error = errors[name];
|
|
|
|
const toUnixSeconds = (v: unknown): number | null => {
|
|
if (v === "" || v === undefined || v === null) return null;
|
|
const n = typeof v === "number" ? v : Number(v);
|
|
return Number.isFinite(n) ? n : null;
|
|
};
|
|
|
|
const toPickerValue = ():
|
|
| DateObject
|
|
| DateObject[]
|
|
| undefined => {
|
|
if (value === "" || value === undefined || value === null) {
|
|
return undefined;
|
|
}
|
|
|
|
if (multiple && Array.isArray(value)) {
|
|
const secs = value
|
|
.map(toUnixSeconds)
|
|
.filter((s): s is number => s !== null);
|
|
if (!secs.length) return undefined;
|
|
return secs.map((unix) => new DateObject(new Date(unix * 1000)));
|
|
}
|
|
|
|
if (range && Array.isArray(value)) {
|
|
const secs = value
|
|
.map(toUnixSeconds)
|
|
.filter((s): s is number => s !== null);
|
|
if (!secs.length) return undefined;
|
|
return secs.map((unix) => new DateObject(new Date(unix * 1000)));
|
|
}
|
|
|
|
if (range && !Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
const unix = toUnixSeconds(value);
|
|
if (unix === null) return undefined;
|
|
return new DateObject(new Date(unix * 1000));
|
|
};
|
|
|
|
const pickerValue = toPickerValue();
|
|
const hasValue =
|
|
pickerValue !== undefined &&
|
|
(Array.isArray(pickerValue) ? pickerValue.length > 0 : true);
|
|
|
|
return (
|
|
<>
|
|
<div className="relative">
|
|
<MultiDatePicker
|
|
range={range}
|
|
minDate={minDate}
|
|
maxDate={maxDate}
|
|
onlyMonthPicker={onlyMonthPicker}
|
|
plugins={
|
|
timePicker
|
|
? [<TimePicker key={name} position="bottom" />]
|
|
: []
|
|
}
|
|
onlyYearPicker={onlyYearPicker}
|
|
multiple={multiple}
|
|
value={pickerValue}
|
|
onChange={(date: DateObject | DateObject[] | null) => {
|
|
if (!date) {
|
|
onChange("");
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(date)) {
|
|
const unixTimestamps = date.map((d) => d.toUnix());
|
|
onChange(unixTimestamps);
|
|
} else if (date instanceof DateObject) {
|
|
onChange(date.isValid ? date.toUnix() : "");
|
|
}
|
|
}}
|
|
format={`YYYY/MM/DD ${timePicker ? "HH:mm" : ""}`}
|
|
containerClassName="w-full"
|
|
inputClass={cn(
|
|
"w-full rounded-xl border border-stroke/70 bg-white/90 px-5.5 py-3 pl-12.5 text-dark shadow-[0_1px_2px_rgba(16,24,40,0.04),0_8px_20px_rgba(16,24,40,0.04)] outline-none backdrop-blur-[1px] transition-all duration-200 focus:border-primary/70 focus:bg-white focus:shadow-[0_0_0_2px_rgba(59,130,246,0.14),0_10px_24px_rgba(59,130,246,0.08)] disabled:cursor-default disabled:bg-gray-2/70 dark:border-dark-3/80 dark:bg-dark-2/85 dark:text-white dark:focus:border-primary/80 dark:focus:shadow-[0_0_0_2px_rgba(59,130,246,0.2),0_10px_24px_rgba(15,23,42,0.45)] dark:disabled:bg-dark",
|
|
clearable && hasValue && "pr-11",
|
|
error &&
|
|
"border-red/80 focus:border-red focus:shadow-[0_0_0_2px_rgba(239,68,68,0.16),0_10px_24px_rgba(239,68,68,0.1)] dark:border-red/80 dark:focus:shadow-[0_0_0_2px_rgba(239,68,68,0.24),0_10px_24px_rgba(127,29,29,0.45)]",
|
|
)}
|
|
placeholder={placeholder}
|
|
/>
|
|
<span className="pointer-events-none absolute left-4.5 top-1/2 -translate-y-1/2 text-dark-5 transition-colors dark:text-dark-6">
|
|
<CalendarIcon />
|
|
</span>
|
|
{clearable && hasValue && (
|
|
<button
|
|
type="button"
|
|
aria-label="Clear date"
|
|
className="absolute right-3 top-1/2 z-10 -translate-y-1/2 rounded-md p-1 text-dark-5 transition hover:bg-gray-2 hover:text-dark dark:text-dark-6 dark:hover:bg-dark-3 dark:hover:text-white"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onChange("");
|
|
}}
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden
|
|
>
|
|
<path
|
|
d="M12 4L4 12M4 4L12 12"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="mt-1.5 text-sm font-medium text-error">
|
|
{error.message as string}
|
|
</p>
|
|
)}
|
|
</>
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DatePicker;
|