232 lines
9.9 KiB
TypeScript
232 lines
9.9 KiB
TypeScript
"use client";
|
||
|
||
import * as React from "react";
|
||
import { format } from "date-fns";
|
||
import { ru } from "date-fns/locale";
|
||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import { Calendar } from "@/components/ui/calendar";
|
||
import {
|
||
Popover,
|
||
PopoverContent,
|
||
PopoverTrigger,
|
||
} from "@/components/ui/popover";
|
||
import Icon from "./icon";
|
||
|
||
interface DatePickerProps {
|
||
showIcon?: boolean;
|
||
variant?: "default" | "small";
|
||
placeholder?: string;
|
||
value?: Date | null;
|
||
departureTime?: string;
|
||
arrivalTime?: string;
|
||
onDateChange?: (date: Date | undefined) => void;
|
||
onDepartureTimeChange?: (time: string) => void;
|
||
onArrivalTimeChange?: (time: string) => void;
|
||
onlyDeparture?: boolean;
|
||
onlyArrival?: boolean;
|
||
}
|
||
|
||
export function DatePicker({
|
||
showIcon = true,
|
||
variant = "default",
|
||
placeholder = "Выберите дату и время",
|
||
value,
|
||
departureTime: externalDepartureTime,
|
||
arrivalTime: externalArrivalTime,
|
||
onDateChange,
|
||
onDepartureTimeChange,
|
||
onArrivalTimeChange,
|
||
onlyDeparture,
|
||
onlyArrival,
|
||
}: DatePickerProps) {
|
||
const [internalDate, setInternalDate] = React.useState<Date>();
|
||
const [internalDepartureTime, setInternalDepartureTime] =
|
||
React.useState("12:00");
|
||
const [internalArrivalTime, setInternalArrivalTime] =
|
||
React.useState("13:00");
|
||
const [open, setOpen] = React.useState(false);
|
||
|
||
// Определяем, является ли компонент контролируемым
|
||
const isControlled =
|
||
value !== undefined ||
|
||
externalDepartureTime !== undefined ||
|
||
externalArrivalTime !== undefined;
|
||
|
||
// Используем внешние значения, если они предоставлены, иначе внутренние
|
||
const date = value !== undefined ? value || undefined : internalDate;
|
||
const departureTime =
|
||
externalDepartureTime !== undefined
|
||
? externalDepartureTime
|
||
: internalDepartureTime;
|
||
const arrivalTime =
|
||
externalArrivalTime !== undefined
|
||
? externalArrivalTime
|
||
: internalArrivalTime;
|
||
|
||
const handleDateChange = (newDate: Date | undefined) => {
|
||
if (onDateChange) {
|
||
onDateChange(newDate);
|
||
} else if (!isControlled) {
|
||
setInternalDate(newDate);
|
||
}
|
||
};
|
||
|
||
const handleDepartureTimeChange = (time: string) => {
|
||
if (onDepartureTimeChange) {
|
||
onDepartureTimeChange(time);
|
||
} else if (!isControlled) {
|
||
setInternalDepartureTime(time);
|
||
}
|
||
};
|
||
|
||
const handleArrivalTimeChange = (time: string) => {
|
||
if (onArrivalTimeChange) {
|
||
onArrivalTimeChange(time);
|
||
} else if (!isControlled) {
|
||
setInternalArrivalTime(time);
|
||
}
|
||
};
|
||
|
||
const handleApply = () => {
|
||
// Закрываем popover после применения
|
||
setOpen(false);
|
||
};
|
||
|
||
const heightClass = variant === "small" ? "h-[48px]" : "h-[64px]";
|
||
|
||
return (
|
||
<Popover open={open} onOpenChange={setOpen}>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
data-empty={!date}
|
||
className={`w-full ${heightClass} justify-between text-left font-normal`}
|
||
>
|
||
<div className="flex items-center">
|
||
{showIcon && (
|
||
<Icon
|
||
name="calendar"
|
||
className="w-4 h-4 text-brand mr-2"
|
||
/>
|
||
)}
|
||
{date ? (
|
||
(() => {
|
||
let timeFormat = "";
|
||
if (onlyDeparture) {
|
||
timeFormat = `d MMMM, ${departureTime}`;
|
||
} else if (onlyArrival) {
|
||
timeFormat = `d MMMM, ${arrivalTime}`;
|
||
} else {
|
||
timeFormat = `d MMMM, ${departureTime} - ${arrivalTime}`;
|
||
}
|
||
return format(date, timeFormat, {
|
||
locale: ru,
|
||
});
|
||
})()
|
||
) : (
|
||
<span>{placeholder}</span>
|
||
)}
|
||
</div>
|
||
{open ? (
|
||
<ChevronUpIcon className="w-4 h-4" />
|
||
) : (
|
||
<ChevronDownIcon className="w-4 h-4" />
|
||
)}
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="p-0 bg-white rounded-[20px] shadow-lg">
|
||
<div className="p-4 w-full">
|
||
{/* Календарь */}
|
||
<Calendar
|
||
mode="single"
|
||
selected={date}
|
||
onSelect={handleDateChange}
|
||
className="mb-4 "
|
||
locale={ru}
|
||
disabled={(date) =>
|
||
date < new Date(new Date().setHours(0, 0, 0, 0))
|
||
}
|
||
classNames={{
|
||
root: "w-full",
|
||
month: "flex w-full flex-col gap-4",
|
||
button_previous:
|
||
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
|
||
button_next:
|
||
"h-8 w-8 flex items-center justify-center hover:bg-gray-100 rounded-md",
|
||
month_caption:
|
||
"flex h-8 w-full items-center justify-center px-8 text-gray-700 font-semibold",
|
||
table: "w-full border-collapse",
|
||
weekdays: "flex",
|
||
weekday:
|
||
"flex-1 text-gray-500 text-xs font-normal p-2 text-center",
|
||
day_button: "font-bold ring-0 focus:ring-0",
|
||
week: "mt-2 flex w-full",
|
||
today: "bg-gray-100 text-gray-900 rounded-full",
|
||
outside: "text-gray-300",
|
||
disabled: "text-gray-400 cursor-not-allowed",
|
||
selected:
|
||
"rounded-full border-none outline-none !bg-brand text-white",
|
||
}}
|
||
/>
|
||
|
||
{/* Поля времени */}
|
||
<div className="flex gap-3 mb-4">
|
||
{onlyDeparture && (
|
||
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
|
||
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
|
||
Выход
|
||
</label>
|
||
<div className="relative h-full flex align-center">
|
||
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
||
<input
|
||
type="time"
|
||
value={departureTime}
|
||
onChange={(e) =>
|
||
handleDepartureTimeChange(
|
||
e.target.value
|
||
)
|
||
}
|
||
className="w-full focus:outline-none focus:border-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{onlyArrival && (
|
||
<div className="relative w-full h-12 px-3 border border-gray-300 rounded-full text-gray-700 font-medium text-center">
|
||
<label className="absolute left-[24px] top-0 transform -translate-y-1/2 text-xs text-gray-500 pointer-events-none transition-all duration-200 bg-white px-1">
|
||
Заход
|
||
</label>
|
||
<div className="relative h-full flex align-center">
|
||
<ChevronDownIcon className="absolute right-0 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
||
<input
|
||
type="time"
|
||
value={arrivalTime}
|
||
onChange={(e) =>
|
||
handleArrivalTimeChange(
|
||
e.target.value
|
||
)
|
||
}
|
||
className="w-full focus:outline-none focus:border-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Кнопка Применить */}
|
||
<Button
|
||
onClick={handleApply}
|
||
variant="gradient"
|
||
className="font-bold text-white h-[44px] w-full px-8"
|
||
>
|
||
Применить
|
||
</Button>
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
}
|