Skip to Content
Module 6: Professional Forms🎯 Mini Project: Job Application

Module 6.5: Mini Project 5 - Job Application Portal

A 3D isometric view of a multi-stage portal. A user avatar is hopping across floating platforms labeled "Personal Info", "Experience", and "Review". Each platform lights up green as the user passes. In the background, a large "Submit" button glows at the finish line. The design is sleek, using professional corporate colors like navy blue and white.

(AI Image Gen): > Prompt: A 3D isometric view of a multi-stage portal. A user avatar is hopping across floating platforms labeled “Personal Info”, “Experience”, and “Review”. Each platform lights up green as the user passes. In the background, a large “Submit” button glows at the finish line. The design is sleek, using professional corporate colors like navy blue and white.

🛠️ Image Assets Script

Run the following script in your terminal to create placeholder image files:

mkdir -p images/b06-5 touch images/b06-5/b06-5-banner.png touch images/b06-5/b06-5-multi-step-flow.png touch images/b06-5/b06-5-final-ui.png

🎯 Learning Objectives

In this project, we’ll combine everything we’ve learned about Forms (RHF, Zod, Dynamic Fields) to build a professional job application form.

Key Features:

  • Multi-step Form (Wizard): Break down a long form into 3 smaller steps.
  • Step Validation: Validate data before allowing users to proceed to the next step.
  • Dynamic Fields: Enable applicants to add/remove their Work History (useFieldArray).
  • Review Mode: Review all submitted information before final submission.

1. Project Structure & Flow

The form will be divided into 3 steps:

  1. Personal Info: Name, Email, Phone Number.
  2. Experience: List of previous companies (Dynamic).
  3. Review & Submit: Display an overview and a submit button.

A horizontal timeline diagram showing 3 steps. Step 1 (Active): "Contact Details" with an input icon. Step 2 (Pending): "Work History" with a dynamic list icon. Step 3 (Pending): "Review" with a magnifying glass icon. Arrows connect the steps indicating forward/backward navigation.

(AI Image Gen): > Prompt: A horizontal timeline diagram showing the 3 steps. Step 1 (Active): “Contact Details” with an input icon. Step 2 (Pending): “Work History” with a dynamic list icon. Step 3 (Pending): “Review” with a magnifying glass icon. Arrows connect the steps indicating forward/backward navigation.

2. Setup and Schema

Install the necessary libraries:

npm install react-hook-form zod @hookform/resolvers

File: src/schemas/jobApplicationSchema.ts

import { z } from 'zod'; export const jobApplicationSchema = z.object({ fullName: z.string().min(2, 'Full name must be at least 2 characters'), email: z.string().email('Invalid email address'), phone: z.string().min(10, 'Invalid phone number'), // Dynamic array experiences: z .array( z.object({ company: z.string().min(1, 'Please enter company name'), position: z.string().min(1, 'Please enter position'), years: z.number().min(0, 'Invalid number of years'), }) ) .min(1, 'Please add at least 1 work experience'), }); // Infer Type from Schema export type JobApplicationFormValues = z.infer<typeof jobApplicationSchema>;

3. Building the Form Component

We’ll use a single form to wrap all steps and maintain state. Switching steps simply involves toggling the UI visibility.

File: src/components/JobApplicationForm.tsx

3.1. Setup & Step 1 (Personal Info)

import { useState } from "react"; import { useForm, useFieldArray, SubmitHandler, TriggerConfig } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { jobApplicationSchema, JobApplicationFormValues } from "../schemas/jobApplicationSchema"; export default function JobApplicationForm() { const [step, setStep] = useState(1); const { register, control, handleSubmit, trigger, // Used for manual step-by-step validation formState: { errors }, watch } = useForm<JobApplicationFormValues>({ resolver: zodResolver(jobApplicationSchema), defaultValues: { experiences: [{ company: "", position: "", years: 0 }] } }); const { fields, append, remove } = useFieldArray({ control, name: "experiences" }); // Data to display for Review const formData = watch(); // Function to safely advance to the next step const nextStep = async () => { let isValid = false; // Validate fields based on the current step if (step === 1) { isValid = await trigger(["fullName", "email", "phone"]); } else if (step === 2) { isValid = await trigger("experiences"); } if (isValid) setStep((prev) => prev + 1); }; const prevStep = () => setStep((prev) => prev - 1); const onSubmit: SubmitHandler<JobApplicationFormValues> = (data) => { console.log("Final Submission:", data); alert("Application submitted successfully!"); }; return ( <div className="max-w-2xl mx-auto p-6 bg-white shadow-xl rounded-2xl mt-10"> {/* Progress Bar */} <div className="flex justify-between mb-8 text-sm font-bold text-gray-400"> <span className={step >= 1 ? "text-blue-600" : ""}>Step 1: Info</span> <span className={step >= 2 ? "text-blue-600" : ""}>Step 2: Experience</span> <span className={step >= 3 ? "text-blue-600" : ""}>Step 3: Review</span> </div> <form onSubmit={handleSubmit(onSubmit)}> {/* === STEP 1: PERSONAL INFO === */} {step === 1 && ( <div className="space-y-4 animate-fade-in"> <h2 className="text-xl font-bold">Personal Information</h2> <div> <label>Full Name</label> <input {...register("fullName")} className="w-full border p-2 rounded" /> {errors.fullName && <p className="text-red-500 text-sm">{errors.fullName.message}</p>} </div> <div> <label>Email</label> <input {...register("email")} className="w-full border p-2 rounded" /> {errors.email && <p className="text-red-500 text-sm">{errors.email.message}</p>} </div> <div> <label>Phone Number</label> <input {...register("phone")} className="w-full border p-2 rounded" /> {errors.phone && <p className="text-red-500 text-sm">{errors.phone.message}</p>} </div> </div> )} {/* CONTINUED... */}

3.2. Step 2: Dynamic Experience & Navigation

Next, let’s add Step 2 and the navigation buttons within the form.

{/* === STEP 2: EXPERIENCE (Dynamic) === */} {step === 2 && ( <div className="space-y-4 animate-fade-in"> <h2 className="text-xl font-bold">Work Experience</h2> {fields.map((field, index) => ( <div key={field.id} className="p-4 border rounded bg-gray-50 relative"> <div className="grid grid-cols-2 gap-4"> <div> <input {...register(`experiences.${index}.company` as const)} placeholder="Company" className="w-full p-2 border rounded" /> {errors.experiences?.[index]?.company && <p className="text-red-500 text-xs">Required</p>} </div> <div> <input {...register(`experiences.${index}.position` as const)} placeholder="Position" className="w-full p-2 border rounded" /> </div> </div> <div className="mt-2"> <input type="number" {...register(`experiences.${index}.years` as const, { valueAsNumber: true })} placeholder="Years" className="w-20 p-2 border rounded" /> </div> {index > 0 && ( <button type="button" onClick={() => remove(index)} className="absolute top-2 right-2 text-red-500 font-bold" > </button> )} </div> ))} <button type="button" onClick={() => append({ company: "", position: "", years: 0 })} className="mt-2 text-blue-600 font-bold hover:underline" > + Add Experience </button> {errors.experiences && <p className="text-red-500 text-sm">{errors.experiences.message}</p>} </div> )} {/* === STEP 3: REVIEW === */} {step === 3 && ( <div className="space-y-4 animate-fade-in"> <h2 className="text-xl font-bold">Review Application</h2> <div className="bg-gray-100 p-4 rounded"> <p><strong>Full Name:</strong> {formData.fullName}</p> <p><strong>Email:</strong> {formData.email}</p> <p><strong>Experience:</strong> {formData.experiences?.length} companies</p> </div> </div> )} {/* NAVIGATION BUTTONS */} <div className="mt-8 flex justify-between"> {step > 1 && ( <button type="button" onClick={prevStep} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400" > Back </button> )} {step < 3 ? ( <button type="button" onClick={nextStep} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 ml-auto" > Continue </button> ) : ( <button type="submit" className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 ml-auto" > Submit Application </button> )} </div> </form> </div> ); }

A laptop screen showing the "Job Application Portal". The interface is clean and split into sections. The user is currently on Step 2, adding a second work experience entry. The buttons "Back" and "Next" are clearly visible.

(AI Image Gen): > Prompt: A laptop screen showing the “Job Application Portal”. The interface is clean and split into sections. The user is currently on Step 2, adding a second work experience entry. The buttons “Back” and “Next” are clearly visible.

4. Summary

In this Mini Project, you practiced:

  1. Multi-step Wizard: Techniques for breaking down a form and using state to switch the UI.
  2. Trigger Validation: Using the trigger("fieldName") function to manually validate each step before allowing users to Continue.
  3. Complex Zod Schema: Validating an array of objects (z.array(z.object(...))).
  4. Watch: Tracking form values to display the Review page.

5. Advanced Challenges (Optional)

  1. Persist Form: Save form data to localStorage to prevent data loss on page refresh.
  2. File Upload: Add a CV (PDF) upload step using an Uncontrolled Input within RHF.
  3. Animation: Add fluid transition effects when switching steps (using Framer Motion).
Last updated on