11import { Popover , PopoverContent , PopoverTrigger } from '@/components/ui/popover' ;
2- import { Clock } from 'lucide-react' ;
3- import { format , isValid } from 'date-fns' ;
4- import { useState , useEffect } from 'react' ;
2+ import { Clock , Calendar as CalendarIcon } from 'lucide-react' ;
3+ import { format , startOfToday } from 'date-fns' ;
4+ import { useState , useCallback } from 'react' ;
55import { cn } from '@/lib/utils' ;
66import { toast } from 'sonner' ;
7+ import { Calendar } from '@/components/ui/calendar' ;
8+ import { Input } from '@/components/ui/input' ;
9+
10+ const pad2 = ( n : number ) => n . toString ( ) . padStart ( 2 , '0' ) ;
11+ const getLocalTimeFromDate = ( d : Date ) => `${ pad2 ( d . getHours ( ) ) } :${ pad2 ( d . getMinutes ( ) ) } ` ;
12+ const getNowTime = ( ) => getLocalTimeFromDate ( new Date ( ) ) ;
713
814interface ScheduleSendPickerProps {
915 value ?: string | undefined ;
@@ -12,107 +18,191 @@ interface ScheduleSendPickerProps {
1218 onValidityChange ?: ( isValid : boolean ) => void ;
1319}
1420
15- const toLocalInputValue = ( date : Date ) => {
16- const tzOffsetMs = date . getTimezoneOffset ( ) * 60 * 1000 ;
17- const local = new Date ( date . getTime ( ) - tzOffsetMs ) ;
18- return local . toISOString ( ) . slice ( 0 , 16 ) ;
19- } ;
20-
2121export const ScheduleSendPicker : React . FC < ScheduleSendPickerProps > = ( {
2222 value,
2323 onChange,
2424 className,
2525 onValidityChange,
2626} ) => {
27- const [ isOpen , setIsOpen ] = useState ( false ) ;
27+ const [ datePickerOpen , setDatePickerOpen ] = useState ( false ) ;
28+ const [ timePickerOpen , setTimePickerOpen ] = useState ( false ) ;
2829
29- const [ localValue , setLocalValue ] = useState < string > ( ( ) => {
30- if ( value ) {
31- const d = new Date ( value ) ;
32- if ( ! isNaN ( d . getTime ( ) ) ) return toLocalInputValue ( d ) ;
33- }
34- return '' ;
35- } ) ;
36-
37- useEffect ( ( ) => {
38- if ( value ) {
39- const d = new Date ( value ) ;
40- if ( ! isNaN ( d . getTime ( ) ) ) {
41- setLocalValue ( toLocalInputValue ( d ) ) ;
42- }
43- } else {
44- setLocalValue ( '' ) ;
45- }
46- } , [ value ] ) ;
47-
48- const handleChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
49- const val = e . target . value ;
50- setLocalValue ( val ) ;
30+ const isScheduling = ! ! value ;
31+ const selectedDate = value ? new Date ( value ) : undefined ;
32+ const time = value ? getLocalTimeFromDate ( new Date ( value ) ) : getNowTime ( ) ;
5133
52- if ( ! val ) {
34+ const emitChange = useCallback ( ( datePart : Date | undefined , timePart : string , validate : boolean = false ) => {
35+ if ( ! datePart ) {
5336 onChange ( undefined ) ;
54- onValidityChange ?.( true ) ;
37+ if ( validate ) {
38+ onValidityChange ?.( true ) ;
39+ }
5540 return ;
5641 }
5742
58- const maybeDate = new Date ( val ) ;
43+ const [ hhStr , mmStr = '00' ] = timePart . split ( ':' ) ;
44+ const hours = Number ( hhStr ) ;
45+ const minutes = Number ( mmStr ) ;
5946
60- // Invalid date string
61- if ( isNaN ( maybeDate . getTime ( ) ) ) {
62- onValidityChange ?.( false ) ;
47+ if ( Number . isNaN ( hours ) || Number . isNaN ( minutes ) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59 ) {
48+ if ( validate ) {
49+ onValidityChange ?.( false ) ;
50+ }
6351 return ;
6452 }
6553
66- const now = new Date ( ) ;
67- if ( maybeDate . getTime ( ) < now . getTime ( ) ) {
54+ const combinedDate = new Date ( datePart ) ;
55+ combinedDate . setHours ( hours , minutes , 0 , 0 ) ;
56+
57+ if ( validate && combinedDate . getTime ( ) < Date . now ( ) ) {
6858 toast . error ( 'Scheduled time cannot be in the past' ) ;
6959 onValidityChange ?.( false ) ;
7060 return ;
7161 }
7262
73- onValidityChange ?.( true ) ;
74- onChange ( maybeDate . toISOString ( ) ) ;
63+ if ( validate ) {
64+ onValidityChange ?.( true ) ;
65+ }
66+ onChange ( combinedDate . toISOString ( ) ) ;
67+ } , [ onChange , onValidityChange ] ) ;
68+
69+ const handleDateSelect = useCallback ( ( d ?: Date ) => {
70+ emitChange ( d , time , false ) ;
71+ } , [ emitChange , time ] ) ;
72+
73+ const handleTimeChange = useCallback ( ( e : React . ChangeEvent < HTMLInputElement > ) => {
74+ const val = e . target . value ;
75+ emitChange ( selectedDate , val , false ) ;
76+ } , [ selectedDate , emitChange ] ) ;
77+
78+ const handleDatePickerClose = useCallback ( ( open : boolean ) => {
79+ setDatePickerOpen ( open ) ;
80+ if ( ! open && selectedDate ) {
81+ emitChange ( selectedDate , time , true ) ;
82+ }
83+ } , [ selectedDate , time , emitChange ] ) ;
84+
85+ const handleTimePickerClose = useCallback ( ( open : boolean ) => {
86+ setTimePickerOpen ( open ) ;
87+ if ( ! open && selectedDate ) {
88+ emitChange ( selectedDate , time , true ) ;
89+ }
90+ } , [ selectedDate , time , emitChange ] ) ;
91+
92+ const handleToggleScheduling = useCallback ( ( ) => {
93+ if ( isScheduling ) {
94+ onChange ( undefined ) ;
95+ } else {
96+ const now = new Date ( ) ;
97+ emitChange ( now , getNowTime ( ) ) ;
98+ }
99+ } , [ isScheduling , onChange , emitChange ] ) ;
100+
101+ const formatTime12Hour = ( timeStr : string ) => {
102+ try {
103+ const [ hhStr , mmStr = '00' ] = timeStr . split ( ':' ) ;
104+ const preview = new Date ( ) ;
105+ preview . setHours ( Number ( hhStr ) , Number ( mmStr ) , 0 , 0 ) ;
106+ return format ( preview , 'hh:mm aaa' ) ;
107+ } catch {
108+ return timeStr ;
109+ }
75110 } ;
76111
77- const displayValue = localValue || toLocalInputValue ( new Date ( ) ) ;
112+ const triggerLabel = ( ( ) => {
113+ if ( ! selectedDate ) return 'Send later' ;
114+ try {
115+ const formattedTime = formatTime12Hour ( time ) ;
116+ const formattedDate = format ( selectedDate , 'dd MMM yyyy' ) ;
117+ return `${ formattedDate } ${ formattedTime } ` ;
118+ } catch {
119+ return 'Send later' ;
120+ }
121+ } ) ( ) ;
122+
123+ if ( isScheduling ) {
124+ return (
125+ < >
126+ < Popover open = { datePickerOpen } onOpenChange = { handleDatePickerClose } >
127+ < PopoverTrigger asChild >
128+ < button
129+ type = "button"
130+ className = { cn (
131+ 'flex items-center gap-1 rounded-md border px-2 py-1 text-sm hover:bg-accent' ,
132+ className ,
133+ ) }
134+ >
135+ < CalendarIcon className = "h-4 w-4" />
136+ < span >
137+ { selectedDate ? format ( selectedDate , 'dd MMM yyyy' ) : 'Select Date' }
138+ </ span >
139+ </ button >
140+ </ PopoverTrigger >
141+ < PopoverContent className = "z-[100] w-auto p-4" align = "start" side = "top" sideOffset = { 8 } >
142+ < div className = "space-y-4" >
143+ < Calendar
144+ mode = "single"
145+ selected = { selectedDate }
146+ onSelect = { handleDateSelect }
147+ disabled = { { before : startOfToday ( ) } }
148+ className = "rounded-md"
149+ captionLayout = "dropdown"
150+ />
151+ </ div >
152+ </ PopoverContent >
153+ </ Popover >
154+
155+ < Popover open = { timePickerOpen } onOpenChange = { handleTimePickerClose } >
156+ < PopoverTrigger asChild >
157+ < button
158+ type = "button"
159+ className = { cn (
160+ 'flex items-center gap-1 rounded-md border px-2 py-1 text-sm hover:bg-accent' ,
161+ className ,
162+ ) }
163+ >
164+ < Clock className = "h-4 w-4" />
165+ < span > { formatTime12Hour ( time ) } </ span >
166+ </ button >
167+ </ PopoverTrigger >
168+ < PopoverContent className = "z-[100] w-auto p-4" align = "start" side = "top" sideOffset = { 8 } >
169+ < div className = "space-y-4" >
170+ < h3 className = "text-sm font-medium" > Select Time</ h3 >
171+ < Input
172+ type = "time"
173+ value = { time }
174+ onChange = { handleTimeChange }
175+ className = "w-full"
176+ />
177+ </ div >
178+ </ PopoverContent >
179+ </ Popover >
78180
79- return (
80- < Popover open = { isOpen } onOpenChange = { setIsOpen } >
81- < PopoverTrigger asChild >
82181 < button
83182 type = "button"
183+ onClick = { handleToggleScheduling }
84184 className = { cn (
85185 'flex items-center gap-1 rounded-md border px-2 py-1 text-sm hover:bg-accent' ,
86186 className ,
87187 ) }
88188 >
89- < Clock className = "h-4 w-4" />
90- < span >
91- { ( ( ) => {
92- if ( ! localValue ) return 'Send later' ;
93- const parsed = new Date ( localValue ) ;
94- if ( ! isValid ( parsed ) ) return 'Send later' ;
95- try {
96- return format ( parsed , 'dd MMM yyyy hh:mm aaa' ) ;
97- } catch ( error ) {
98- console . error ( 'Error formatting date' , error ) ;
99- return 'Send later' ;
100- }
101- } ) ( ) }
102- </ span >
189+ < span > Cancel</ span >
103190 </ button >
104- </ PopoverTrigger >
105- < PopoverContent className = "z-[100] w-64 p-4" align = "start" side = "top" sideOffset = { 8 } >
106- < div className = "flex flex-col gap-4" >
107- < label className = "text-sm font-semibold" > Choose date & time </ label >
108- < input
109- type = "datetime-local"
110- value = { displayValue }
111- onChange = { handleChange }
112- className = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:opacity-0"
113- />
114- </ div >
115- </ PopoverContent >
116- </ Popover >
191+ </ >
192+ ) ;
193+ }
194+
195+ return (
196+ < button
197+ type = "button"
198+ onClick = { handleToggleScheduling }
199+ className = { cn (
200+ 'flex items-center gap-1 rounded-md border px-2 py-1 text-sm hover:bg-accent' ,
201+ className ,
202+ ) }
203+ >
204+ < Clock className = "h-4 w-4" />
205+ < span > { triggerLabel } </ span >
206+ </ button >
117207 ) ;
118208} ;
0 commit comments