Module 6.5: Mini Project 5 - Job Application Portal

(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:
- Personal Info: Name, Email, Phone Number.
- Experience: List of previous companies (Dynamic).
- Review & Submit: Display an overview and a submit button.

(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/resolversFile: 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>
);
}
(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:
- Multi-step Wizard: Techniques for breaking down a form and using
stateto switch theUI. - Trigger Validation: Using the
trigger("fieldName")functionto manually validate each step before allowing users toContinue. - Complex Zod Schema: Validating an
arrayofobjects(z.array(z.object(...))). - Watch: Tracking form values to display the Review page.
5. Advanced Challenges (Optional)
- Persist Form: Save form data to
localStorageto prevent data loss on page refresh. - File Upload: Add a
CV (PDF)upload step using an Uncontrolled Input withinRHF. - Animation: Add
fluidtransition effects when switching steps (using Framer Motion).