Skip to content

Commit

Permalink
add time picker (#83)
Browse files Browse the repository at this point in the history
* add time picker

* use js dates for time picker
  • Loading branch information
fmtabbara authored Dec 7, 2024
1 parent 687fbf4 commit ff7c17f
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 110 deletions.
9 changes: 7 additions & 2 deletions suncityla/app/booking/components/BookingCard/BookingEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { format } from 'date-fns';
import { add, format } from 'date-fns';
import { Booking } from '@prisma/client';
import {
Table,
Expand Down Expand Up @@ -28,6 +28,7 @@ const BookingEntry = ({ booking }: { booking: Booking }) => {
<TableHead>Firstname</TableHead>
<TableHead>Lastname</TableHead>
<TableHead>Booking date</TableHead>
<TableHead>Booking time</TableHead>
<TableHead>Address line 1</TableHead>
<TableHead>Address line 2</TableHead>
<TableHead>State</TableHead>
Expand All @@ -39,7 +40,11 @@ const BookingEntry = ({ booking }: { booking: Booking }) => {
<TableRow>
<TableCell>{booking.firstname}</TableCell>
<TableCell>{booking.lastname}</TableCell>
<TableCell>{format(new Date(booking.bookingDate), 'yyyy-MM-dd')}</TableCell>
<TableCell>{format(new Date(booking.bookingDate), 'EE dd/MM/yy')}</TableCell>
<TableCell>{`${format(new Date(booking.bookingDate), 'h:mm a')} - ${format(
add(new Date(booking.bookingDate), { hours: 1 }),
'h:mm a',
)}`}</TableCell>
<TableCell>{booking.streetAddress}</TableCell>
<TableCell>{booking.postalCode}</TableCell>
<TableCell>{booking.state}</TableCell>
Expand Down
41 changes: 34 additions & 7 deletions suncityla/app/booking/components/BookingForm/BookingForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useActionState, useRef, startTransition, useEffect } from 'react';
import { useActionState, useRef, startTransition, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
Expand All @@ -13,6 +13,8 @@ import LoadingModal from '@/components/modals/Loading';
import onSubmitAction from '@/app/booking/actions/onSubmitAction';
import { bookingFormSchema } from '../../schemas/newBookingForm';
import { Booking } from '@prisma/client';
import TimeSelect from '../TimeSelect/TimeSelect';
import createAvailableTimes from '../TimeSelect/times';

export type BookingFormData = z.infer<typeof bookingFormSchema>;

Expand All @@ -32,15 +34,21 @@ export default function BookingForm({ booking }: { booking?: Booking | null }) {
'street-address': booking?.streetAddress ?? '',
'postal-code': booking?.postalCode ?? '',
state: 'LA',
bookingDate: booking?.bookingDate.toISOString() ?? new Date().toISOString(),
bookingDate: booking?.bookingDate.toISOString() || '',
...(state.fields ?? {}),
},
});

// Time Stamp
const [time, setSelectedTime] = useState<number | undefined>(booking?.bookingDate.getTime());
const watchDate = form.watch('bookingDate');
const availableTimes = createAvailableTimes(new Date(watchDate));

const formRef = useRef<HTMLFormElement>(null);

const handleClearForm = () => {
form.reset();
setSelectedTime(undefined);
};

useEffect(() => {
Expand All @@ -49,6 +57,16 @@ export default function BookingForm({ booking }: { booking?: Booking | null }) {
}
}, [state.bookingRef, isPending, router, state.message]);

useEffect(() => {
if (time) {
const newDateWithTime = new Date(watchDate);
const hours = new Date(time).getHours();
newDateWithTime.setHours(hours);
form.setValue('bookingDate', newDateWithTime.toISOString());
return;
}
}, [watchDate, time, form]);

return (
<div className="w-96 border p-4 bg-slate-100 rounded-md">
<LoadingModal show={isPending} />
Expand All @@ -75,12 +93,21 @@ export default function BookingForm({ booking }: { booking?: Booking | null }) {
<BookingFormField placeholder="Address" name="street-address" />
<BookingFormField placeholder="Postcode" name="postal-code" />
<BookingFormField placeholder="State" name="state" disabled />
<DateTimePickerForm />
<DateTimePickerForm onChange={() => setSelectedTime(undefined)} />
{watchDate && (
<TimeSelect
selectedTime={time}
availableTimes={availableTimes}
onTimeSelect={setSelectedTime}
/>
)}
<div className="flex gap-1 justify-end mt-4">
<Button onClick={handleClearForm} variant="outline">
Clear
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{!booking?.id && (
<Button onClick={handleClearForm} variant="outline">
Clear
</Button>
)}
<Button type="submit" disabled={form.formState.isSubmitting || !time}>
Submit
</Button>
</div>
Expand Down
43 changes: 15 additions & 28 deletions suncityla/app/booking/components/TimeDatePicker/TimeDatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
"use client";
'use client';

import { useState } from "react";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useState } from 'react';
import { format } from 'date-fns';
import { Calendar as CalendarIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
} from '@/components/ui/form';

export function DateTimePickerForm() {
export function DateTimePickerForm({ onChange }: { onChange: () => void }) {
const [open, setOpen] = useState(false);

return (
Expand All @@ -34,16 +30,12 @@ export function DateTimePickerForm() {
onClick={() => setOpen(true)}
variant="outline"
className={cn(
"w-full justify-start font-normal",
!field.value && "text-muted-foreground"
'w-full justify-start font-normal',
!field.value && 'text-muted-foreground',
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
{field.value ? format(field.value, 'PPP') : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
</FormControl>
Expand All @@ -54,17 +46,12 @@ export function DateTimePickerForm() {
onSelect={(date) => {
field.onChange(date?.toISOString());
setOpen(false);
onChange();
}}
initialFocus
/>
</PopoverContent>
</Popover>
<input
type="hidden"
{...field}
name={field.name}
value={field.value}
/>
<input type="hidden" {...field} name={field.name} value={field.value} />
<FormDescription />
<FormMessage />
</FormItem>
Expand Down
59 changes: 59 additions & 0 deletions suncityla/app/booking/components/TimeSelect/TimeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';

import * as React from 'react';

import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { FormField, FormItem } from '@/components/ui/form';
import { format } from 'date-fns';

export function TimeSelect({
availableTimes,
selectedTime,
onTimeSelect,
}: {
availableTimes: { from: number; to: number }[];
selectedTime?: number;
onTimeSelect: (time: number) => void;
}) {
return (
<FormField
name=""
render={() => {
return (
<FormItem>
<Select
onValueChange={(time) => onTimeSelect(Number(time))}
value={selectedTime?.toString()}
>
<SelectTrigger className="w-[180px] bg-white">
<SelectValue placeholder="Select a time" />
</SelectTrigger>
<div className="bg-white">
<SelectContent>
<SelectGroup>
<SelectLabel>Times</SelectLabel>
{availableTimes.map(({ from, to }, i) => (
<SelectItem className="cursor-pointer" key={i} value={from.toString()}>
{format(from, 'h:mm a')} - {format(to, 'h:mm a')}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</div>
</Select>
</FormItem>
);
}}
/>
);
}

export default TimeSelect;
12 changes: 12 additions & 0 deletions suncityla/app/booking/components/TimeSelect/times.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const createAvailableTimes = (date: Date) => {
const availableTimes = [];
for (let i = 8; i < 20; i++) {
availableTimes.push({
from: date.setHours(i, 0, 0, 0),
to: date.setHours(i + 1, 0, 0, 0),
});
}
return availableTimes;
};

export default createAvailableTimes;
81 changes: 37 additions & 44 deletions suncityla/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,60 @@
"use client";
'use client';

import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import * as React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from 'react-day-picker';

import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';

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

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
className={cn('p-3', className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md',
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal aria-selected:opacity-100',
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
Chevron(props) {
if (props.orientation === "left") {
if (props.orientation === 'left') {
return <ChevronLeft className="h-4 w-4" {...props} />;
}
return <ChevronRight className="h-4 w-4" {...props} />;
Expand All @@ -71,6 +64,6 @@ function Calendar({
/>
);
}
Calendar.displayName = "Calendar";
Calendar.displayName = 'Calendar';

export { Calendar };
Loading

0 comments on commit ff7c17f

Please sign in to comment.