diff --git a/.husky/pre-commit b/.husky/pre-commit index 9d8f28a..98475b5 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh - . "$(dirname "$0")/_/husky.sh" - -pnpm lint:fix && pnpm format && pnpm type-check +pnpm test diff --git a/package.json b/package.json index e649e4f..8831fa9 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.70.0", + "ts-jest": "^29.1.2", + "zod": "^4.1.13" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/playwright-tests/tests/home.page.spec.ts b/playwright-tests/tests/home.page.spec.ts index 462316e..6144c1a 100644 --- a/playwright-tests/tests/home.page.spec.ts +++ b/playwright-tests/tests/home.page.spec.ts @@ -53,7 +53,7 @@ test.describe('Validate Home Page', () => { await basePage.verifyURL('/mentorship/mentor-registration'); await basePage.verifyPageContainsText( - 'Welcome to the MentorRegistrationPage', + 'WCC: Registration Form for Mentors', ); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c94e8b2..d7926c3 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.70.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.13 + 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/mentor-hero-bg.png b/public/mentor-hero-bg.png new file mode 100644 index 0000000..8472353 Binary files /dev/null and b/public/mentor-hero-bg.png differ diff --git a/src/components/mentorship/MentorshipSelect.tsx b/src/components/mentorship/MentorshipSelect.tsx new file mode 100644 index 0000000..93e2d5a --- /dev/null +++ b/src/components/mentorship/MentorshipSelect.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { TextField, MenuItem } from '@mui/material'; +import { inputStyle } from './mentorshipStyles'; + +interface MentorshipSelectProps { + name: string; + label?: string; + options: string[]; +} + +export const MentorshipSelect = ({ name, label, options }: MentorshipSelectProps) => { + const { control } = useFormContext(); + + return ( + ( + { + if (!selected) return "Not Applicable"; + return selected; + } + }} + > + {options.map((option) => ( + + {option} + + ))} + + )} + /> + ); +}; diff --git a/src/components/mentorship/Step1BasicInfo.tsx b/src/components/mentorship/Step1BasicInfo.tsx new file mode 100644 index 0000000..f43a70a --- /dev/null +++ b/src/components/mentorship/Step1BasicInfo.tsx @@ -0,0 +1,242 @@ +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { + Grid, TextField, MenuItem, Typography, Box, FormHelperText, + FormGroup, FormControlLabel, Checkbox, Radio, RadioGroup, FormControl, FormLabel, + Table, TableBody, TableCell, TableHead, TableRow +} from '@mui/material'; +import StepSection from './StepSection'; +import { COUNTRIES } from '../../utils/mentorshipConstants'; + +const Step1BasicInfo = () => { + const { register, watch, control, formState: { errors } } = useFormContext(); + + const isLongTerm = watch("isLongTermMentor"); + const isAdHoc = watch("isAdHocMentor"); + + const months = ["May", "June", "July", "August", "September", "October", "November"]; + + return ( + + + * Indicates a required field + + + + + What is your full name? * + + + + What is your email address? * + + + + Slack Name * + + {!errors.slackName && ( + Please note your application will be rejected if you are not in our Slack community. + )} + + + Location * + + + + + Select a country + + {COUNTRIES.map((country) => ( + + {country.name} + + ))} + + + + + + + + + What is your current job title? * + + + + Company name * + + + + + + Which kind of mentor you want to be? * + + + + } label="Long-Term Format" /> + + {isLongTerm && ( + + + Maximum number of mentees you are available to support. * + + ( + + {[1, 2, 3, 4, 5, 6].map((num) => ( + + {num === 6 ? '6+' : num} + + ))} + + )} + /> + + )} + + } label="Ad-Hoc Format" /> + + {isAdHoc && ( + + + For each month below, please indicate the maximum number of mentees you are available to support. * + + + + + + + + {[1, 2, 3, 4, '5+'].map(num => ( + {num} + ))} + + + + {months.map((month) => ( + + {month} + + + {[1, 2, 3, 4, 5].map((val) => ( + + ))} + + + + ))} + +
+
+ + + {months.map((month) => ( + + + {month} + + + {[1, 2, 3, 4, 5].map((val) => ( + } + label={val === 5 ? '5+' : String(val)} + sx={{ + flex: 1, + mx: 0, + '& .MuiFormControlLabel-label': { + fontSize: '14px' + } + }} + /> + ))} + + + ))} + + {errors.adHocAvailability && ( + + Please select availability for at least one month + + )} +
+ + )} +
+ + {errors.isLongTermMentor && (Please select at least one mentorship format.)} +
+ + + Calendly schedule link * + + + + + What kind of Mentee are you looking for? * + + + + + + + Are you open to mentoring individuals who do not identify as women? * + + + ( + field.onChange(e.target.value)} + > + } label="Yes" /> + } label="No" /> + + )} + /> + + + {errors.openToNonWomen?.message as string || ''} + + + + +
+
+ ); +}; + +export default Step1BasicInfo; diff --git a/src/components/mentorship/Step2Skills.tsx b/src/components/mentorship/Step2Skills.tsx new file mode 100644 index 0000000..46728c3 --- /dev/null +++ b/src/components/mentorship/Step2Skills.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import { useFormContext, Controller } from 'react-hook-form'; +import { + Grid, TextField, MenuItem, Typography, Box, + RadioGroup, FormControlLabel, Radio, + Select, Checkbox, ListItemText, OutlinedInput +} from '@mui/material'; +import StepSection from './StepSection'; + +const inputStyle = { + '& .MuiOutlinedInput-root': { + backgroundColor: '#F5F5F5', + borderRadius: '4px', + '& fieldset': { border: 'none' }, + '&:hover fieldset': { border: 'none' }, + '&.Mui-focused fieldset': { border: '1px solid #333' }, + }, + '& .MuiInputBase-input': { padding: '12px 14px' } +}; + +const boldLabelStyle = { + fontWeight: 700, color: '#1B1919', mb: 0.5, display: 'block', fontSize: '16px', fontFamily: 'Roboto' +}; + +const helperTextStyle = { + fontSize: '12px', color: '#666', mt: 1, lineHeight: 1.5 +}; + +const LANGUAGES = ['English', 'Spanish', 'Russian', 'Polish', 'Ukrainian', 'French', 'Portuguese', 'Other']; +const EXPERIENCE = ['0–2 years', '3–5 years', '6–10 years', '10+ years']; + +const Step2Skills = () => { + const { register, watch, control, formState: { errors } } = useFormContext(); + const photoSource = watch('photoSource'); + + return ( + + + + Which languages do you speak? * + ( + + )} + /> + {errors.languages && ( + {errors.languages.message as string} + )} + + + + How many years of experience do you have in tech? * + + {EXPERIENCE.map((exp) => ( + {exp} + ))} + + + + + + Please share a brief summary of your professional background and expertise. * + + + + You may include: + +
    +
  • Areas of expertise and specializations
  • +
  • Years of experience
  • +
  • Notable achievements or certifications
  • +
  • Successful projects or career highlights
  • +
+
+ + + This information will be displayed in your mentor profile on the programme website and serve as your personal introduction to mentees + +
+ + + + List potential mentoring topics you can discuss with your mentee + + + Software Development Strategies, Resume Review, Preparation for The Technical Interview etc. + + + + This information will be displayed in your mentor profile on the programme website. + + + + + + Which photo should we use to show your profile as a mentor on our website? * + + + ( + + } label="Linked In" /> + } label="Slack" /> + } label="Image link from a public profile" /> + + )} + /> + {errors.photoSource && ( + {errors.photoSource.message as string} + )} + + {photoSource === 'other' && ( + + )} + +
+
+ ); +}; + +export default Step2Skills; diff --git a/src/components/mentorship/Step3DomainSkills.tsx b/src/components/mentorship/Step3DomainSkills.tsx new file mode 100644 index 0000000..1fa73e1 --- /dev/null +++ b/src/components/mentorship/Step3DomainSkills.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + Typography, Box +} from '@mui/material'; +import { boldLabelStyle, sectionHeaderStyle } from './mentorshipStyles'; +import StepSection from './StepSection'; +import { DOMAIN_GROUPS, SKILL_LEVELS } from '../../utils/mentorshipConstants'; +import { MentorshipSelect } from './MentorshipSelect'; + +const Step3DomainSkills = () => { + + return ( + + + {DOMAIN_GROUPS.map((group) => ( + + + {group.title} + + + {group.fields.map((skill) => ( + + + {skill.label} + + + + ))} + + ))} + + + + Page 3 of 6 + + + + + ); +}; + +export default Step3DomainSkills; diff --git a/src/components/mentorship/Step4ProgrammingSkills.tsx b/src/components/mentorship/Step4ProgrammingSkills.tsx new file mode 100644 index 0000000..cd6a050 --- /dev/null +++ b/src/components/mentorship/Step4ProgrammingSkills.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { + Typography, Box, Divider +} from '@mui/material'; +import { boldLabelStyle, sectionHeaderStyle } from './mentorshipStyles'; +import StepSection from './StepSection'; +import { CAREER_GOALS, PROGRAMMING_LANGUAGES, PREFERENCE_LEVELS } from '../../utils/mentorshipConstants'; +import { MentorshipSelect } from './MentorshipSelect'; + +const Step4ProgrammingSkills = () => { + + return ( + + + + {CAREER_GOALS.map((goal) => ( + + + {goal.label} + + + + ))} + + + + + + Add programming languages you can help your mentee with. + Mark your preference from Expert to Not Applicable. * + + + + Programming Languages: + + + {PROGRAMMING_LANGUAGES.map((lang) => ( + + + {lang.label} + + + + ))} + + + + + Page 4 of 6 + + + + + + ); +}; + +export default Step4ProgrammingSkills; diff --git a/src/components/mentorship/Step5Review.tsx b/src/components/mentorship/Step5Review.tsx new file mode 100644 index 0000000..17e445a --- /dev/null +++ b/src/components/mentorship/Step5Review.tsx @@ -0,0 +1,231 @@ +import React from 'react'; +import { useFormContext, Controller } from 'react-hook-form'; +import { + Grid, TextField, Typography, Box, Radio, RadioGroup, + FormControlLabel, FormControl, FormHelperText, Checkbox, + Accordion, AccordionSummary, AccordionDetails, Link +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import StepSection from './StepSection'; + +const inputStyle = { + '& .MuiOutlinedInput-root': { + backgroundColor: 'custom.softGray', + borderRadius: '4px', + '& fieldset': { border: 'none' }, + '&:hover fieldset': { border: 'none' }, + '&.Mui-focused fieldset': { + border: '1px solid', + borderColor: 'text.primary' + }, + }, + '& .MuiInputBase-input': { + padding: '16px 14px', + fontSize: '16px', + color: 'text.primary' + }, +}; + +const boldLabelStyle = { + fontWeight: 600, + color: 'text.primary', + mb: 1, + display: 'block', + fontSize: '15px', + fontFamily: 'Roboto' +}; + +const accordionStyle = { + backgroundColor: 'custom.softGray', + boxShadow: 'none', + borderRadius: '4px !important', + '&:before': { display: 'none' }, + mb: 3 +}; + +const Step5Review = () => { + const { register, control, formState: { errors } } = useFormContext(); + + return ( + + + + + + LinkedIn * + + + + + + + Other social media + + + + } + aria-controls="social-media-content" + id="social-media-header" + sx={{ minHeight: '56px' }} + > + Other social media + + + + + Github + + + + + Instagram + + + + + Medium + + + + + Website + + + + Other + + + + + + + + + + + Do you identify as a woman or non-binary? * + + ( + + } label="Yes" /> + } label="No" /> + } label="Prefer not to say" /> + + )} + /> + {errors.identity?.message as string} + + + + + + What are your preferred pronouns? e.g., he/him, she/her, they/them * + + + + We ask for your pronouns to ensure we address you correctly and respectfully. + + {errors.pronouns && ( + {errors.pronouns.message as string} + )} + + + + + + Are you happy for us to highlight/promote you as our mentor on our social media? * + + ( + + } label="Yes" /> + } label="No" /> + + )} + /> + + Please make sure your details are always up to date on our website. + + {errors.socialHighlight && ( + {errors.socialHighlight.message as string} + )} + + + + + + By selecting the checkbox below, you confirm that you: + + + +
  • + Have read and agree to the code of conduct{' '} and the {' '}mentorship code of conduct. +
  • +
  • + Grant Women Coding Community permission to store my contact information, use it to reach out to me, and publish mine mentor profile on the community's website. +
  • +
  • + I hereby grant the Women Coding Community the right to store the above-mentioned contact details to contact me and include my mentor profile on our website. +
  • +
    + + + ( + + )} + /> + } + label={ + + I accept and agree to the above terms * + + } + /> + {errors.termsAgreed?.message as string} + +
    + +
    + + + + Page 5 of 5 + + +
    + ); +}; + +export default Step5Review; diff --git a/src/components/mentorship/StepSection.tsx b/src/components/mentorship/StepSection.tsx new file mode 100644 index 0000000..4085caf --- /dev/null +++ b/src/components/mentorship/StepSection.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; + +interface StepSectionProps { + title: string; + description?: string; + children: React.ReactNode; +} + +const StepSection = ({ title, description, children }: StepSectionProps) => ( + + + {title} + + + {description && ( + + {description} + + )} + + {children} + +); + +export default StepSection; diff --git a/src/components/mentorship/mentorshipStyles.ts b/src/components/mentorship/mentorshipStyles.ts new file mode 100644 index 0000000..0c3a018 --- /dev/null +++ b/src/components/mentorship/mentorshipStyles.ts @@ -0,0 +1,38 @@ +// src/components/mentorship/mentorshipStyles.ts + +export const inputStyle = { + '& .MuiOutlinedInput-root': { + backgroundColor: 'custom.softGray', + borderRadius: '4px', + '& fieldset': { border: 'none' }, + '&:hover fieldset': { border: 'none' }, + '&.Mui-focused fieldset': { + border: '1px solid', + borderColor: 'text.primary', + }, + }, + '& .MuiInputBase-input': { + padding: '16px 14px', + fontSize: '16px', + color: 'text.primary', + }, + mb: 2, +}; + +export const boldLabelStyle = { + fontWeight: 600, + color: 'text.primary', + mb: 1, + display: 'block', + fontSize: '15px', + fontFamily: 'Roboto', +}; + +export const sectionHeaderStyle = { + fontWeight: 700, + color: 'text.primary', + fontSize: '18px', + mt: 4, + mb: 2, + fontFamily: 'Roboto', +}; \ No newline at end of file diff --git a/src/pages/mentorship/long-term-timeline.tsx b/src/pages/mentorship/long-term-timeline.tsx index 8b15104..7279c53 100644 --- a/src/pages/mentorship/long-term-timeline.tsx +++ b/src/pages/mentorship/long-term-timeline.tsx @@ -26,8 +26,8 @@ export const getServerSideProps: GetServerSideProps = async () => { try { const response = await fetchData('mentorship/long-term-timeline'); const props: CombinedResponse = { - data: response.data, - footer: response.footer, + data: response.data || null, + footer: response.footer || null, }; return { props, diff --git a/src/pages/mentorship/mentor-registration.tsx b/src/pages/mentorship/mentor-registration.tsx index 9294af1..9f1abb5 100644 --- a/src/pages/mentorship/mentor-registration.tsx +++ b/src/pages/mentorship/mentor-registration.tsx @@ -1,14 +1,259 @@ -// path: /mentorship/mentor-registration +import React, { useState } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Box, Container, Paper, Typography, Button, Stack, useMediaQuery, useTheme } from '@mui/material'; +import Step1BasicInfo from '../../components/mentorship/Step1BasicInfo'; +import Step2Skills from '../../components/mentorship/Step2Skills'; +import Step3DomainSkills from 'components/mentorship/Step3DomainSkills'; +import Step4ProgrammingSkills from 'components/mentorship/Step4ProgrammingSkills'; -import { Typography } from '@mui/material'; +import { mentorRegistrationSchema, MentorRegistrationData } from '../../schemas/mentorSchema'; +import Step5Review from 'components/mentorship/Step5Review'; + +const validateStep1 = async (formMethods: any) => { + const isStandardValid = await formMethods.trigger([ + 'firstName', 'email', 'slackName', 'country', 'city', + 'jobTitle', 'company', 'calendlyLink', 'menteeExpectations', 'openToNonWomen', + ]); + + const isLongTerm = formMethods.getValues('isLongTermMentor'); + const isAdHoc = formMethods.getValues('isAdHocMentor'); + + formMethods.clearErrors(['isLongTermMentor', 'maxMentees', 'adHocAvailability']); + + let isTypeValid = true; + + if (!isLongTerm && !isAdHoc) { + formMethods.setError('isLongTermMentor', { + type: 'manual', + message: 'Please select at least one mentorship format.' + }); + isTypeValid = false; + } + + if (isLongTerm) { + const maxMentees = formMethods.getValues('maxMentees'); + if (!maxMentees) { + formMethods.setError('maxMentees', { + type: 'manual', + message: 'Please select the number of mentees' + }); + isTypeValid = false; + } + } + + if (isAdHoc) { + const adHoc = formMethods.getValues('adHocAvailability'); + if (!adHoc || Object.keys(adHoc).length === 0) { + formMethods.setError('adHocAvailability', { + type: 'manual', + message: 'Please select availability for at least one month' + }); + isTypeValid = false; + } + } + + return isStandardValid && isTypeValid; +}; + +const validateStep2 = async (formMethods: any) => { + return await formMethods.trigger([ + 'languages', 'yearsOfExperience', 'bio', + 'mentoringTopics', 'photoSource', 'customPhotoUrl' + ] as const); +}; + +const validateStep5 = async (formMethods: any) => { + return await formMethods.trigger([ + 'linkedin', 'github', 'instagram', 'medium', + 'website', 'otherSocial', 'identity', 'pronouns', + 'socialHighlight', 'termsAgreed' + ]); +}; const MentorRegistrationPage = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const formMethods = useForm({ + resolver: zodResolver(mentorRegistrationSchema), + mode: 'onChange', + }); + + const [activeStep, setActiveStep] = useState(1); + const totalSteps = 5; + + const handleNext = async () => { + let isStepValid = false; + + switch (activeStep) { + case 1: + isStepValid = await validateStep1(formMethods); + break; + case 2: + isStepValid = await validateStep2(formMethods); + break; + case 3: + case 4: + isStepValid = true; + break; + case 5: + isStepValid = await validateStep5(formMethods); + break; + default: + break; + } + + if (isStepValid && activeStep < totalSteps) { + setActiveStep(prev => prev + 1); + window.scrollTo(0, 0); + } + }; + + const handleBack = () => { + if (activeStep > 1) setActiveStep(prev => prev - 1); + }; + + const onSubmit = (data: MentorRegistrationData) => { + console.log('Form Data Submitted:', data); + }; + return ( -
    - - Welcome to the MentorRegistrationPage - -
    + + + + + + + + Step {activeStep} of {totalSteps} + + + + + + + + {activeStep === 1 && } + {activeStep === 2 && } + {activeStep === 3 && } + {activeStep === 4 && } + {activeStep === 5 && } + + + + + + {activeStep === totalSteps ? ( + + ) : ( + + )} + + + + + ); }; diff --git a/src/schemas/mentorSchema.ts b/src/schemas/mentorSchema.ts new file mode 100644 index 0000000..3d41840 --- /dev/null +++ b/src/schemas/mentorSchema.ts @@ -0,0 +1,174 @@ +import { z } from 'zod'; + +export const basicInfoObj = z.object({ + firstName: z.string().min(1, 'Please enter your full name'), + email: z.string().email('Please enter a valid email address'), + slackName: z.string().min(1, 'Please enter your Slack name'), + country: z.string().min(1, 'Please select your country'), + city: z.string().min(1, 'Please enter your city'), + jobTitle: z.string().min(1, 'Please enter your job title'), + company: z.string().min(1, 'Please enter your company name'), + + isLongTermMentor: z.boolean().optional(), + isAdHocMentor: z.boolean().optional(), + + maxMentees: z.string().optional(), + adHocAvailability: z.record(z.string(), z.string()).optional(), + + calendlyLink: z.string() + .url('Please enter a valid URL') + .refine( + (url) => url.includes('calendly.com'), + 'Please enter a valid Calendly URL (e.g., https://calendly.com/yourname)' + ), + menteeExpectations: z.string() + .min(10, 'Please provide at least 10 characters describing your ideal mentee'), + openToNonWomen: z.enum(['true', 'false'], { + message: 'Please select an option', +}).transform((val) => val === 'true'), + +}); + +const validateBasicInfo = (data: z.infer, ctx: z.RefinementCtx) => { + if (!data.isLongTermMentor && !data.isAdHocMentor) { + ctx.addIssue({ + code: "custom", + message: "Please select at least one mentorship type", + path: ["isLongTermMentor"], + }); + } + + if (data.isLongTermMentor) { + if (!data.maxMentees || data.maxMentees.length === 0) { + ctx.addIssue({ + code: "custom", + message: "Please select the number of mentees", + path: ["maxMentees"], + }); + } + } + + if (data.isAdHocMentor) { + const hasAvailability = data.adHocAvailability && Object.keys(data.adHocAvailability).length > 0; + if (!hasAvailability) { + ctx.addIssue({ + code: "custom", + message: "Please select availability for at least one month", + path: ["adHocAvailability"], + }); + } + } +}; +export const basicInfoSchema = basicInfoObj.superRefine(validateBasicInfo); +export type BasicInfoData = z.infer; + +export const profileSchema = z.object({ + languages: z.array(z.string()).min(1, "Please select at least one language"), + yearsOfExperience: z.string().min(1, "Please select your years of experience"), + bio: z.string() + .min(10, "Please provide at least 10 characters for your bio") + .max(1000, "Bio must not exceed 1000 characters"), + mentoringTopics: z.string().optional(), + photoSource: z.enum(['linkedin', 'slack', 'other'], { + message: 'Please select a photo source', +}), + customPhotoUrl: z.string() + .url('Please enter a valid URL') + .optional() + .or(z.literal('')), +}).refine( + (data) => { + if (data.photoSource === 'other') { + return data.customPhotoUrl && data.customPhotoUrl.length > 0; + } + return true; + }, + { + message: 'Please provide a photo URL', + path: ['customPhotoUrl'], + } +); + +export type ProfileData = z.infer; + +const skillLevel = z.string().optional(); + +export const skillsSchema = z.object({ + dataEngineering: skillLevel, + dataScience: skillLevel, + genAI: skillLevel, + machineLearning: skillLevel, + mlOps: skillLevel, + + cloudComputing: skillLevel, + devOps: skillLevel, + networkEngineering: skillLevel, + platformEngineering: skillLevel, + security: skillLevel, + sre: skillLevel, + + agile: skillLevel, + businessAnalysis: skillLevel, + engineeringMgmt: skillLevel, + productMgmt: skillLevel, + projectMgmt: skillLevel, + technicalLeadership: skillLevel, + + backend: skillLevel, + frontend: skillLevel, + fullstack: skillLevel, + mobileAndroid: skillLevel, + mobileIos: skillLevel, + qaAutomation: skillLevel, + systemDesign: skillLevel, +}); +export type SkillsData = z.infer; + +export const programmingSchema = z.object({ + careerSwitch: skillLevel, + beginnerToMid: skillLevel, + midToSenior: skillLevel, + seniorPlus: skillLevel, + icToManager: skillLevel, + specialisationSwitch: skillLevel, + + c: skillLevel, + cSharp: skillLevel, + go: skillLevel, + java: skillLevel, + javascript: skillLevel, + kotlin: skillLevel, + python: skillLevel, + rust: skillLevel, + scala: skillLevel, + sql: skillLevel, + swift: skillLevel, + typescript: skillLevel, +}); +export type ProgrammingData = z.infer; + +export const reviewSchema = z.object({ + linkedin: z.string().url({ message: "Invalid LinkedIn URL" }), + github: z.string().url({ message: "Invalid URL" }).optional().or(z.literal('')), + instagram: z.string().url({ message: "Invalid URL" }).optional().or(z.literal('')), + medium: z.string().url({ message: "Invalid URL" }).optional().or(z.literal('')), + website: z.string().url({ message: "Invalid URL" }).optional().or(z.literal('')), + otherSocial: z.string().optional(), + identity: z.string().min(1, "Please select an option"), + pronouns: z.string().min(1, "Pronouns are required"), + socialHighlight: z.string().min(1, "Please select Yes or No"), + termsAgreed: z.boolean().refine((val) => val === true, { + message: "You must agree to the code of conduct and terms", + }), +}); +export type ReviewData = z.infer; + +export const mentorRegistrationSchema = z.object({ + ...basicInfoObj.shape, + ...profileSchema.shape, + ...skillsSchema.shape, + ...programmingSchema.shape, + ...reviewSchema.shape, +}).superRefine(validateBasicInfo); + +export type MentorRegistrationData = z.infer; diff --git a/src/utils/mentorshipConstants.ts b/src/utils/mentorshipConstants.ts new file mode 100644 index 0000000..b45e3b0 --- /dev/null +++ b/src/utils/mentorshipConstants.ts @@ -0,0 +1,138 @@ +// src/utils/mentorshipConstants.ts +export const SKILL_LEVELS = [ + 'Expert', + 'Proficient', + 'Experienced', + 'Familiar', + 'Not Applicable' +]; + +export const PREFERENCE_LEVELS = [ + 'Low', + 'Medium', + 'High', + 'Not Applicable' +]; + +interface DomainField { + name: string; + label: string; +} + +interface DomainGroup { + title: string; + fields: DomainField[]; +} + +interface Country { + code: string; + name: string; +} + +const createFields = (fields: Array<[string, string]>): DomainField[] => { + return fields.map(([name, label]) => ({ name, label })); +}; + +const createCountries = (data: Array<[string, string]>): Country[] => { + return data.map(([code, name]) => ({ code, name })); +}; + +const createGroup = (title: string, fieldData: Array<[string, string]>): DomainGroup => { + return { + title, + fields: createFields(fieldData) + }; +}; + +export const DOMAIN_GROUPS: DomainGroup[] = [ + createGroup("AI, Data & ML", [ + ['dataEngineering', 'Data Engineering'], + ['dataScience', 'Data Science'], + ['genAI', 'Generative AI and LLMs'], + ['machineLearning', 'Machine Learning and AI'], + ['mlOps', 'MLOps and AI Deployment'], + ]), + createGroup("Infrastructure & Operations", [ + ['cloudComputing', 'Cloud Computing'], + ['devOps', 'DevOps'], + ['networkEngineering', 'Network Engineering'], + ['platformEngineering', 'Platform Engineering'], + ['security', 'Security and Cybersecurity'], + ['sre', 'Site Reliability Engineering'], + ]), + createGroup("Product, Leadership & Delivery", [ + ['agile', 'Agile and Scrum Practices'], + ['businessAnalysis', 'Business Analysis'], + ['engineeringMgmt', 'Engineering Management'], + ['productMgmt', 'Product Management'], + ['projectMgmt', 'Project Management'], + ['technicalLeadership', 'Technical Leadership'], + ]), + createGroup("Software Development", [ + ['backend', 'Backend Development'], + ['frontend', 'Frontend Development'], + ['fullstack', 'Fullstack Development'], + ['mobileAndroid', 'Mobile Development - Android'], + ['mobileIos', 'Mobile Development - iOS'], + ['qaAutomation', 'QA and Test Automation'], + ['systemDesign', 'System Design and Software Architecture'], + ]), +]; + +export const CAREER_GOALS = createFields([ + ['careerSwitch', 'Switch career to IT'], + ['beginnerToMid', 'Grow from beginner to mid-level'], + ['midToSenior', 'Grow from mid-level to senior-level'], + ['seniorPlus', 'Grow beyond senior level'], + ['icToManager', 'Switch from IC to management position'], + ['specialisationSwitch', 'Change specialisation within IT'], +]); + +export const PROGRAMMING_LANGUAGES = createFields([ + ['c', 'C'], + ['cSharp', 'C#'], + ['go', 'Go'], + ['java', 'Java'], + ['javascript', 'JavaScript'], + ['kotlin', 'Kotlin'], + ['python', 'Python'], + ['rust', 'Rust'], + ['scala', 'Scala'], + ['sql', 'SQL'], + ['swift', 'Swift'], + ['typescript', 'TypeScript'], +]); + +export const COUNTRIES = createCountries([ + ['GB', 'United Kingdom'], + ['US', 'United States'], + ['CA', 'Canada'], + ['AU', 'Australia'], + ['DE', 'Germany'], + ['FR', 'France'], + ['ES', 'Spain'], + ['IT', 'Italy'], + ['NL', 'Netherlands'], + ['SE', 'Sweden'], + ['NO', 'Norway'], + ['DK', 'Denmark'], + ['FI', 'Finland'], + ['IE', 'Ireland'], + ['PL', 'Poland'], + ['PT', 'Portugal'], + ['GR', 'Greece'], + ['CH', 'Switzerland'], + ['AT', 'Austria'], + ['BE', 'Belgium'], + ['CZ', 'Czech Republic'], + ['IN', 'India'], + ['SG', 'Singapore'], + ['JP', 'Japan'], + ['CN', 'China'], + ['BR', 'Brazil'], + ['MX', 'Mexico'], + ['AR', 'Argentina'], + ['ZA', 'South Africa'], + ['NZ', 'New Zealand'], + ['OTHER', 'Other'], +]);