diff --git a/.gitignore b/.gitignore index db5330e1..a1274b2b 100755 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ dist buildcoverage package-lock.json .DS_Store -build/ \ No newline at end of file +build/ \ No newline at end of file diff --git a/src/components/Education.tsx b/src/components/Education.tsx new file mode 100644 index 00000000..b424e28c --- /dev/null +++ b/src/components/Education.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import InputField from './form/InputField'; + +interface FormData { + gender: string; + birth_date: string; + Address: string; + phone: string; + field_of_study: string; + education_level: string; + province: string; + district: string; + sector: string; + isEmployed: string; + haveLaptop: string; + isStudent: string; + past_andela_programs: string; + understandTraining: string; + } + +interface EducationSectionProps { + formData: FormData; + handleInputChange: (e: React.ChangeEvent) => void; + isDarkMode: boolean; +} + +const EducationSection: React.FC = ({ formData, handleInputChange, isDarkMode }) => { + const inputClassName = `w-52 md:w-2/3 rounded-md px-2 py-2 border ${ + isDarkMode + ? 'border-white placeholder:text-gray-400 text-white bg-[#1F2A37]' + : 'border-gray-300 placeholder:text-gray-500 text-gray-900 bg-white' + } sm:text-[12px] outline-none`; + return ( + <> +
+
+ + +
+
+ + +
+
+ + ); +}; + +export default EducationSection; \ No newline at end of file diff --git a/src/components/Location.tsx b/src/components/Location.tsx new file mode 100644 index 00000000..0fd5c5c6 --- /dev/null +++ b/src/components/Location.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import InputField from './form/InputField'; + +interface FormData { + gender: string; + birth_date: string; + Address: string; + phone: string; + field_of_study: string; + education_level: string; + province: string; + district: string; + sector: string; + isEmployed: string; + haveLaptop: string; + isStudent: string; + past_andela_programs: string; + understandTraining: string; +} + +interface LocationSectionProps { + formData: FormData; + handleInputChange: (e: React.ChangeEvent) => void; + isDarkMode: boolean; +} + +const LocationSection: React.FC = ({ + formData, + handleInputChange, + isDarkMode +}) => { + const inputClassName = `w-52 md:w-2/3 rounded-md px-2 py-2 border ${ + isDarkMode + ? 'border-white placeholder:text-gray-400 text-white bg-[#1F2A37]' + : 'border-gray-300 placeholder:text-gray-500 text-gray-900 bg-white' + } sm:text-[12px] outline-none`; + + return ( + <> + + + + + + + + +
+ +
+ Male +
+
+ Female +
+
+ + ); +}; + +export default LocationSection; \ No newline at end of file diff --git a/src/components/Personal.tsx b/src/components/Personal.tsx new file mode 100644 index 00000000..053b4ce5 --- /dev/null +++ b/src/components/Personal.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import InputField from './form/InputField'; + +interface FormData { + gender: string; + birth_date: string; + Address: string; + phone: string; + field_of_study: string; + education_level: string; + province: string; + district: string; + sector: string; + isEmployed: string; + haveLaptop: string; + isStudent: string; + past_andela_programs: string; + understandTraining: string; +} + +interface PersonalInfoSectionProps { + formData: FormData; + handleInputChange: (e: React.ChangeEvent) => void; + isDarkMode: boolean; + } + + const PersonalInfoSection: React.FC = ({ formData, handleInputChange, isDarkMode }) => { + const inputClassName = `w-52 md:w-2/3 rounded-md px-2 py-2 border ${ + isDarkMode + ? 'border-white placeholder:text-gray-400 text-white bg-[#1F2A37]' + : 'border-gray-300 placeholder:text-gray-500 text-gray-900 bg-white' + } sm:text-[12px] outline-none`; + + return ( + <> + + + + +
+ +
+ Yes +
+
+ No +
+
+ + ); + }; + + export default PersonalInfoSection; \ No newline at end of file diff --git a/src/components/Tables/DynamicTable.tsx b/src/components/Tables/DynamicTable.tsx index 514eb60d..1a53b6b3 100644 --- a/src/components/Tables/DynamicTable.tsx +++ b/src/components/Tables/DynamicTable.tsx @@ -1,5 +1,5 @@ import React from "react"; - +import { Link } from 'react-router-dom'; interface FieldData { key: string; value: string | null; diff --git a/src/components/TraineeApply/TraineeApply.tsx b/src/components/TraineeApply/TraineeApply.tsx new file mode 100644 index 00000000..33eb2681 --- /dev/null +++ b/src/components/TraineeApply/TraineeApply.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import Button from '../form/Button'; +import InputField from '../form/InputField'; +import { useTraineeFormLogic } from '../../hooks/useTraineeFormLogic'; +import { useTheme } from '../../hooks/darkmode'; + +interface Cycle { + id: string; + name: string; +} + +const CycleSelector: React.FC<{ + value: string, + onChange: (e: React.ChangeEvent) => void, + cycles: Cycle[], + cyclesLoading: boolean, + error?: string, + isDarkMode: boolean +}> = ({ value, onChange, cycles, cyclesLoading, error, isDarkMode }) => ( + <> + + {error &&

{error}

} + +); + +const TraineeApplicationForm: React.FC = () => { + const { + formData, + errors, + isSubmitting, + submitError, + cycles, + cyclesLoading, + handleInputChange, + handleSubmit, + isLoading + } = useTraineeFormLogic(); + + const { theme } = useTheme(); + const isDarkMode = theme === false; + + if (isLoading) { + return
Loading user data...
; + } + + return ( +
+
+

Trainee Application

+
+ {renderFormFields(formData, errors, handleInputChange, isDarkMode)} + {renderCycleSelector(formData, errors, handleInputChange, cycles, cyclesLoading, isDarkMode)} + {renderSubmitButton(isSubmitting, isDarkMode)} +
+
+
+ ); +}; + +const renderFormFields = (formData, errors, handleInputChange, isDarkMode) => + ['firstName', 'lastName', 'email'].map((field) => ( + + )); + +const renderCycleSelector = (formData, errors, handleInputChange, cycles, cyclesLoading, isDarkMode) => ( + <> + + {errors.cycle_id &&

{errors.cycle_id}

} + +); + +const renderSubmitButton = (isSubmitting, isDarkMode) => ( + + + ); +}; + +export default TraineeFormPage1; \ No newline at end of file diff --git a/src/components/TraineeFormPage2.tsx b/src/components/TraineeFormPage2.tsx new file mode 100644 index 00000000..49d4b045 --- /dev/null +++ b/src/components/TraineeFormPage2.tsx @@ -0,0 +1,170 @@ +import InputField from './form/InputField'; + +interface ComponentProps { + formData: any; + handleInputChange?: (e: React.ChangeEvent) => void; + inputClassName?: string; + isDarkMode: boolean; +} + +const EmploymentSection: React.FC = ({ formData, handleInputChange, isDarkMode }) => { + + const inputClassName = `w-52 md:w-2/3 rounded-md px-2 py-2 border ${ + isDarkMode + ? 'border-white placeholder:text-gray-400 text-white bg-[#1F2A37]' + : 'border-gray-300 placeholder:text-gray-500 text-gray-900 bg-white' + } sm:text-[12px] outline-none`; + + return ( +
+ +
+ Yes +
+
+ No +
+ +
+ + + +
+ +
+ + +)}; + +const ProgramInfoSection: React.FC = ({ formData, handleInputChange,inputClassName, isDarkMode }) => { + + return ( + <> +
+ +
+ Yes +
+
+ No +
+
+ +
+ +
+ Yes +
+
+ No +
+
+ + ); +}; + +const TraineeFormPage2 = ({ formData, setFormData, onSubmit, onBack, isDarkMode }) => { + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target; + setFormData(prevData => ({ + ...prevData, + [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value + })); + }; + + const inputClassName = `w-52 md:w-2/3 rounded-md px-2 py-2 border ${ + isDarkMode + ? 'border-white placeholder:text-gray-400 text-white bg-[#1F2A37]' + : 'border-gray-300 placeholder:text-gray-500 text-gray-900 bg-white' + } sm:text-[12px] outline-none`; + + return ( +
+
+ +
+
+ + +
+
+ + +
+
+ ); +}; + +export default TraineeFormPage2; \ No newline at end of file diff --git a/src/components/TraineeSuccess.tsx b/src/components/TraineeSuccess.tsx new file mode 100644 index 00000000..bd6eea0f --- /dev/null +++ b/src/components/TraineeSuccess.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import Button from '../components/form/Button'; +import { useTheme } from '../hooks/darkmode'; + +interface TraineeSuccessProps { + onContinue: () => void; +} + +const TraineeSuccess: React.FC = ({ onContinue }) => { + const { theme } = useTheme(); + + return ( +
+
+

+ Congratulations +

+

+ Thank you for applying as a trainee! We would like you to provide more information about you. +

+ +
+
+ ); +}; + +export default TraineeSuccess; \ No newline at end of file diff --git a/src/components/form/InputField.tsx b/src/components/form/InputField.tsx index 4bb56fde..322df38b 100644 --- a/src/components/form/InputField.tsx +++ b/src/components/form/InputField.tsx @@ -4,7 +4,7 @@ interface InputFieldProps extends InputHTMLAttributes { parentClassName?: string; styles?: string; label?: string; - error: any; + error?: any; } const InputField = forwardRef( diff --git a/src/components/useFormValidation.tsx b/src/components/useFormValidation.tsx new file mode 100644 index 00000000..2b102489 --- /dev/null +++ b/src/components/useFormValidation.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; + +interface FormData { + firstName: string; + lastName: string; + email: string; + cycle_id: string; +} + +interface FormErrors { + firstName?: string; + lastName?: string; + email?: string; + cycle_id?: string; +} + +type ValidationRule = (value: string) => string | undefined; + +const required = (fieldName: string): ValidationRule => + (value) => value.trim() ? undefined : `${fieldName} is required`; + +const validEmail: ValidationRule = (value) => + /\S+@\S+\.\S+/.test(value) ? undefined : 'Email is invalid'; + +const validationRules: Record = { + firstName: [required('First name')], + lastName: [required('Last name')], + email: [required('Email'), validEmail], + cycle_id: [required('Cycle ID')] +}; + +const validateField = (field: keyof FormData, value: string): string | undefined => { + const fieldRules = validationRules[field]; + for (const rule of fieldRules) { + const error = rule(value); + if (error) return error; + } + return undefined; +}; + +const useFormValidation = (formData: FormData) => { + const [errors, setErrors] = useState({}); + + const validateForm = (): boolean => { + const newErrors: FormErrors = Object.keys(formData).reduce((acc, field) => { + const error = validateField(field as keyof FormData, formData[field as keyof FormData]); + return error ? { ...acc, [field]: error } : acc; + }, {}); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + return { errors, validateForm }; +}; + +export default useFormValidation; \ No newline at end of file diff --git a/src/hooks/useTraineeFormLogic.ts b/src/hooks/useTraineeFormLogic.ts new file mode 100644 index 00000000..2cccc2ef --- /dev/null +++ b/src/hooks/useTraineeFormLogic.ts @@ -0,0 +1,112 @@ +import { useState, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { createTrainee } from '../redux/actions/TraineeAction'; +import { getAllCycles } from '../redux/actions/cyclesActions'; +import { loggedUserAction } from '../redux/actions/getLoggedUser'; +import { AppDispatch, RootState } from '../redux/store'; +import useFormValidation from '../components/useFormValidation'; + +interface FormData { + firstName: string; + lastName: string; + email: string; + cycle_id: string; +} + +const initialFormData: FormData = { + firstName: '', + lastName: '', + email: '', + cycle_id: '' +}; + +export const useTraineeFormLogic = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [formData, setFormData] = useState(initialFormData); + const [submitError, setSubmitError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { cycles, cyclesLoading, loggedUser, loggedUserLoading } = useSelector((state: RootState) => ({ + cycles: state.cycles.data, + cyclesLoading: state.cycles.isLoading, + loggedUser: state.loggedUser.user, + loggedUserLoading: state.loggedUser.loading, + })); + + const { errors, validateForm } = useFormValidation(formData); + const isMounted = useRef(true); + + useEffect(() => { + dispatch(getAllCycles()); + dispatch(loggedUserAction()); + return () => { isMounted.current = false; }; + }, [dispatch]); + + useEffect(() => { + if (loggedUser && isMounted.current) { + setFormData(prevData => ({ + ...prevData, + firstName: loggedUser.firstName || '', + lastName: loggedUser.lastName || '', + email: loggedUser.email || '', + })); + } + }, [loggedUser]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prevData => ({ ...prevData, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) { + return; + } + + setSubmitError(null); + setIsSubmitting(true); + + try { + const result = await dispatch(createTrainee(formData)); + + if (result?.errors) { + setSubmitError(result.errors[0].message); + return; + } + + if (result?.data?.createNewTraineeApplicant) { + const traineeId = result.data.createNewTraineeApplicant._id; + navigate(`trainee-success/${traineeId}`, { replace: true }); + } + } catch (error: any) { + console.error('Error submitting form:', error); + setSubmitError(getErrorMessage(error)); + } finally { + setIsSubmitting(false); + } + }; + + return { + formData, + errors, + isSubmitting, + submitError, + cycles, + cyclesLoading, + handleInputChange, + handleSubmit, + isLoading: loggedUserLoading, + }; +}; + +function getErrorMessage(error: any): string { + if (error.response?.data?.errors?.[0]?.message) { + return error.response.data.errors[0].message; + } else if (error.message) { + return error.message; + } + return "An error occurred while creating the trainee."; +} \ No newline at end of file diff --git a/src/pages/JobPost/applicantJobFiltering.tsx b/src/pages/JobPost/applicantJobFiltering.tsx index f68b5c89..b10c0e44 100644 --- a/src/pages/JobPost/applicantJobFiltering.tsx +++ b/src/pages/JobPost/applicantJobFiltering.tsx @@ -143,6 +143,11 @@ const ApplicantSeachJobPost = (props: any) => {
+ + +