Improve booking form.
CI / Docker Lint (push) Successful in 23s

This commit is contained in:
2024-09-09 09:27:09 +02:00
parent 191379ed03
commit 532337a46a
3 changed files with 165 additions and 153 deletions
+52 -50
View File
@@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
@@ -8,27 +8,27 @@ import {
FieldValues, FieldValues,
FormProvider, FormProvider,
useFormContext, useFormContext,
} from "react-hook-form" } from "react-hook-form";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue,
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
@@ -36,21 +36,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@@ -59,36 +59,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue,
) );
const FormItem = React.forwardRef< const FormItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
}) });
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
@@ -97,15 +97,16 @@ const FormLabel = React.forwardRef<
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<Slot <Slot
@@ -119,15 +120,15 @@ const FormControl = React.forwardRef<
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
}) });
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
@@ -136,19 +137,19 @@ const FormDescription = React.forwardRef<
className={cn("text-[0.8rem] text-muted-foreground", className)} className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props} {...props}
/> />
) );
}) });
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
@@ -160,11 +161,12 @@ const FormMessage = React.forwardRef<
> >
{body} {body}
</p> </p>
) );
}) });
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage";
export { export {
// eslint-disable-next-line react-refresh/only-export-components
useFormField, useFormField,
Form, Form,
FormItem, FormItem,
@@ -173,4 +175,4 @@ export {
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };
+10
View File
@@ -0,0 +1,10 @@
"use client";
import { z } from "zod";
export const BookingFormSchema = z.object({
name: z.string().min(2).max(50),
surname: z.string().min(2).max(50),
email: z.string().email(),
phone: z.string().min(8),
});
+103 -103
View File
@@ -1,14 +1,24 @@
import { APISendCreateBooking } from "@/api/api-booking"; import { APISendCreateBooking } from "@/api/api-booking";
import { BookingDetails } from "@/components/booking/booking-details"; import { BookingDetails } from "@/components/booking/booking-details";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Typography } from "@/components/ui/typography"; import { Typography } from "@/components/ui/typography";
import { useToast } from "@/components/ui/use-toast"; import { BookingFormSchema } from "@/model/FormSchema";
import { useAppStore } from "@/store"; import { useAppStore } from "@/store";
import { ChangeEvent, useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
enum PageState { enum PageState {
noData, noData,
@@ -22,58 +32,29 @@ function BookPage() {
const [pageState, setPageState] = useState<PageState>(PageState.showForm); const [pageState, setPageState] = useState<PageState>(PageState.showForm);
const bookingData = useAppStore((state) => state.newBookingData); const bookingData = useAppStore((state) => state.newBookingData);
const setCustomerData = useAppStore((state) => state.setBookingCustomer); const setCustomerData = useAppStore((state) => state.setBookingCustomer);
const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
if (!bookingData || !bookingData.customerDetails) { const bookingForm = useForm<z.infer<typeof BookingFormSchema>>({
return ( resolver: zodResolver(BookingFormSchema),
<Typography variant={"h2"}>You must select some rooms...</Typography> defaultValues: {
); email: "",
} name: "",
surname: "",
phone: "",
},
});
function handleOnChange(e: ChangeEvent<HTMLInputElement>) { function onSubmit(values: z.infer<typeof BookingFormSchema>) {
if (bookingData == null) { if (bookingData == null) {
return; return;
} }
const newCustomerData = bookingData.customerDetails; const newCustomerData = bookingData.customerDetails;
newCustomerData.name = values.name;
switch (e.target.name) { newCustomerData.surname = values.surname;
case "name": newCustomerData.email = values.email;
newCustomerData.name = e.target.value; newCustomerData.phoneNumber = values.phone;
break;
case "surname":
newCustomerData.surname = e.target.value;
break;
case "email":
newCustomerData.email = e.target.value;
break;
case "phoneNumber":
newCustomerData.phoneNumber = e.target.value;
break;
default:
break;
}
setCustomerData(newCustomerData); setCustomerData(newCustomerData);
}
function handleOnBookingSubmit() {
const customerDetails = bookingData?.customerDetails;
if (
!customerDetails ||
customerDetails.name == "" ||
customerDetails.surname == "" ||
customerDetails.phoneNumber == "" ||
customerDetails.email == ""
) {
toast({
variant: "destructive",
title: "Uh oh! You must fill all fields.",
});
return;
}
setPageState(PageState.sending); setPageState(PageState.sending);
console.log(bookingData); console.log(bookingData);
@@ -90,70 +71,89 @@ function BookPage() {
} }
}; };
if (!bookingData || !bookingData.customerDetails) {
return (
<Typography variant={"h2"}>You must select some rooms...</Typography>
);
}
return ( return (
<> <>
<Typography variant={"h1"}>Booking Details</Typography> <Typography variant={"h1"}>Booking Details</Typography>
<Separator className="my-10" /> <Separator className="my-10" />
<div className="lg:grid lg:grid-cols-40/60 lg:gap-x-10">
<div> <div>
<Typography variant={"h3"}>Personal Details</Typography> <Typography variant={"h3"} className="my-4">
<div> Personal Details
<div>
<Label htmlFor="name">Name</Label>
<Input
type="text"
name="name"
id="name"
placeholder="Name"
value={bookingData.customerDetails.name ?? ""}
onChange={handleOnChange}
/>
</div>
<div>
<Label htmlFor="surname">Surname</Label>
<Input
type="text"
name="surname"
id="surname"
placeholder="Surname"
value={bookingData.customerDetails.surname ?? ""}
onChange={handleOnChange}
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
type="email"
name="email"
id="email"
placeholder="Email"
value={bookingData.customerDetails.email ?? ""}
onChange={handleOnChange}
/>
</div>
<div>
<Label htmlFor="phone">Phone Number</Label>
<Input
type="tel"
name="phoneNumber"
id="phone"
placeholder="Number"
value={bookingData.customerDetails.phoneNumber ?? ""}
onChange={handleOnChange}
/>
</div>
</div>
</div>
<Separator className="my-10" />
<BookingDetails mode="View"></BookingDetails>
<Separator className="my-10" />
<Button
onClick={handleOnBookingSubmit}
disabled={pageState == PageState.sending}
>
<Typography variant={"h3"} className="text-primary-foreground">
Book it now!
</Typography> </Typography>
<Form {...bookingForm}>
<form
onSubmit={bookingForm.handleSubmit(onSubmit)}
className="space-y-8"
>
<div className="flex w-full flex-row gap-x-3">
<FormField
control={bookingForm.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
></FormField>
<FormField
control={bookingForm.control}
name="surname"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Surname</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
></FormField>
</div>
<FormField
control={bookingForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
></FormField>
<FormField
control={bookingForm.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
></FormField>
<Button type="submit" disabled={pageState == PageState.sending}>
Submit
</Button> </Button>
</form>
</Form>
</div>
<Separator className="my-10 lg:hidden" />
<BookingDetails mode="View"></BookingDetails>
</div>
</> </>
); );
} }