284 lines
8.5 KiB
TypeScript
284 lines
8.5 KiB
TypeScript
import * as React from "react";
|
|
import { t as i18nT } from "i18next";
|
|
import { parse } from "date-fns";
|
|
import { CalendarIcon, ChevronDownIcon, XIcon } from "~/components/ui/icons/vendor";
|
|
import { Button } from "../button";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
import { cn } from "~/lib/utils";
|
|
import { Calendar } from "../calendar";
|
|
import { Input } from "../input";
|
|
import { Tabs, TabsContent } from "../tabs";
|
|
import { useLocaleFormatters } from "~/locale/format";
|
|
|
|
type Props = {
|
|
name?: string;
|
|
date: Date | undefined | null;
|
|
setDate: (date: Date | undefined) => void;
|
|
dirty?: boolean;
|
|
canClear?: boolean;
|
|
className?: string;
|
|
closeOnSelect?: boolean;
|
|
showInput?: boolean;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
autoFocus?: boolean;
|
|
style?: React.CSSProperties;
|
|
fromYear?: number;
|
|
toYear?: number;
|
|
};
|
|
|
|
export function InputDatePickerDeprecated({
|
|
name,
|
|
date,
|
|
setDate,
|
|
dirty,
|
|
canClear = true,
|
|
className,
|
|
closeOnSelect = false,
|
|
showInput = false,
|
|
placeholder,
|
|
required,
|
|
disabled,
|
|
autoFocus,
|
|
style,
|
|
fromYear,
|
|
toYear,
|
|
}: Props) {
|
|
const formatters = useLocaleFormatters();
|
|
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
|
const [inputValue, setInputValue] = React.useState("");
|
|
const [activeTab, setActiveTab] = React.useState("calendar");
|
|
const [month, setMonth] = React.useState<Date | undefined>(date || undefined);
|
|
|
|
React.useEffect(() => {
|
|
if (date) {
|
|
try {
|
|
const parsedDate = new Date(date);
|
|
if (!isNaN(parsedDate.getTime())) {
|
|
setInputValue(parsedDate.toISOString().split("T")[0]);
|
|
}
|
|
} catch (e) {
|
|
console.error("Invalid date:", date);
|
|
}
|
|
} else {
|
|
setInputValue("");
|
|
}
|
|
}, [date]);
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setInputValue(value);
|
|
if (value) {
|
|
// Parse the date string in local timezone
|
|
const [year, month, day] = value.split("-").map(Number);
|
|
const newDate = new Date(year, month - 1, day);
|
|
if (!isNaN(newDate.getTime())) {
|
|
setDate(newDate);
|
|
}
|
|
} else {
|
|
setDate(undefined);
|
|
}
|
|
};
|
|
|
|
const displayDate = React.useMemo(() => {
|
|
if (!date) return null;
|
|
try {
|
|
const parsedDate = new Date(date);
|
|
if (!isNaN(parsedDate.getTime())) {
|
|
return formatters.date(parsedDate, { dateStyle: "long" });
|
|
}
|
|
} catch (e) {
|
|
console.error("Invalid date:", date);
|
|
}
|
|
return null;
|
|
}, [date, formatters]);
|
|
|
|
return (
|
|
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
|
{name && <input type="hidden" name={name} value={date ? new Date(date).toISOString() : ""} readOnly hidden />}
|
|
<PopoverTrigger asChild>
|
|
<div className="relative truncate">
|
|
<Button
|
|
type="button"
|
|
variant={"outline"}
|
|
className={cn("w-full justify-start truncate text-left font-normal", !date && "text-muted-foreground", className)}
|
|
disabled={disabled}
|
|
autoFocus={autoFocus}
|
|
style={style}
|
|
>
|
|
<CalendarIcon className="size-3" />
|
|
{displayDate || <span>{i18nT("components.date.pick")}</span>}
|
|
</Button>
|
|
{canClear && date && (
|
|
<div className="absolute top-1 right-1">
|
|
<Button
|
|
type="button"
|
|
variant={"ghost"}
|
|
size="xs"
|
|
className="text-muted-foreground"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setDate(undefined);
|
|
}}
|
|
>
|
|
<XIcon className="size-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0" align="start">
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full gap-0">
|
|
<TabsContent value="calendar" className="w-[250px] p-0">
|
|
<div className="px-3 pt-3">
|
|
<Input
|
|
type="date"
|
|
value={inputValue}
|
|
onChange={handleInputChange}
|
|
className="mb-2"
|
|
required={required}
|
|
disabled={disabled}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
setIsCalendarOpen(false);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<Calendar
|
|
mode="single"
|
|
selected={date ? new Date(date) : undefined}
|
|
month={month || date || undefined}
|
|
onMonthChange={(e) => {
|
|
setMonth(e);
|
|
}}
|
|
onSelect={(e: Date | undefined) => {
|
|
setDate(e);
|
|
if (closeOnSelect) {
|
|
setIsCalendarOpen(false);
|
|
}
|
|
}}
|
|
required={required}
|
|
disabled={disabled}
|
|
fromYear={fromYear}
|
|
toYear={toYear}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="manual" className="w-[250px] p-3">
|
|
<Input
|
|
type="date"
|
|
value={inputValue}
|
|
onChange={handleInputChange}
|
|
className="mb-2"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
setIsCalendarOpen(false);
|
|
}
|
|
}}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
export default function InputDatePicker(props: Props) {
|
|
const formatters = useLocaleFormatters();
|
|
const [open, setOpen] = React.useState(false);
|
|
const [date, setDate] = React.useState<Date | undefined | null>(props.date ? new Date(props.date) : undefined);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
id="date"
|
|
className={cn("justify-between bg-transparent font-normal hover:bg-transparent", props.className)}
|
|
dirty={props.dirty}
|
|
disabled={props.disabled}
|
|
style={props.style}
|
|
>
|
|
{date ? formatters.date(date, { dateStyle: "long" }) : props.placeholder || i18nT("components.date.select")}
|
|
<ChevronDownIcon />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="bg-background w-auto overflow-hidden p-0" align="start">
|
|
<Calendar
|
|
mode="single"
|
|
selected={date || undefined}
|
|
captionLayout="dropdown"
|
|
onSelect={(date) => {
|
|
setDate(date);
|
|
setOpen(false);
|
|
props.setDate(date);
|
|
}}
|
|
fromYear={props.fromYear}
|
|
toYear={props.toYear}
|
|
/>
|
|
{/* <div className="px-2 pb-2">
|
|
<ManualInputDatePicker date={date} onChange={setDate} />
|
|
</div> */}
|
|
<div className="flex items-center justify-center gap-2 px-3 pb-2">
|
|
{/* Today button */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setDate(new Date());
|
|
props.setDate(new Date());
|
|
}}
|
|
className="w-1/2"
|
|
>
|
|
{i18nT("shared.dateRange.today")}
|
|
</Button>
|
|
{/* Clear button */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setDate(null);
|
|
props.setDate(undefined);
|
|
}}
|
|
disabled={!date}
|
|
className="w-1/2"
|
|
>
|
|
{i18nT("shared.clear")}
|
|
</Button>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// function ManualInputDatePicker({
|
|
// date,
|
|
// onChange,
|
|
// inputFormat = "mm/dd/yyyy",
|
|
// }: {
|
|
// date: Date | undefined;
|
|
// onChange: (date: Date | undefined) => void;
|
|
// inputFormat?: string;
|
|
// }) {
|
|
// const [inputValue, setInputValue] = React.useState(date ? format(date, inputFormat) : "");
|
|
|
|
// function tryParseDate(value: string) {
|
|
// setInputValue(value);
|
|
// try {
|
|
// const parsedDate = parse(value, inputFormat, new Date());
|
|
// if (!isNaN(parsedDate.getTime())) {
|
|
// onChange(parsedDate);
|
|
// }
|
|
// } catch (e) {
|
|
// console.error("Invalid date:", value);
|
|
// }
|
|
// }
|
|
|
|
// return <Input value={inputValue} onChange={(e) => tryParseDate(e.target.value)} placeholder={inputFormat} />;
|
|
// }
|