Merge pull request 'added more descriptive error handling of forms' (#11) from admin-forms into main
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Vercel Production Deployment / Deploy-Production (push) Successful in 1m23s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Vercel Production Deployment / Deploy-Production (push) Successful in 1m23s
				
			Reviewed-on: #11
This commit is contained in:
		
						commit
						a507fe84a4
					
				@ -23,19 +23,22 @@ export default function CreateCountryForm() {
 | 
				
			|||||||
  }, [formState.message]);
 | 
					  }, [formState.message]);
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <form ref={formRef} action={formAction} className="flex flex-col gap-2">
 | 
					    <form ref={formRef} action={formAction} className="flex flex-col gap-2">
 | 
				
			||||||
      <div className="flex items-center gap-2">
 | 
					      <div className="flex max-w-3xl items-center gap-2">
 | 
				
			||||||
        <Input
 | 
					        <Input
 | 
				
			||||||
          name="name"
 | 
					          name="name"
 | 
				
			||||||
          id="name"
 | 
					          id="name"
 | 
				
			||||||
          className={clsx({ "border-red-500": formState.errors?.name })}
 | 
					          className={clsx({ "border-red-500": formState.errors?.name })}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        {formState.message === "success" ? (
 | 
					        {formState.message !== "" && !formState.errors?.name ? (
 | 
				
			||||||
          <Check className="text-green-500" />
 | 
					          <Check className="text-green-500" />
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
          ""
 | 
					          ""
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        {formState.message === "error" ? (
 | 
					        {formState.errors?.name ? (
 | 
				
			||||||
          <CircleX className="text-red-500" />
 | 
					          <div className="flex min-w-96 items-center gap-1 text-red-500">
 | 
				
			||||||
 | 
					            <CircleX />
 | 
				
			||||||
 | 
					            <span className="text-sm">{formState.errors?.name}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
          ""
 | 
					          ""
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
import { Input } from "~/components/ui/input";
 | 
					import { Input } from "~/components/ui/input";
 | 
				
			||||||
import SubmitButton from "../SubmitButton";
 | 
					import SubmitButton from "../SubmitButton";
 | 
				
			||||||
import { useActionState } from "react";
 | 
					import { useEffect, useRef } from "react";
 | 
				
			||||||
import { addRegion } from "~/server/actions/addRegion";
 | 
					import { addRegion } from "~/server/actions/addRegion";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Select,
 | 
					  Select,
 | 
				
			||||||
@ -11,6 +11,8 @@ import {
 | 
				
			|||||||
  SelectValue,
 | 
					  SelectValue,
 | 
				
			||||||
} from "~/components/ui/select";
 | 
					} from "~/components/ui/select";
 | 
				
			||||||
import { useFormState } from "react-dom";
 | 
					import { useFormState } from "react-dom";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import { Check, CircleX } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Country {
 | 
					interface Country {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
@ -18,25 +20,72 @@ interface Country {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function CreateRegionForm(props: { countries: Country[] }) {
 | 
					export default function CreateRegionForm(props: { countries: Country[] }) {
 | 
				
			||||||
  const [state, formAction] = useFormState(addRegion, null);
 | 
					  const [formState, formAction] = useFormState(addRegion, {
 | 
				
			||||||
 | 
					    message: "",
 | 
				
			||||||
 | 
					    errors: undefined,
 | 
				
			||||||
 | 
					    fieldValues: {
 | 
				
			||||||
 | 
					      name: "",
 | 
				
			||||||
 | 
					      countryId: "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
  const { countries } = props;
 | 
					  const { countries } = props;
 | 
				
			||||||
 | 
					  const formRef = useRef<HTMLFormElement>(null);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (formState.message === "success") {
 | 
				
			||||||
 | 
					      formRef.current?.reset();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [formState.message]);
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <form action={formAction}>
 | 
					    <form ref={formRef} action={formAction} className="flex flex-col gap-2">
 | 
				
			||||||
      <Input name="name" />
 | 
					      <div className="flex max-w-3xl items-center gap-2">
 | 
				
			||||||
      <Select name="country">
 | 
					        <Input
 | 
				
			||||||
        <SelectTrigger className="w-[180px]">
 | 
					          name="name"
 | 
				
			||||||
          <SelectValue placeholder="Country" />
 | 
					          id="name"
 | 
				
			||||||
        </SelectTrigger>
 | 
					          placeholder="Name"
 | 
				
			||||||
        <SelectContent>
 | 
					          className={`${clsx({ "border-red-500": formState.errors?.name })}`}
 | 
				
			||||||
          {countries.map((country) => (
 | 
					        />
 | 
				
			||||||
            <SelectItem key={country.id} value={country.id}>
 | 
					        {formState.message === "success" && !formState.errors?.name ? (
 | 
				
			||||||
              {country.name}
 | 
					          <Check className="text-green-500" />
 | 
				
			||||||
            </SelectItem>
 | 
					        ) : (
 | 
				
			||||||
          ))}
 | 
					          ""
 | 
				
			||||||
        </SelectContent>
 | 
					        )}
 | 
				
			||||||
      </Select>
 | 
					        {formState.errors?.name ? (
 | 
				
			||||||
 | 
					          <div className="flex min-w-40 items-center gap-1 text-red-500">
 | 
				
			||||||
 | 
					            <CircleX className="text-red-500" />
 | 
				
			||||||
 | 
					            <span className="text-sm">{formState.errors?.name}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          ""
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className="flex max-w-3xl items-center gap-2">
 | 
				
			||||||
 | 
					        <Select name="country">
 | 
				
			||||||
 | 
					          <SelectTrigger
 | 
				
			||||||
 | 
					            className={`w-[180px] ${clsx({ "border-red-500": formState.errors?.countryId })}`}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <SelectValue placeholder="Country" />
 | 
				
			||||||
 | 
					          </SelectTrigger>
 | 
				
			||||||
 | 
					          <SelectContent>
 | 
				
			||||||
 | 
					            {countries.map((country) => (
 | 
				
			||||||
 | 
					              <SelectItem key={country.id} value={country.id}>
 | 
				
			||||||
 | 
					                {country.name}
 | 
				
			||||||
 | 
					              </SelectItem>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </SelectContent>
 | 
				
			||||||
 | 
					        </Select>
 | 
				
			||||||
 | 
					        {formState.message !== "" && !formState.errors?.countryId ? (
 | 
				
			||||||
 | 
					          <Check className="text-green-500" />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          ""
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {formState.errors?.countryId ? (
 | 
				
			||||||
 | 
					          <CircleX className="text-red-500" />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          ""
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <SubmitButton text={"Region"} />
 | 
					      <SubmitButton text={"Region"} />
 | 
				
			||||||
      <span>{state?.message}</span>
 | 
					 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,9 @@ import { ZodError, z } from "zod";
 | 
				
			|||||||
import { eq } from "drizzle-orm";
 | 
					import { eq } from "drizzle-orm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function addCountry(prevstate: any, formData: FormData) {
 | 
					export async function addCountry(prevstate: any, formData: FormData) {
 | 
				
			||||||
 | 
					  //assign formdaata to variables.
 | 
				
			||||||
  const name = (formData.get("name") as string).toLowerCase();
 | 
					  const name = (formData.get("name") as string).toLowerCase();
 | 
				
			||||||
 | 
					  //check if country already exists
 | 
				
			||||||
  const exists = await db
 | 
					  const exists = await db
 | 
				
			||||||
    .select({ name: countries.name })
 | 
					    .select({ name: countries.name })
 | 
				
			||||||
    .from(countries)
 | 
					    .from(countries)
 | 
				
			||||||
@ -26,10 +28,7 @@ export async function addCountry(prevstate: any, formData: FormData) {
 | 
				
			|||||||
      name,
 | 
					      name,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    //If the name doesn't exist, add the country to the database abd revalidate the page
 | 
					    //If the name doesn't exist, add the country to the database abd revalidate the page
 | 
				
			||||||
    await db
 | 
					    await db.insert(countries).values({ name });
 | 
				
			||||||
      .insert(countries)
 | 
					 | 
				
			||||||
      .values({ name })
 | 
					 | 
				
			||||||
      .returning({ name: countries.name });
 | 
					 | 
				
			||||||
    revalidatePath("/");
 | 
					    revalidatePath("/");
 | 
				
			||||||
    //Return a success message
 | 
					    //Return a success message
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@ -42,6 +41,7 @@ export async function addCountry(prevstate: any, formData: FormData) {
 | 
				
			|||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    const zodError = error as ZodError;
 | 
					    const zodError = error as ZodError;
 | 
				
			||||||
    const errorMap = zodError.flatten().fieldErrors;
 | 
					    const errorMap = zodError.flatten().fieldErrors;
 | 
				
			||||||
 | 
					    //Return an error object with the field values and errors.
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      message: "error",
 | 
					      message: "error",
 | 
				
			||||||
      errors: {
 | 
					      errors: {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,40 +3,63 @@
 | 
				
			|||||||
import { revalidatePath } from "next/cache";
 | 
					import { revalidatePath } from "next/cache";
 | 
				
			||||||
import { db } from "../db";
 | 
					import { db } from "../db";
 | 
				
			||||||
import { regions } from "../db/schema";
 | 
					import { regions } from "../db/schema";
 | 
				
			||||||
import { z } from "zod";
 | 
					import { ZodError, z } from "zod";
 | 
				
			||||||
 | 
					import { eq } from "drizzle-orm";
 | 
				
			||||||
const schema = z.object({
 | 
					 | 
				
			||||||
  countryId: z.string().min(1, "No country selected"),
 | 
					 | 
				
			||||||
  name: z.string().min(1, "Name is required"),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addRegion = async (prevstate: any, formData: FormData) => {
 | 
					export const addRegion = async (prevstate: any, formData: FormData) => {
 | 
				
			||||||
  const regionData = {
 | 
					  //assign formdaata to variables.
 | 
				
			||||||
    name: (formData.get("name") as string).toLowerCase(),
 | 
					  const name = (formData.get("name") as string).toLowerCase();
 | 
				
			||||||
    countryId: formData.get("country") as string,
 | 
					  const countryId = formData.get("country") as string;
 | 
				
			||||||
  };
 | 
					
 | 
				
			||||||
  const newRegion = schema.safeParse(regionData);
 | 
					  //check if region already exists in country
 | 
				
			||||||
  if (!newRegion.success) {
 | 
					  const exists = await db
 | 
				
			||||||
    return {
 | 
					    .select({ name: regions.name })
 | 
				
			||||||
      message: newRegion.error.issues[0]?.message,
 | 
					    .from(regions)
 | 
				
			||||||
      data: newRegion.data,
 | 
					    .where(eq(regions.countryId, countryId) && eq(regions.name, name));
 | 
				
			||||||
    };
 | 
					
 | 
				
			||||||
  }
 | 
					  //Define the schema for the form data
 | 
				
			||||||
  const confirmedRegion = await db
 | 
					  const schema = z.object({
 | 
				
			||||||
    .insert(regions)
 | 
					    countryId: z.string().min(1, "No country selected"),
 | 
				
			||||||
    .values(newRegion.data)
 | 
					    name: z
 | 
				
			||||||
    .onConflictDoNothing()
 | 
					      .string()
 | 
				
			||||||
    .returning({ name: regions.name });
 | 
					      .min(1, "Name is required")
 | 
				
			||||||
  if (!confirmedRegion[0]) {
 | 
					      .refine(() => !exists[0], {
 | 
				
			||||||
    return {
 | 
					        message: `${name} already exists in selected country`,
 | 
				
			||||||
      message: `${newRegion.data.name} already exists`,
 | 
					      }),
 | 
				
			||||||
      data: newRegion.data,
 | 
					  });
 | 
				
			||||||
    };
 | 
					
 | 
				
			||||||
  } else {
 | 
					  //Parse the form data using the schema for validation, and check if the name already exists
 | 
				
			||||||
    const message = `${newRegion.data.name} added`;
 | 
					  try {
 | 
				
			||||||
    const errors = newRegion.error;
 | 
					    schema.parse({
 | 
				
			||||||
    const data = newRegion.data;
 | 
					      countryId,
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    //If the name doesn't exist, add the country to the database abd revalidate the page
 | 
				
			||||||
 | 
					    await db.insert(regions).values({ countryId, name });
 | 
				
			||||||
    revalidatePath("/");
 | 
					    revalidatePath("/");
 | 
				
			||||||
    return { message, errors, data };
 | 
					    //Return a success message
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      message: "success",
 | 
				
			||||||
 | 
					      errors: undefined,
 | 
				
			||||||
 | 
					      fieldValues: {
 | 
				
			||||||
 | 
					        name: "",
 | 
				
			||||||
 | 
					        countryId: "",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    const zodError = error as ZodError;
 | 
				
			||||||
 | 
					    const errorMap = zodError.flatten().fieldErrors;
 | 
				
			||||||
 | 
					    //Return an error object with the field values and errors.
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      message: "error",
 | 
				
			||||||
 | 
					      errors: {
 | 
				
			||||||
 | 
					        name: errorMap["name"]?.[0] ?? "",
 | 
				
			||||||
 | 
					        countryId: errorMap["countryId"]?.[0] ?? "",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      fieldValues: {
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        countryId,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user