generated from chingu-voyages/voyage-template
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add new bookings * add date picker * make bookings readable * add response to booking submission * add pending state * fix ci * new booking button in nav bar * reduce hero image size + move to tailwind classes * start moving some css over to tailwind * fix state bug
- Loading branch information
Showing
36 changed files
with
3,017 additions
and
464 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export default async function BookingConfirmed({ | ||
params, | ||
}: { | ||
params: Promise<{ bookingId: string }>; | ||
}) { | ||
const { bookingId } = await params; | ||
|
||
return ( | ||
<div> | ||
<h1>Booking Confirmed</h1> | ||
<p>Your booking has been confirmed.</p> | ||
<p>Booking id: {bookingId}</p> | ||
</div> | ||
); | ||
} |
4 changes: 2 additions & 2 deletions
4
suncityla/app/bookings/action.ts → ...ityla/app/bookings/actions/getBookings.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
"use server"; | ||
|
||
import prisma from "@/prisma/prismaClient"; | ||
import { bookingFormSchema } from "../new/schema"; | ||
|
||
export type FormState = { | ||
message: string; | ||
fields?: Record<string, string>; | ||
errors?: string[]; | ||
bookingRef?: string; | ||
}; | ||
|
||
const onSubmitAction = async ( | ||
prvState: FormState, | ||
data: FormData | ||
): Promise<FormState> => { | ||
const formData = Object.fromEntries(data); | ||
const parsed = bookingFormSchema.safeParse(formData); | ||
|
||
if (!parsed.success) { | ||
const fields: Record<string, string> = {}; | ||
for (const key of Object.keys(formData)) { | ||
fields[key] = formData[key].toString(); | ||
} | ||
|
||
return { | ||
message: "Invalid form data", | ||
fields, | ||
errors: parsed.error.issues.map((issue) => issue.message), | ||
}; | ||
} | ||
|
||
const booking = await prisma.booking.create({ | ||
data: { | ||
firstname: parsed.data.fname, | ||
lastname: parsed.data.lname, | ||
streetAddress: parsed.data["street-address"], | ||
state: parsed.data.state, | ||
postalCode: parsed.data["postal-code"], | ||
bookingDate: parsed.data.bookingDate, | ||
}, | ||
}); | ||
|
||
return { | ||
bookingRef: booking.id, | ||
message: "Booking created", | ||
}; | ||
}; | ||
|
||
export default onSubmitAction; |
90 changes: 90 additions & 0 deletions
90
suncityla/app/bookings/components/BookingForm/BookingForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
"use client"; | ||
|
||
import { Button } from "@/components/ui/button"; | ||
import { Form } from "@/components/ui/form"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
import { bookingFormSchema } from "../../new/schema"; | ||
import { useActionState, useRef, startTransition, useEffect } from "react"; | ||
import onSubmitAction from "../../actions/onSubmitAction"; | ||
import BookingFormField from "./BookingFormField"; | ||
import { DateTimePickerForm } from "../TimeDatePicker/TimeDatePicker"; | ||
import { useRouter } from "next/navigation"; | ||
import LoadingModal from "@/app/components/modals/Loading"; | ||
|
||
export type BookingFormData = z.infer<typeof bookingFormSchema>; | ||
|
||
export default function BookingForm() { | ||
const [state, formAction, isPending] = useActionState(onSubmitAction, { | ||
message: "", | ||
bookingRef: undefined, | ||
}); | ||
|
||
const router = useRouter(); | ||
|
||
const form = useForm<z.infer<typeof bookingFormSchema>>({ | ||
resolver: zodResolver(bookingFormSchema), | ||
defaultValues: { | ||
fname: "", | ||
lname: "", | ||
"street-address": "", | ||
"postal-code": "", | ||
state: "LA", | ||
bookingDate: "", | ||
...(state.fields ?? {}), | ||
}, | ||
}); | ||
|
||
const formRef = useRef<HTMLFormElement>(null); | ||
|
||
const handleClearForm = () => { | ||
form.reset(); | ||
}; | ||
|
||
useEffect(() => { | ||
if (state.bookingRef && !isPending) { | ||
router.push(`/bookings/${state.bookingRef}`); | ||
} | ||
}, [state.bookingRef, isPending, router]); | ||
|
||
return ( | ||
<div className="w-96 border p-4 bg-slate-100 rounded-md"> | ||
{isPending && <LoadingModal />} | ||
<Form {...form}> | ||
<form | ||
className="space-y-4" | ||
autoComplete="on" | ||
ref={formRef} | ||
action={formAction} | ||
onSubmit={(evt) => { | ||
evt.preventDefault(); | ||
form.handleSubmit(() => { | ||
startTransition(() => { | ||
if (!formRef.current) { | ||
throw Error("Form element not found"); | ||
} | ||
formAction(new FormData(formRef.current)); | ||
}); | ||
})(evt); | ||
}} | ||
> | ||
<BookingFormField placeholder="First name" name="fname" /> | ||
<BookingFormField placeholder="Last name" name="lname" /> | ||
<BookingFormField placeholder="Address" name="street-address" /> | ||
<BookingFormField placeholder="Postcode" name="postal-code" /> | ||
<BookingFormField placeholder="State" name="state" disabled /> | ||
<DateTimePickerForm /> | ||
<div className="flex gap-1 justify-end mt-4"> | ||
<Button onClick={handleClearForm} variant="outline"> | ||
Clear | ||
</Button> | ||
<Button type="submit" disabled={form.formState.isSubmitting}> | ||
Submit | ||
</Button> | ||
</div> | ||
</form> | ||
</Form> | ||
</div> | ||
); | ||
} |
45 changes: 45 additions & 0 deletions
45
suncityla/app/bookings/components/BookingForm/BookingFormField.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormControl, | ||
FormDescription, | ||
FormMessage, | ||
} from "@/components/ui/form"; | ||
import { Input } from "@/components/ui/input"; | ||
|
||
const BookingFormField = ({ | ||
placeholder, | ||
name, | ||
type, | ||
disabled, | ||
}: { | ||
placeholder: string; | ||
name: string; | ||
type?: string; | ||
disabled?: boolean; | ||
}) => { | ||
return ( | ||
<FormField | ||
name={name} | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel /> | ||
<FormControl> | ||
<Input | ||
type={type || "text"} | ||
className="bg-white" | ||
placeholder={placeholder} | ||
readOnly={disabled} | ||
{...field} | ||
/> | ||
</FormControl> | ||
<FormDescription /> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
); | ||
}; | ||
|
||
export default BookingFormField; |
74 changes: 74 additions & 0 deletions
74
suncityla/app/bookings/components/TimeDatePicker/TimeDatePicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
"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 { | ||
FormControl, | ||
FormDescription, | ||
FormField, | ||
FormItem, | ||
FormMessage, | ||
} from "@/components/ui/form"; | ||
|
||
export function DateTimePickerForm() { | ||
const [open, setOpen] = useState(false); | ||
|
||
return ( | ||
<FormField | ||
name="bookingDate" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<Popover open={open}> | ||
<FormControl> | ||
<PopoverTrigger asChild> | ||
<Button | ||
onClick={() => setOpen(true)} | ||
variant="outline" | ||
className={cn( | ||
"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> | ||
)} | ||
</Button> | ||
</PopoverTrigger> | ||
</FormControl> | ||
<PopoverContent className="w-auto p-0"> | ||
<Calendar | ||
mode="single" | ||
selected={field.value} | ||
onSelect={(date) => { | ||
field.onChange(date?.toISOString()); | ||
setOpen(false); | ||
}} | ||
initialFocus | ||
/> | ||
</PopoverContent> | ||
</Popover> | ||
<input | ||
type="hidden" | ||
{...field} | ||
name={field.name} | ||
value={field.value} | ||
/> | ||
<FormDescription /> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
); | ||
} |
50 changes: 50 additions & 0 deletions
50
suncityla/app/bookings/components/TimeDatePicker/TimePicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
"use client"; | ||
|
||
import * as React from "react"; | ||
import { Clock } from "lucide-react"; | ||
import { Label } from "@/components/ui/label"; | ||
import { TimePickerInput } from "./TimePickerInput"; | ||
|
||
interface TimePickerDemoProps { | ||
date: Date | undefined; | ||
setDate: (date: Date | undefined) => void; | ||
} | ||
|
||
export function TimePicker({ date, setDate }: TimePickerDemoProps) { | ||
const minuteRef = React.useRef<HTMLInputElement>(null); | ||
const hourRef = React.useRef<HTMLInputElement>(null); | ||
const secondRef = React.useRef<HTMLInputElement>(null); | ||
|
||
return ( | ||
<div className="flex items-end gap-2"> | ||
<div className="grid gap-1 text-center"> | ||
<Label htmlFor="hours" className="text-xs"> | ||
Hours | ||
</Label> | ||
<TimePickerInput | ||
picker="hours" | ||
date={date} | ||
setDate={setDate} | ||
ref={hourRef} | ||
onRightFocus={() => minuteRef.current?.focus()} | ||
/> | ||
</div> | ||
<div className="grid gap-1 text-center"> | ||
<Label htmlFor="minutes" className="text-xs"> | ||
Minutes | ||
</Label> | ||
<TimePickerInput | ||
picker="minutes" | ||
date={date} | ||
setDate={setDate} | ||
ref={minuteRef} | ||
onLeftFocus={() => hourRef.current?.focus()} | ||
onRightFocus={() => secondRef.current?.focus()} | ||
/> | ||
</div> | ||
<div className="flex h-10 items-center"> | ||
<Clock className="ml-2 h-4 w-4" /> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.