Skip to content

Commit

Permalink
Feature/address search (#67)
Browse files Browse the repository at this point in the history
* 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
fmtabbara authored Nov 22, 2024
1 parent 3bb8342 commit ee3faac
Show file tree
Hide file tree
Showing 36 changed files with 3,017 additions and 464 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm ci --force
- run: echo ${{ env.DATABASE_URL }}
- run: npm run build --if-present
env:
Expand Down
15 changes: 15 additions & 0 deletions suncityla/app/bookings/[bookingId]/page.tsx
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>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import prisma from "@/prisma/prismaClient";
"use server";

export const dynamic = "force-dynamic";
import prisma from "@/prisma/prismaClient";

const getBookings = async () => {
const bookings = await prisma.booking.findMany();
Expand Down
50 changes: 50 additions & 0 deletions suncityla/app/bookings/actions/onSubmitAction.ts
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 suncityla/app/bookings/components/BookingForm/BookingForm.tsx
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 suncityla/app/bookings/components/BookingForm/BookingFormField.tsx
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;
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 suncityla/app/bookings/components/TimeDatePicker/TimePicker.tsx
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>
);
}
Loading

0 comments on commit ee3faac

Please sign in to comment.