diff --git a/.eslintrc.json b/.eslintrc.json index 47816e2..489049b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -68,6 +68,12 @@ "settings": { "react": { "version": "detect" + }, + "import/resolver": { + "typescript": { + "alwaysTryTypes": true, + "project": "./tsconfig.json" + } } } } diff --git a/package.json b/package.json index e649e4f..b821787 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@hookform/resolvers": "^5.2.2", "@mui/icons-material": "^5.15.18", "@mui/material": "^5.15.18", "@testing-library/dom": "^10.1.0", @@ -29,7 +30,9 @@ "next": "14.2.3", "react": "18.2.0", "react-dom": "18.2.0", - "ts-jest": "^29.1.2" + "react-hook-form": "^7.66.0", + "ts-jest": "^29.1.2", + "zod": "^4.1.12" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c94e8b2..fa4866a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@emotion/styled': specifier: ^11.11.5 version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.2.0))(@types/react@18.3.26)(react@18.2.0) + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.70.0(react@18.2.0)) '@mui/icons-material': specifier: ^5.15.18 version: 5.18.0(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.2.0))(@types/react@18.3.26)(react@18.2.0))(@types/react@18.3.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.26)(react@18.2.0) @@ -44,9 +47,15 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.66.0 + version: 7.70.0(react@18.2.0) ts-jest: specifier: ^29.1.2 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.24)(typescript@5.9.3)))(typescript@5.9.3) + zod: + specifier: ^4.1.12 + version: 4.3.5 devDependencies: '@playwright/test': specifier: ^1.57.0 @@ -845,6 +854,11 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1153,6 +1167,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -3273,6 +3290,12 @@ packages: peerDependencies: react: ^18.2.0 + react-hook-form@7.70.0: + resolution: {integrity: sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3887,6 +3910,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -4821,6 +4847,11 @@ snapshots: '@eslint/js@8.57.1': {} + '@hookform/resolvers@5.2.2(react-hook-form@7.70.0(react@18.2.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.70.0(react@18.2.0) + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -5201,6 +5232,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@standard-schema/utils@0.3.0': {} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -7735,6 +7768,10 @@ snapshots: react: 18.2.0 scheduler: 0.23.2 + react-hook-form@7.70.0(react@18.2.0): + dependencies: + react: 18.2.0 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -8413,3 +8450,5 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zod@4.3.5: {} diff --git a/public/mentee-form-bg.png b/public/mentee-form-bg.png new file mode 100644 index 0000000..3ca3451 Binary files /dev/null and b/public/mentee-form-bg.png differ diff --git a/src/components/forms/CheckboxGroup.tsx b/src/components/forms/CheckboxGroup.tsx new file mode 100644 index 0000000..1a7b4d8 --- /dev/null +++ b/src/components/forms/CheckboxGroup.tsx @@ -0,0 +1,105 @@ +import { + FormControl, + FormLabel, + FormGroup, + FormControlLabel, + Checkbox, + FormHelperText, + TextField as MuiTextField, +} from '@mui/material'; +import { Controller, Control, FieldPath, FieldValues } from 'react-hook-form'; + +interface CheckboxGroupProps { + name: FieldPath; + control: Control; + label: string; + options: Array<{ value: string; label: string }>; + showOtherOption?: boolean; + otherFieldName?: FieldPath; +} + +function CheckboxGroup({ + name, + control, + label, + options, + showOtherOption = false, + otherFieldName, +}: CheckboxGroupProps) { + return ( + { + const selectedValues = (value as string[]) || []; + + const handleChange = (optionValue: string, checked: boolean) => { + if (checked) { + onChange([...selectedValues, optionValue]); + } else { + onChange(selectedValues.filter((v) => v !== optionValue)); + } + }; + + return ( + + {label} + + {options.map((option) => ( + + handleChange(option.value, e.target.checked) + } + /> + } + label={option.label} + /> + ))} + {showOtherOption && otherFieldName && ( + handleChange('other', e.target.checked)} + /> + } + label={ +
+ Other: + ( + + )} + /> +
+ } + /> + )} +
+ {error && {error.message}} +
+ ); + }} + /> + ); +} + +export default CheckboxGroup; diff --git a/src/components/forms/MenteeFormLayout.tsx b/src/components/forms/MenteeFormLayout.tsx new file mode 100644 index 0000000..1fcd26f --- /dev/null +++ b/src/components/forms/MenteeFormLayout.tsx @@ -0,0 +1,95 @@ +import { Box, Container, Paper, Typography } from '@mui/material'; +import React, { ReactNode } from 'react'; + +interface MenteeFormLayoutProps { + title: string; + description?: ReactNode; + children: ReactNode; +} + +const MenteeFormLayout: React.FC = ({ + title, + description, + children, +}) => { + return ( + + + + + + {title} + + + + * + + + Indicates a required field + + + {description && {description}} + + {children} + + + + ); +}; + +export default MenteeFormLayout; diff --git a/src/components/forms/RadioGroup.tsx b/src/components/forms/RadioGroup.tsx new file mode 100644 index 0000000..5cf65a9 --- /dev/null +++ b/src/components/forms/RadioGroup.tsx @@ -0,0 +1,48 @@ +import { + FormControl, + FormLabel, + RadioGroup as MuiRadioGroup, + FormControlLabel, + Radio, + FormHelperText, +} from '@mui/material'; +import { Controller, Control, FieldPath, FieldValues } from 'react-hook-form'; + +interface RadioGroupProps { + name: FieldPath; + control: Control; + label: string; + options: Array<{ value: string; label: string }>; +} + +function RadioGroup({ + name, + control, + label, + options, +}: RadioGroupProps) { + return ( + ( + + {label} + + {options.map((option) => ( + } + label={option.label} + /> + ))} + + {error && {error.message}} + + )} + /> + ); +} + +export default RadioGroup; diff --git a/src/components/forms/Select.tsx b/src/components/forms/Select.tsx new file mode 100644 index 0000000..7762d8e --- /dev/null +++ b/src/components/forms/Select.tsx @@ -0,0 +1,67 @@ +import { + FormControl, + InputLabel, + Select as MuiSelect, + MenuItem, + FormHelperText, +} from '@mui/material'; +import type { SelectProps as MuiSelectProps } from '@mui/material'; +import { Controller, Control, FieldPath, FieldValues } from 'react-hook-form'; + +interface SelectProps + extends Omit { + name: FieldPath; + control: Control; + label: string; + options: Array<{ value: string; label: string }>; +} + +function Select({ + name, + control, + label, + options, + ...other +}: SelectProps) { + return ( + ( + + {label} + + + Select {label} + + {options.map((option) => ( + + {option.label} + + ))} + + {error && {error.message}} + + )} + /> + ); +} + +export default Select; diff --git a/src/components/forms/TextArea.tsx b/src/components/forms/TextArea.tsx new file mode 100644 index 0000000..bd101c1 --- /dev/null +++ b/src/components/forms/TextArea.tsx @@ -0,0 +1,52 @@ +import { TextField as MuiTextField } from '@mui/material'; +import type { TextFieldProps as MuiTextFieldProps } from '@mui/material'; +import { Controller, Control, FieldPath, FieldValues } from 'react-hook-form'; + +interface TextAreaProps + extends Omit { + name: FieldPath; + control: Control; + rows?: number; +} + +function TextArea({ + name, + control, + rows = 4, + ...other +}: TextAreaProps) { + return ( + ( + + )} + /> + ); +} + +export default TextArea; diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx new file mode 100644 index 0000000..70e55b7 --- /dev/null +++ b/src/components/forms/TextField.tsx @@ -0,0 +1,48 @@ +import { TextField as MuiTextField } from '@mui/material'; +import type { TextFieldProps as MuiTextFieldProps } from '@mui/material'; +import { Controller, Control, FieldPath, FieldValues } from 'react-hook-form'; + +interface TextFieldProps + extends Omit { + name: FieldPath; + control: Control; +} + +function TextField({ + name, + control, + ...other +}: TextFieldProps) { + return ( + ( + + )} + /> + ); +} + +export default TextField; diff --git a/src/components/forms/index.tsx b/src/components/forms/index.tsx new file mode 100644 index 0000000..0af9ce3 --- /dev/null +++ b/src/components/forms/index.tsx @@ -0,0 +1,6 @@ +export { default as CheckboxGroup } from './CheckboxGroup'; +export { default as MenteeFormLayout } from './MenteeFormLayout'; +export { default as RadioGroup } from './RadioGroup'; +export { default as Select } from './Select'; +export { default as TextArea } from './TextArea'; +export { default as TextField } from './TextField'; diff --git a/src/components/index.tsx b/src/components/index.tsx index 369ee4a..8e3aa1d 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -23,6 +23,14 @@ export { Title } from './Title'; export { VolunteerSection } from './VolunteerSection'; export { CodeOfConductSection } from './CodeOfConduct'; export { TimelineCard } from './TimelineCard'; +export { + CheckboxGroup, + MenteeFormLayout, + RadioGroup, + Select, + TextArea, + TextField, +} from './forms'; export { ResourcesCard } from './ResourcesCard'; export { HeroWithImage } from './HeroWithImage'; export { InfoWithContact } from './InfoWithContact'; diff --git a/src/pages/mentorship/mentee-registration.tsx b/src/pages/mentorship/mentee-registration.tsx index cf4c0d2..113be09 100644 --- a/src/pages/mentorship/mentee-registration.tsx +++ b/src/pages/mentorship/mentee-registration.tsx @@ -1,14 +1,489 @@ -// path: /mentorship/mentee-registration +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Button, + Box, + Typography, + Breadcrumbs, + Link, + FormControl, + InputLabel, + Select as MuiSelect, + MenuItem, + FormHelperText, + TextField as MuiTextField, +} from '@mui/material'; +import NextLink from 'next/link'; +import { useForm, Controller } from 'react-hook-form'; -import { Typography } from '@mui/material'; +import { + CheckboxGroup, + MenteeFormLayout, + TextArea, + TextField, +} from '@components'; +import { + menteeFormDefaultValues, + menteeFormSchema, + MenteeFormData, +} from '@schemas/menteeSchema'; const MenteeRegistrationPage = () => { + const { + control, + handleSubmit, + formState: { isSubmitting }, + } = useForm({ + resolver: zodResolver(menteeFormSchema), + defaultValues: menteeFormDefaultValues, + }); + + const onSubmit = async (data: MenteeFormData) => { + const networkLinks = data.network || []; + if (data.linkedInProfile) { + networkLinks.push({ + type: 'LINKEDIN' as const, + link: data.linkedInProfile, + }); + } + + const payload = { + mentee: { + fullName: data.fullName, + position: data.position, + email: data.email, + slackDisplayName: data.slackDisplayName, + country: data.country || { countryCode: '', countryName: '' }, + city: data.city, + companyName: data.companyName || '', + images: [], + network: networkLinks, + profileStatus: 'ACTIVE', + skills: data.skills, + spokenLanguages: data.spokenLanguages, + bio: data.bio, + }, + mentorshipType: data.mentorshipType, + cycleYear: data.cycleYear, + applications: data.applications, + }; + // eslint-disable-next-line no-console + console.log('Form payload:', payload); + await new Promise((resolve) => setTimeout(resolve, 2000)); + }; + + const spokenLanguages = [ + { value: 'English', label: 'English' }, + { value: 'French', label: 'French' }, + { value: 'German', label: 'German' }, + { value: 'Spanish', label: 'Spanish' }, + { value: 'Italian', label: 'Italian' }, + { value: 'Dutch', label: 'Dutch' }, + { value: 'Hindi', label: 'Hindi' }, + { value: 'Portuguese', label: 'Portuguese' }, + ]; + + const skillAreas = [ + { value: 'FRONTEND', label: 'Frontend' }, + { value: 'BACKEND', label: 'Backend' }, + { value: 'DEVOPS', label: 'DevOps' }, + { value: 'FULLSTACK', label: 'Full Stack' }, + { value: 'DATA', label: 'Data' }, + { value: 'MOBILE', label: 'Mobile' }, + { value: 'OTHER', label: 'Other' }, + ]; + + const programmingLanguages = [ + { value: 'JavaScript', label: 'JavaScript' }, + { value: 'TypeScript', label: 'TypeScript' }, + { value: 'Python', label: 'Python' }, + { value: 'Java', label: 'Java' }, + { value: 'C', label: 'C' }, + { value: 'C++', label: 'C++' }, + { value: 'C#', label: 'C#' }, + { value: 'Ruby', label: 'Ruby' }, + { value: 'Go', label: 'Go' }, + { value: 'Rust', label: 'Rust' }, + { value: 'PHP', label: 'PHP' }, + { value: 'Swift', label: 'Swift' }, + { value: 'Kotlin', label: 'Kotlin' }, + ]; + + const mentorshipFocusOptions = [ + { value: 'Switch career to IT', label: 'Switch career to IT' }, + { + value: 'Grow from beginner to mid-level', + label: 'Grow from beginner to mid-level', + }, + { + value: 'Grow from mid-level to senior-level', + label: 'Grow from mid-level to senior-level', + }, + { + value: 'Engineering management', + label: 'Engineering management', + }, + { + value: 'Technical leadership', + label: 'Technical leadership', + }, + { value: 'Career advancement', label: 'Career advancement' }, + ]; + + const currentYear = new Date().getFullYear(); + const cycleYears = Array.from({ length: 7 }, (_, i) => ({ + value: currentYear + i, + label: String(currentYear + i), + })); + return ( -
- - Welcome to the MenteeRegistrationPage - -
+ <> + + + + Home + + + Mentorship + + Mentee Registration + + + + + Thank you for your interest in our Mentoring Programme (Long-Term + mentoring). We appreciate your enthusiasm and look forward to + connecting with the mentor of your choice. + + + + + ‼️Important Information, Please read before submitting your + application‼️ + + + + + We want to emphasise that applying for a long-term mentorship + carries a responsibility to maintain{' '} + timely communication and{' '} + respectful engagement with your chosen mentor and + the mentorship coordinating team throughout the entire programme. + + + + By submitting your application, you are committing to abide + strictly by the community's{' '} + + Mentee code of conduct + + . Repeated violation will result in a ban for future applications. + + + + Thank you for your cooperation and we look forward to processing + your application! + + + } + > +
+ + + Basic Information + + + + + + + + + + + + + + Please note your application will be rejected if you are not in + our Slack community.{' '} + + Click here to join us on Slack + + . + + + + + + + + + + + + + + + + + + + + + + + Skills & Experience + + + + ( + + field.onChange(parseInt(e.target.value) || 0) + } + sx={{ + backgroundColor: 'rgba(223, 227, 231, 1)', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(223, 227, 231, 1)', + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(223, 227, 231, 1)', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(223, 227, 231, 1)', + }, + }} + /> + )} + /> + + + + + + + + + + + + + + + + + + Languages + + + + + + + + + + Bio + + + +