返回 presentation-ai
date-time-picker.tsx
根目录 / src / components / ui / date-time-picker.tsx
1 "use client";
2
3 import {
4 Popover,
5 PopoverContent,
6 PopoverTrigger,
7 } from "@/components/ui/popover";
8 import { ScrollArea } from "@/components/ui/scroll-area";
9 import { cn } from "@/lib/utils";
10 import { format } from "date-fns";
11 import { CalendarIcon } from "lucide-react";
12 import { useRef, useState } from "react";
13 import { Button } from "./button";
14 import { Calendar } from "./calendar";
15
16 interface DateTimePickerProps {
17 date: Date | null;
18 onDateChange: (date: Date | null) => void;
19 className?: string;
20 }
21
22 export function DateTimePicker({
23 date,
24 onDateChange,
25 className,
26 }: DateTimePickerProps) {
27 const [isOpen, setIsOpen] = useState(false);
28 const [selectedDate, setSelectedDate] = useState<Date | null>(date);
29 const calendarRef = useRef<HTMLDivElement | null>(null);
30
31 // Format time as 24-hour format for internal calculations
32 const getTimeString = (date: Date | null) => {
33 if (!date) return "00:00";
34 const hours = date.getHours().toString().padStart(2, "0");
35 const minutes = date.getMinutes().toString().padStart(2, "0");
36 return `${hours}:${minutes}`;
37 };
38
39 const [time, setTime] = useState<string>(() => {
40 if (date) {
41 return getTimeString(date);
42 }
43 // Default to current hour, rounded to nearest 15 minutes
44 const now = new Date();
45 const minutes = Math.ceil(now.getMinutes() / 15) * 15;
46 now.setMinutes(minutes === 60 ? 0 : minutes, 0, 0);
47 if (minutes === 60) {
48 now.setHours(now.getHours() + 1);
49 }
50 return getTimeString(now);
51 });
52
53 const handleDateSelect = (selected: Date | undefined) => {
54 if (!selected) return;
55
56 setSelectedDate(selected);
57
58 // Apply the selected time to the date
59 const [hours, minutes] = time.split(":");
60 selected.setHours(parseInt(hours ?? "0", 10), parseInt(minutes ?? "0", 10));
61
62 onDateChange(selected);
63 };
64
65 const handleTimeSelect = (timeValue: string) => {
66 setTime(timeValue);
67
68 if (selectedDate) {
69 const [hours, minutes] = timeValue.split(":");
70 const newDate = new Date(selectedDate.getTime());
71 newDate.setHours(
72 parseInt(hours ?? "0", 10),
73 parseInt(minutes ?? "0", 10),
74 );
75
76 setSelectedDate(newDate);
77 onDateChange(newDate);
78 }
79 setIsOpen(false);
80 };
81
82 return (
83 <div className={cn("flex w-full", className)}>
84 <Popover open={isOpen} onOpenChange={setIsOpen}>
85 <PopoverTrigger asChild>
86 <Button
87 variant={"outline"}
88 className={cn(
89 "w-full font-normal",
90 !date && "text-muted-foreground",
91 )}
92 >
93 {date ? (
94 `${format(date, "PPP")}, ${format(date, "p")}`
95 ) : (
96 <span>Pick a date</span>
97 )}
98 <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
99 </Button>
100 </PopoverTrigger>
101 <PopoverContent className="flex w-auto items-start p-0" align="start">
102 <div ref={calendarRef}>
103 <Calendar
104 mode="single"
105 selected={selectedDate ?? undefined}
106 onSelect={handleDateSelect}
107 fromYear={2000}
108 toYear={new Date().getFullYear() + 10}
109 defaultMonth={date ?? undefined}
110 />
111 </div>
112 <div className="my-4 mr-2 w-[120px]">
113 <ScrollArea
114 className="h-72 pr-2.5"
115 onWheel={(e) => {
116 e.stopPropagation();
117 }}
118 >
119 <div className="flex h-max flex-col gap-2">
120 {Array.from({ length: 96 }).map((_, i) => {
121 const hour = Math.floor(i / 4)
122 .toString()
123 .padStart(2, "0");
124 const minute = ((i % 4) * 15).toString().padStart(2, "0");
125 const timeValue = `${hour}:${minute}`;
126 const displayTime = new Date();
127 displayTime.setHours(
128 parseInt(hour, 10),
129 parseInt(minute, 10),
130 );
131
132 return (
133 <Button
134 key={i}
135 className="w-full px-2 text-left"
136 variant="outline"
137 onClick={() => handleTimeSelect(timeValue)}
138 >
139 {format(displayTime, "p")}
140 </Button>
141 );
142 })}
143 </div>
144 </ScrollArea>
145 </div>
146 </PopoverContent>
147 </Popover>
148 </div>
149 );
150 }
151
151 lines Plain Text