Design Applicant Registration Form Specification
This form facilitates applicant registration. It comprises sections for academic history, personal data, contact details, and account creation.
- Application Category
- Secondary School
- Primary School
- Personal Details
- Contact Details
- Password
This React application uses Ant Design components, including but not limited to Tabs, Form, Input, Button, and Select, to render conditionally displayed sections based on data availability. State management is implemented using React hooks, specifically useState and useEffect, while backend interactions are simulated using mock JSON data. The following sections detail the individual components, their input parameters, and their corresponding React implementation strategies.
Project Setup
Before diving into the sections, set up your React project with Vite and Ant Design:
-
Main Component: Create a
RegistrationForm
component as the parent, managing the overall form state and submission. -
State Management: Use a single
useState
hook inRegistrationForm
to hold all form data, passing props to child components for each section. -
Ant Design: Utilize
Form
for form handling, validation, and submission. -
Mock Data: Use mock JSON objects or a mock API tool (e.g.,
json-server
) to simulate backend responses.
Here is a suggested state structure for the form:
const [formData, setFormData] = useState({
programme_level_id: "",
academic_year: "",
intake_description: "",
csee_country_id: "",
index_number: "",
exam_year: "",
primary_school: "",
primary_school_region: "",
primary_school_district: "",
first_name: "",
middle_name: "",
surname: "",
dob: "",
country_id_nationality: "",
country_id_residence: "",
marital_status: "",
impairment_id: "",
email: "",
phone: "",
password: "",
re_password: "",
});
const [nectaData, setNectaData] = useState(null); // For Secondary School NECTA data
const [showProfile, setShowProfile] = useState(false); // Controls visibility of profile sections
1. Application Category Section
Description
This section collects the applicant’s intended area of study and displays the current academic year and intake. It is the first step in the form.
Inputs
-
Applying For (
programme_level_id
):- Type: Dropdown (
<Select>
from Antd) - Options: List of active application categories (e.g., Undergraduate, Postgraduate)
- Required: Yes
- Type: Dropdown (
-
Year (
academic_year
):- Type: Text input (
<Input>
from Antd) - Read-only: Yes
- Value: Current academic year (e.g., "2023/2024")
- Type: Text input (
-
Intake (
intake_description
):- Type: Text input (
<Input>
from Antd) - Read-only: Yes
- Value: Current intake description (e.g., "September Intake")
- Type: Text input (
Steps
- Fetch the list of application categories for the dropdown when the component mounts.
- Fetch the current academic settings (year and intake) to populate the read-only fields.
- Allow the user to select an application category from the dropdown.
When to Fetch Data from Backend
-
Application Categories: Fetch when the component mounts to populate the dropdown.
-
Request:
GET /api/application-categories
- Sample Response:
[ { "id": 1, "title": "Undergraduate", "programme_level_id": 1 }, { "id": 2, "title": "Postgraduate", "programme_level_id": 2 } ]
-
Request:
-
Academic Settings: Fetch when the component mounts to set the year and intake.
-
Request:
GET /api/academic-settings
- Sample Response:
{ "academic_year": "2023/2024", "intake_description": "September Intake" }
-
Request:
React Implementation
import { Form, Select, Input } from "antd";
import { useEffect, useState } from "react";
const ApplicationCategorySection = ({ formData, setFormData }) => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Simulate fetching categories
const fetchCategories = async () => {
const response = [
{ id: 1, title: "\"Undergraduate\", programme_level_id: 1 },"
{ id: 2, title: "\"Postgraduate\", programme_level_id: 2 },"
];
setCategories(response);
};
const fetchSettings = async () => {
const settings = { academic_year: "2023/2024", intake_description: "\"September Intake\" };"
setFormData((prev) => ({ ...prev, ...settings }));
};
fetchCategories();
fetchSettings();
}, [setFormData]);
return (
<div>
<h5>Application Category</h5>
<Form.Item label="Applying For" required>
<Select
value={formData.programme_level_id}
onChange={(value) => setFormData({ ...formData, programme_level_id: value })}
placeholder="Choose Area of Study"
>
{categories.map((cat) => (
<Select.Option key={cat.id} value={cat.programme_level_id}>
{cat.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Year">
<Input value={formData.academic_year} readOnly />
</Form.Item>
<Form.Item label="Intake">
<Input value={formData.intake_description} readOnly />
</Form.Item>
</div>
);
};
Notes
- If the category list is empty, display a warning: "Window for new Application is currently closed," and disable the rest of the form.
2. Secondary School Section
Description
This section collects secondary school examination details (e.g., NECTA Form Four in Tanzania) and fetches additional data based on the input. It is only shown if there are active application categories.
Inputs
-
CSEE Country (
csee_country_id
):- Type: Dropdown (
<Select>
from Antd) - Options: List of countries
- Required: Yes
- Type: Dropdown (
-
Index Number (
index_number
):- Type: Text input (
<Input>
from Antd) - Placeholder: "CSEE or Equivalent Index Number"
- Read-only: Optional (can be editable for now)
- Format Note: "SXXXX/XXXX" or "PXXXX/XXXX" for NECTA, "EQYYYYXXXXXX" for equivalents
- Type: Text input (
-
Exam Year (
exam_year
):- Type: Dropdown (
<Select>
from Antd) - Options: Years from 1970 to the previous year (generate dynamically)
- Required: Yes
- Type: Dropdown (
Steps
- Fetch the list of countries for the CSEE Country dropdown.
- Generate the Exam Year options dynamically (1970 to current year - 1).
- When the user changes the Index Number or Exam Year, fetch NECTA data if both fields are filled and the Index Number is at least 10 characters long.
- Display a loading indicator (e.g.,
<Spin>
from Antd) while fetching. - Show the fetched school name and applicant name below the inputs.
When to Fetch Data from Backend
-
Countries: Fetch when the component mounts.
-
Request:
GET /api/countries
- Sample Response:
[ { "id": 1, "name": "Tanzania" }, { "id": 2, "name": "Kenya" } ]
-
Request:
-
NECTA Data: Fetch when Index Number or Exam Year changes.
-
Request:
POST /api/get-necta-data
- Request Body:
{ "index_number": "S1234/5678", "exam_year": "2020", "csee_country_id": 1, "exam_type_id": 1 }
- Sample Success Response:
{ "status": 1, "message": "Data fetched successfully", "csee_school_name": "Sample School", "result_string": "John Middle Doe" }
- Sample Error Response:
{ "status": 0, "message": "Invalid index number" }
-
Request:
React Implementation
import { Form, Select, Input, Spin } from "antd";
import { useEffect, useState } from "react";
const SecondarySchoolSection = ({ formData, setFormData, setNectaData, setShowProfile }) => {
const [countries, setCountries] = useState([]);
const [years] = useState(() => {
const currentYear = new Date().getFullYear() - 1;
return Array.from({ length: currentYear - 1970 + 1 }, (_, i) => currentYear - i);
});
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchCountries = async () => {
const response = [
{ id: 1, name: "Tanzania" },
{ id: 2, name: "Kenya" },
];
setCountries(response);
};
fetchCountries();
}, []);
const fetchNectaData = async () => {
if (formData.index_number && formData.exam_year && formData.index_number.length >= 10) {
setLoading(true);
const mockResponse = {
status: 1,
message: "Data fetched successfully",
csee_school_name: "Sample School",
result_string: "John Middle Doe",
};
setLoading(false);
if (mockResponse.status === 1) {
setNectaData(mockResponse);
const [first_name, ...rest] = mockResponse.result_string.split(" ");
const surname = rest.pop();
const middle_name = rest.join(" ");
setFormData((prev) => ({
...prev,
first_name,
middle_name,
surname,
}));
setShowProfile(true);
}
}
};
return (
<div>
<h5>Secondary School</h5>
<p>
<strong>Note:</strong> For NECTA, use format SXXXX/XXXX or PXXXX/XXXX. For equivalents, use
EQYYYYXXXXXX.
</p>
<Form.Item label="CSEE Country" required>
<Select
value={formData.csee_country_id}
onChange={(value) => setFormData({ ...formData, csee_country_id: value })}
placeholder="Select O' Level Country"
>
{countries.map((country) => (
<Select.Option key={country.id} value={country.id}>
{country.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Index Number" required>
<Input
value={formData.index_number}
onChange={(e) => {
setFormData({ ...formData, index_number: e.target.value });
fetchNectaData();
}}
placeholder="CSEE or Equivalent Index Number"
/>
</Form.Item>
<Form.Item label="Exam Year" required>
<Select
value={formData.exam_year}
onChange={(value) => {
setFormData({ ...formData, exam_year: value });
fetchNectaData();
}}
placeholder="Select CSEE Year"
>
{years.map((year) => (
<Select.Option key={year} value={year}>
{year}
</Select.Option>
))}
</Select>
</Form.Item>
{loading && <Spin />}
{nectaData && (
<div>
<p><strong>O'LEVEL SCHOOL NAME:</strong> {nectaData.csee_school_name}</p>
<p><strong>APPLICANT NAME:</strong> {nectaData.result_string}</p>
</div>
)}
</div>
);
};
Notes
- After a successful NECTA fetch, parse
result_string
(e.g., "John Middle Doe") intofirst_name
,middle_name
, andsurname
for form submission, even though these fields aren’t editable in the UI.
3. Primary School Section
Description
This section collects primary school details and is shown only after NECTA data is successfully fetched (controlled by showProfile
).
Inputs
-
Primary School (
primary_school
):- Type: Text input (
<Input>
from Antd) - Required: Yes
- Type: Text input (
-
Primary School Region (
primary_school_region
):- Type: Dropdown (
<Select>
from Antd) - Options: List of regions
- Required: Yes
- Type: Dropdown (
-
Primary School District (
primary_school_district
):- Type: Dropdown (
<Select>
from Antd) - Options: List of districts, dependent on the selected region
- Required: Yes
- Type: Dropdown (
Steps
- Fetch the list of regions when the component mounts.
- When the user selects a region, fetch the corresponding districts.
- Allow the user to input the primary school name and select a region and district.
When to Fetch Data from Backend
-
Regions: Fetch when the component mounts.
-
Request:
GET /api/regions
- Sample Response:
[ { "id": 1, "name": "Dar es Salaam" }, { "id": 2, "name": "Arusha" } ]
-
Request:
-
Districts: Fetch when the region changes.
-
Request:
GET /api/districts?region_id=1
- Sample Response:
[ { "id": 1, "name": "Ilala" }, { "id": 2, "name": "Kinondoni" } ]
-
Request:
React Implementation
import { Form, Input, Select } from "antd";
import { useEffect, useState } from "react";
const PrimarySchoolSection = ({ formData, setFormData }) => {
const [regions, setRegions] = useState([]);
const [districts, setDistricts] = useState([]);
useEffect(() => {
const fetchRegions = async () => {
const response = [
{ id: 1, name: "Dar es Salaam" },
{ id: 2, name: "Arusha" },
];
setRegions(response);
};
fetchRegions();
}, []);
const fetchDistricts = async (regionId) => {
const response = [
{ id: 1, name: "Ilala" },
{ id: 2, name: "Kinondoni" },
];
setDistricts(response);
};
return (
<div>
<h5>Primary School</h5>
<Form.Item label="Primary School" required>
<Input
value={formData.primary_school}
onChange={(e) => setFormData({ ...formData, primary_school: e.target.value })}
/>
</Form.Item>
<Form.Item label="Region" required>
<Select
value={formData.primary_school_region}
onChange={(value) => {
setFormData({ ...formData, primary_school_region: value, primary_school_district: "" });
fetchDistricts(value);
}}
placeholder="Select Region"
>
{regions.map((region) => (
<Select.Option key={region.id} value={region.id}>
{region.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="District" required>
<Select
value={formData.primary_school_district}
onChange={(value) => setFormData({ ...formData, primary_school_district: value })}
placeholder="Select District"
disabled={!formData.primary_school_region}
>
{districts.map((district) => (
<Select.Option key={district.id} value={district.id}>
{district.name}
</Select.Option>
))}
</Select>
</Form.Item>
</div>
);
};
4. Personal Details Section
Description
This section collects personal information and is shown only after NECTA data is fetched (showProfile
is true).
Inputs
-
Date of Birth (
dob
):- Type: Date picker (
<DatePicker>
from Antd) - Format: "YYYY-MM-DD"
- Year Range: 1970 to 10 years ago
- Required: Yes
- Type: Date picker (
-
Nationality (
country_id_nationality
):- Type: Dropdown (
<Select>
from Antd) - Options: List of countries
- Required: Yes
- Type: Dropdown (
-
Country of Residence (
country_id_residence
):- Type: Dropdown (
<Select>
from Antd) - Options: List of countries
- Required: Yes
- Type: Dropdown (
-
Marital Status (
marital_status
):- Type: Dropdown (
<Select>
from Antd) - Options: "Single", "Married", "Other"
- Required: Yes
- Type: Dropdown (
-
Impairment (
impairment_id
):- Type: Dropdown (
<Select>
from Antd) - Options: List of impairments
- Required: Yes
- Type: Dropdown (
Steps
- Fetch countries and impairments when the component mounts.
- Allow the user to select their date of birth, nationality, residence, marital status, and impairment.
When to Fetch Data from Backend
-
Countries: Reuse the
/api/countries
endpoint from the Secondary School section. -
Impairments: Fetch when the component mounts.
-
Request:
GET /api/impairments
- Sample Response:
[ { "id": 1, "name": "None" }, { "id": 2, "name": "Visual" } ]
-
Request:
React Implementation
import { Form, Select, DatePicker } from "antd";
import { useEffect, useState } from "react";
import moment from "moment";
const PersonalDetailsSection = ({ formData, setFormData }) => {
const [countries, setCountries] = useState([]);
const [impairments, setImpairments] = useState([]);
const maritalOptions = ["Single", "Married", "Other"];
useEffect(() => {
const fetchData = async () => {
setCountries([
{ id: 1, name: "Tanzania" },
{ id: 2, name: "Kenya" },
]);
setImpairments([
{ id: 1, name: "None" },
{ id: 2, name: "Visual" },
]);
};
fetchData();
}, []);
return (
<div>
<h5>Personal Details</h5>
<Form.Item label="Date of Birth" required>
<DatePicker
value={formData.dob ? moment(formData.dob) : null}
onChange={(date) =>
setFormData({ ...formData, dob: date ? date.format("YYYY-MM-DD") : "" })
}
format="YYYY-MM-DD"
disabledDate={(current) =>
current && (current < moment("1970-01-01") || current > moment().subtract(10, "years"))
}
/>
</Form.Item>
<Form.Item label="Nationality" required>
<Select
value={formData.country_id_nationality}
onChange={(value) => setFormData({ ...formData, country_id_nationality: value })}
placeholder="Select Nationality"
>
{countries.map((country) => (
<Select.Option key={country.id} value={country.id}>
{country.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Country of Residence" required>
<Select
value={formData.country_id_residence}
onChange={(value) => setFormData({ ...formData, country_id_residence: value })}
placeholder="Select Country of Residence"
>
{countries.map((country) => (
<Select.Option key={country.id} value={country.id}>
{country.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Marital Status" required>
<Select
value={formData.marital_status}
onChange={(value) => setFormData({ ...formData, marital_status: value })}
placeholder="Select Marital Status"
>
{maritalOptions.map((status) => (
<Select.Option key={status} value={status}>
{status}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Impairment" required>
<Select
value={formData.impairment_id}
onChange={(value) => setFormData({ ...formData, impairment_id: value })}
placeholder="Select Impairment"
>
{impairments.map((impairment) => (
<Select.Option key={impairment.id} value={impairment.id}>
{impairment.name}
</Select.Option>
))}
</Select>
</Form.Item>
</div>
);
};
5. Contact Details Section
Description
This section collects the applicant’s contact information and is shown only after NECTA data is fetched.
Inputs
-
Email (
email
):- Type: Text input (
<Input>
from Antd) - Validation: Remove spaces on input
- Required: Yes
- Type: Text input (
-
Phone (
phone
):- Type: Text input (
<Input>
from Antd) - Required: Yes
- Type: Text input (
Steps
- Allow the user to input their email and phone number.
- Remove spaces from the email input in real-time.
React Implementation
import { Form, Input } from "antd";
const ContactDetailsSection = ({ formData, setFormData }) => {
return (
<div>
<h5>Contact Details</h5>
<Form.Item label="Email" required>
<Input
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value.replace(/\s/g, "") })
}
/>
</Form.Item>
<Form.Item label="Phone" required>
<Input
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
</Form.Item>
</div>
);
};
6. Password Section
Description
This section collects the applicant’s password and is shown only after NECTA data is fetched.
Inputs
-
Password (
password
):- Type: Password input (
<Input.Password>
from Antd) - Required: Yes
- Type: Password input (
-
Re-enter Password (
re_password
):- Type: Password input (
<Input.Password>
from Antd) - Required: Yes
- Validation: Must match
password
- Type: Password input (
Steps
- Allow the user to input and confirm their password.
- Validate that the passwords match on the form submission.
React Implementation
import { Form, Input } from "antd";
const PasswordSection = ({ formData, setFormData }) => {
return (
<div>
<h5>Password</h5>
<Form.Item label="Password" required>
<Input.Password
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
</Form.Item>
<Form.Item label="Re-enter Password" required>
<Input.Password
value={formData.re_password}
onChange={(e) => setFormData({ ...formData, re_password: e.target.value })}
/>
</Form.Item>
</div>
);
};
Form Submission
Description
The form ends with "Register" and "Cancel" buttons. Submitting the form sends all collected data to the backend.
Steps
- Validate the form (e.g., required fields, password match).
- Send the form data to the backend via a POST request.
- Display success or error messages based on the response.
When to Fetch Data from Backend
-
Registration: On form submission.
-
Request:
POST /api/register
- Request Body:
{ "programme_level_id": 1, "academic_year": "2023/2024", "intake_description": "September Intake", "csee_country_id": 1, "index_number": "S1234/5678", "exam_year": "2020", "primary_school": "Sample Primary", "primary_school_region": 1, "primary_school_district": 1, "first_name": "John", "middle_name": "Middle", "surname": "Doe", "dob": "2000-01-01", "country_id_nationality": 1, "country_id_residence": 1, "marital_status": "Single", "impairment_id": 1, "email": "user@example.com", "phone": "1234567890", "password": "password123", "re_password": "password123" }
- Sample Success Response:
{ "status": "success", "message": "Registration successful" }
- Sample Error Response:
{ "status": "error", "message": "Validation failed", "errors": { "email": ["Email is already taken"], "password": ["Password must be at least 8 characters"] } }
-
Request:
React Implementation (Parent Component)
import { Form, Button, message } from "antd";
import { useState } from "react";
import ApplicationCategorySection from "./ApplicationCategorySection";
import SecondarySchoolSection from "./SecondarySchoolSection";
import PrimarySchoolSection from "./PrimarySchoolSection";
import PersonalDetailsSection from "./PersonalDetailsSection";
import ContactDetailsSection from "./ContactDetailsSection";
import PasswordSection from "./PasswordSection";
const RegistrationForm = () => {
const [formData, setFormData] = useState({
programme_level_id: "",
academic_year: "",
intake_description: "",
csee_country_id: "",
index_number: "",
exam_year: "",
primary_school: "",
primary_school_region: "",
primary_school_district: "",
first_name: "",
middle_name: "",
surname: "",
dob: "",
country_id_nationality: "",
country_id_residence: "",
marital_status: "",
impairment_id: "",
email: "",
phone: "",
password: "",
re_password: "",
});
const [nectaData, setNectaData] = useState(null);
const [showProfile, setShowProfile] = useState(false);
const handleSubmit = async () => {
if (formData.password !== formData.re_password) {
message.error("Passwords do not match");
return;
}
// Simulate API call
const response = { status: "success", message: "Registration successful" };
if (response.status === "success") {
message.success(response.message);
} else {
message.error(response.message);
}
};
return (
<Form onFinish={handleSubmit}>
<ApplicationCategorySection formData={formData} setFormData={setFormData} />
{formData.programme_level_id && (
<SecondarySchoolSection
formData={formData}
setFormData={setFormData}
setNectaData={setNectaData}
setShowProfile={setShowProfile}
/>
)}
{showProfile && (
<>
<PrimarySchoolSection formData={formData} setFormData={setFormData} />
<PersonalDetailsSection formData={formData} setFormData={setFormData} />
<ContactDetailsSection formData={formData} setFormData={setFormData} />
<PasswordSection formData={formData} setFormData={setFormData} />
<Form.Item>
<Button type="primary" htmlType="submit">
Register
</Button>
<Button style={{ marginLeft: 8 }} danger onClick={() => window.location.href = "/"}>
Cancel
</Button>
</Form.Item>
</>
)}
</Form>
);
};
export default RegistrationForm;
Summary for the Developer
-
Structure: Use a parent
RegistrationForm
component with child components for each section. - Initial Data: Fetch application categories, countries, regions, and impairments on mount.
- Dynamic Fetching: Fetch NECTA data on Secondary School input changes and districts on region selection.
- Conditional Rendering: Show Secondary School if categories exist, and profile sections after NECTA data is fetched.
-
UI: Use Ant Design components (
Form
,Select
,Input
,DatePicker
,Button
,Spin
) for consistency. - Validation: Implement client-side checks (e.g., required fields, password match).
-
Submission: Send all form data to
/api/register
and handle responses.
This document outlines the UI development process using mock JSON data. Backend implementation details are outside the scope of this guide; however, you may use any suitable backend technology such as Laravel, Node.js, or Python. The choice of backend technology depends on project requirements.
Top comments (0)