Skip to content

Commit 78be8b4

Browse files
committed
Merge branch 'feature/date-picker' into feature/ci
2 parents 345bc42 + b3d6eb6 commit 78be8b4

File tree

7 files changed

+162
-122
lines changed

7 files changed

+162
-122
lines changed

src/Constants/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 200];
1010
// TABLE POLLING CONSTANTS
1111
export const POLLING_INTERVAL = 30000; // 30 seconds
1212

13+
// Date Picker Allowed Range
14+
export const ALLOWED_YEARS = 5;
15+
export const UTC_IST_OFFSET = 5.5 * 60; // +5:30 (in minutes)
16+
1317
/**
1418
* Keys to be deleted before duplicating a session
1519
* from preventing the fields from being duplicated

src/app/session/[type]/Steps/Timeline/index.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { ActiveDaysOptions } from '@/Constants';
3+
import { ActiveDaysOptions, ALLOWED_YEARS } from '@/Constants';
44
import { FormBuilder } from '@/components/FormBuilder';
55
import { useFormContext } from '@/hooks/useFormContext';
66
import { FieldSchema, Session, timelineFields, timelineSchema } from '@/types';
@@ -15,15 +15,23 @@ const TimelineForm: FC = () => {
1515
return {
1616
startDate: {
1717
type: 'datetime',
18-
label: 'Start Date And Time',
18+
label: {
19+
date: 'Start Date',
20+
time: 'Start Time',
21+
},
1922
placeholder: 'Select start date and time',
20-
disableRange: (date: Date) => date < startOfToday() || date > addYears(startOfToday(), 1),
23+
disableRange: (date: Date) =>
24+
date < startOfToday() || date > addYears(startOfToday(), ALLOWED_YEARS),
2125
},
2226
endDate: {
2327
type: 'datetime',
24-
label: 'End Date And Time',
28+
label: {
29+
date: 'End Date',
30+
time: 'End Time',
31+
},
2532
placeholder: 'Select end date and time',
26-
disableRange: (date: Date) => date < startOfToday() || date > addYears(startOfToday(), 1),
33+
disableRange: (date: Date) =>
34+
date < startOfToday() || date > addYears(startOfToday(), ALLOWED_YEARS),
2735
},
2836
testTakers: {
2937
type: 'number',
@@ -65,8 +73,8 @@ const TimelineForm: FC = () => {
6573
params: data.activeDays.sort((a, b) => a - b),
6674
},
6775
is_active: data.isEnabled,
68-
start_time: new Date(data.startDate).toISOString(),
69-
end_time: new Date(data.endDate).toISOString(),
76+
start_time: new Date(data.startDate).toUTCString(),
77+
end_time: new Date(data.endDate).toUTCString(),
7078
};
7179

7280
updateFormData(addedData);

src/components/ui/calendar.tsx

+38-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DayPicker } from 'react-day-picker';
66

77
import { buttonVariants } from '@/components/ui/button';
88
import { cn } from '@/lib/utils';
9+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
910

1011
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
1112

@@ -18,7 +19,8 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
1819
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
1920
month: 'space-y-4',
2021
caption: 'flex justify-center pt-1 relative items-center',
21-
caption_label: 'text-sm font-medium',
22+
caption_label: 'hidden',
23+
caption_dropdowns: 'flex items-center justify-center gap-2',
2224
nav: 'space-x-1 flex items-center',
2325
nav_button: cn(
2426
buttonVariants({ variant: 'outline' }),
@@ -49,6 +51,41 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
4951
components={{
5052
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
5153
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
54+
Dropdown: ({ name, value, children, onChange }) => {
55+
type OptionElement = React.ReactElement<React.HTMLProps<HTMLOptionElement>>;
56+
const options = React.Children.toArray(children) as OptionElement[];
57+
58+
return (
59+
<Select
60+
value={value?.toString()}
61+
onValueChange={(value) => {
62+
const changeEvent = {
63+
target: {
64+
value,
65+
},
66+
} as React.ChangeEvent<HTMLSelectElement>;
67+
onChange?.(changeEvent);
68+
}}
69+
name={name}
70+
>
71+
<SelectTrigger className="h-fit w-fit border-none p-0 focus:bg-none focus:ring-0 focus:ring-offset-0">
72+
<SelectValue />
73+
</SelectTrigger>
74+
<SelectContent position="popper" side="bottom" align="center" sideOffset={2}>
75+
{options.map((option, index) => {
76+
return (
77+
<SelectItem
78+
key={index}
79+
value={option.props.value?.toString() || index.toString()}
80+
>
81+
{option.props.children}
82+
</SelectItem>
83+
);
84+
})}
85+
</SelectContent>
86+
</Select>
87+
);
88+
},
5289
}}
5390
{...props}
5491
/>

src/components/ui/date-time.tsx

+58-35
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Button } from '@/components/ui/button';
22
import { FormControl, FormLabel } from '@/components/ui/form';
33
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
4+
import { ALLOWED_YEARS } from '@/Constants';
45
import { cn } from '@/lib/utils';
56
import { MyDateTimeProps } from '@/types';
6-
import { format } from 'date-fns';
7+
import { addDays, addYears, format, startOfToday } from 'date-fns';
78
import { CalendarIcon } from 'lucide-react';
89
import { ElementRef, forwardRef } from 'react';
910
import { ControllerRenderProps, UseFormReturn } from 'react-hook-form';
@@ -17,41 +18,63 @@ const DateTimePicker = forwardRef<
1718
const { value, onChange, ref: refField, ...restFieldProps } = field;
1819
const { label, disabled, disableRange, ...restSchemaProps } = schema;
1920

21+
const handleSelect = (newDay: Date | undefined) => {
22+
if (!newDay) return;
23+
if (!value) {
24+
onChange?.(newDay);
25+
return;
26+
}
27+
const diff = newDay.getTime() - value.getTime();
28+
const diffInDays = diff / (1000 * 60 * 60 * 24);
29+
const newDateFull = addDays(value, Math.ceil(diffInDays));
30+
onChange?.(newDateFull);
31+
};
32+
2033
return (
21-
<div className="flex flex-col gap-4">
22-
<FormLabel className="self-start">{label}</FormLabel>
23-
<Popover>
24-
<FormControl>
25-
<PopoverTrigger asChild>
26-
<Button
27-
variant="outline"
28-
className={cn(
29-
'justify-start text-left font-normal',
30-
!value && 'text-muted-foreground'
31-
)}
32-
ref={refField}
33-
disabled={disabled}
34-
>
35-
<CalendarIcon className="mr-2 h-4 w-4" />
36-
{value ? format(value, 'PPp') : <span>Pick a date and time</span>}
37-
</Button>
38-
</PopoverTrigger>
39-
</FormControl>
40-
<PopoverContent className="w-auto p-0">
41-
<Calendar
42-
{...restSchemaProps}
43-
{...restFieldProps}
44-
mode="single"
45-
selected={value}
46-
onSelect={onChange}
47-
initialFocus
48-
disabled={disableRange}
49-
/>
50-
<div className="p-3 border-t border-border">
51-
<TimePicker setDate={onChange} date={value} />
52-
</div>
53-
</PopoverContent>
54-
</Popover>
34+
<div className="flex flex-col sm:flex-row gap-4 sm:gap-10 sm:items-center">
35+
<div className="flex flex-col gap-4 flex-1">
36+
<FormLabel className="self-start">{label?.date}</FormLabel>
37+
<Popover>
38+
<FormControl>
39+
<PopoverTrigger asChild>
40+
<Button
41+
variant="outline"
42+
className={cn('justify-start text-left font-normal')}
43+
ref={refField}
44+
disabled={disabled}
45+
>
46+
<CalendarIcon className="mr-2 h-4 w-4" />
47+
{value ? (
48+
format(value, 'PP')
49+
) : (
50+
<span className="text-muted-foreground/80 italic font-medium">
51+
Pick a date and time
52+
</span>
53+
)}
54+
</Button>
55+
</PopoverTrigger>
56+
</FormControl>
57+
<PopoverContent className="w-auto p-0">
58+
<Calendar
59+
{...restSchemaProps}
60+
{...restFieldProps}
61+
captionLayout="dropdown-buttons"
62+
fromYear={startOfToday().getFullYear()}
63+
toYear={addYears(startOfToday(), ALLOWED_YEARS).getFullYear()}
64+
mode="single"
65+
selected={value}
66+
onSelect={(d) => handleSelect(d)}
67+
onMonthChange={handleSelect}
68+
initialFocus
69+
disabled={disableRange}
70+
/>
71+
</PopoverContent>
72+
</Popover>
73+
</div>
74+
<div className="flex flex-col gap-4 flex-1">
75+
<FormLabel className="self-start">{label?.time}</FormLabel>
76+
<TimePicker setDate={onChange} date={value} />
77+
</div>
5578
</div>
5679
);
5780
});

src/components/ui/time-picker.tsx

+34-49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Input } from '@/components/ui/input';
2-
import { Label } from '@/components/ui/label';
32
import {
43
Select,
54
SelectContent,
@@ -16,6 +15,7 @@ import {
1615
setDateByType,
1716
} from '@/lib/time-picker-utils';
1817
import { cn } from '@/lib/utils';
18+
import { ClockIcon } from 'lucide-react';
1919
import React from 'react';
2020

2121
export interface TimePickerInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -73,7 +73,7 @@ const TimePickerInput = React.forwardRef<HTMLInputElement, TimePickerInputProps>
7373
* If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1.
7474
* The second entered digit will break the condition and the value will be set to 10-12.
7575
*/
76-
if (picker === '12hours') {
76+
if (picker === 'hours') {
7777
if (flag && calculatedValue.slice(1, 2) === '1' && prevIntKey === '0') return '0' + key;
7878
}
7979

@@ -93,7 +93,7 @@ const TimePickerInput = React.forwardRef<HTMLInputElement, TimePickerInputProps>
9393
setDate(setDateByType(tempDate, newValue, picker, period));
9494
}
9595
if (e.key >= '0' && e.key <= '9') {
96-
if (picker === '12hours') setPrevIntKey(e.key);
96+
if (picker === 'hours') setPrevIntKey(e.key);
9797

9898
const newValue = calculateNewValue(e.key);
9999
if (flag) onRightFocus?.();
@@ -157,9 +157,7 @@ const TimePeriodSelect = React.forwardRef<HTMLButtonElement, PeriodSelectorProps
157157
if (date) {
158158
const tempDate = new Date(date);
159159
const hours = display12HourValue(date.getHours());
160-
setDate(
161-
setDateByType(tempDate, hours.toString(), '12hours', period === 'AM' ? 'PM' : 'AM')
162-
);
160+
setDate(setDateByType(tempDate, hours.toString(), 'hours', period === 'AM' ? 'PM' : 'AM'));
163161
}
164162
};
165163

@@ -191,55 +189,42 @@ interface TimePickerProps {
191189
}
192190

193191
function TimePicker({ date, setDate }: TimePickerProps) {
194-
const [period, setPeriod] = React.useState<Period>('PM');
192+
const [period, setPeriod] = React.useState<Period>(date && date.getHours() >= 12 ? 'PM' : 'AM');
195193

196194
const minuteRef = React.useRef<HTMLInputElement>(null);
197195
const hourRef = React.useRef<HTMLInputElement>(null);
198196
const periodRef = React.useRef<HTMLButtonElement>(null);
199197

200198
return (
201-
<div className="flex items-end gap-2 justify-center">
202-
<div className="grid gap-1 text-center">
203-
<Label htmlFor="hours" className="text-xs">
204-
Hours
205-
</Label>
206-
<TimePickerInput
207-
id="hours"
208-
picker="12hours"
209-
period={period}
210-
date={date}
211-
setDate={setDate}
212-
ref={hourRef}
213-
onRightFocus={() => minuteRef.current?.focus()}
214-
/>
215-
</div>
216-
<div className="grid gap-1 text-center">
217-
<Label htmlFor="minutes" className="text-xs">
218-
Minutes
219-
</Label>
220-
<TimePickerInput
221-
id="minutes"
222-
picker="minutes"
223-
date={date}
224-
setDate={setDate}
225-
ref={minuteRef}
226-
onLeftFocus={() => hourRef.current?.focus()}
227-
onRightFocus={() => periodRef.current?.focus()}
228-
/>
229-
</div>
230-
<div className="grid gap-1 text-center">
231-
<Label htmlFor="period" className="text-xs">
232-
Period
233-
</Label>
234-
<TimePeriodSelect
235-
period={period}
236-
setPeriod={setPeriod}
237-
date={date}
238-
setDate={setDate}
239-
ref={periodRef}
240-
onLeftFocus={() => periodRef.current?.focus()}
241-
/>
242-
</div>
199+
<div className="flex items-center gap-4">
200+
<ClockIcon className="size-5 text-current" />
201+
<TimePickerInput
202+
id="hours"
203+
picker="hours"
204+
period={period}
205+
date={date}
206+
setDate={setDate}
207+
ref={hourRef}
208+
onRightFocus={() => minuteRef.current?.focus()}
209+
/>
210+
<span className="-mx-1.5">:</span>
211+
<TimePickerInput
212+
id="minutes"
213+
picker="minutes"
214+
date={date}
215+
setDate={setDate}
216+
ref={minuteRef}
217+
onLeftFocus={() => hourRef.current?.focus()}
218+
onRightFocus={() => periodRef.current?.focus()}
219+
/>
220+
<TimePeriodSelect
221+
period={period}
222+
setPeriod={setPeriod}
223+
date={date}
224+
setDate={setDate}
225+
ref={periodRef}
226+
onLeftFocus={() => periodRef.current?.focus()}
227+
/>
243228
</div>
244229
);
245230
}

0 commit comments

Comments
 (0)