From b08e09cb5fb533f6d41fe71594039b5114688812 Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Sat, 21 Mar 2026 09:18:24 +0100 Subject: [PATCH 1/5] feat: update mentor preference question copy to be more inclusive The original question framing ("do not identify as women") was negatively worded and lacked context for mentors filling out the form. The new copy adds a brief mission statement to help mentors understand the community's priorities before answering, and replaces the bare "Yes/No" radio labels with descriptive options that eliminate ambiguity. The aria-label is also updated to remove the negative framing for screen reader users. --- src/components/mentorship/Step1BasicInfo.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/mentorship/Step1BasicInfo.tsx b/src/components/mentorship/Step1BasicInfo.tsx index 3b9bed6..f0beb86 100644 --- a/src/components/mentorship/Step1BasicInfo.tsx +++ b/src/components/mentorship/Step1BasicInfo.tsx @@ -423,8 +423,9 @@ const Step1BasicInfo = () => { component="legend" sx={{ mb: 0.5, color: 'text.primary' }} > - Are you open to mentoring individuals who do not identify as - women? * + Our community prioritizes supporting women and underrepresented + genders in tech. Are you open to mentoring mentees of all gender + identities? * { render={({ field }) => ( { } - label="Yes" + label="Yes, open to mentoring people of all genders" /> } - label="No" + label="I prefer to focus on women and non-binary individuals" /> )} From 04e77c62149c8778015294561320fd66380b225e Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Sun, 22 Mar 2026 19:49:18 +0100 Subject: [PATCH 2/5] feat: implement mentor registration form submission and API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mentor registration form previously had no real submission logic — onSubmit only logged data to the console. This commit wires up the full submission flow: a new /api/mentor-registration Next.js route maps the frontend schema to the backend MentorDto and calls the platform API via a new shared proxyRequest utility extracted into src/lib/api.ts. The existing mentee-registration and mentors API routes are refactored to use the same utility, eliminating duplicated env-var/header handling. Schema fixes address adHocAvailability empty-value handling, and default values are added to prevent uncontrolled-to-controlled input warnings. Step 3 and Step 4 validation are wired up, and loading/error UI states give the user feedback during submission. --- .claude/skills/open-pr/SKILL.md | 69 +++++++ src/__tests__/api/mentee-registration.test.ts | 95 +++------- src/__tests__/api/mentor-registration.test.ts | 162 +++++++++++++++++ src/lib/__tests__/api.test.ts | 94 ++++++++++ src/lib/api.test.ts | 77 -------- src/lib/api.ts | 70 ++++---- src/pages/api/mentee-registration.ts | 41 ++--- src/pages/api/mentor-registration.ts | 112 ++++++++++++ src/pages/api/mentors.ts | 34 ++-- src/pages/mentorship/mentor-registration.tsx | 169 +++++++++++++++++- src/schemas/__tests__/mentorSchema.test.ts | 86 +++++++++ src/schemas/mentorSchema.ts | 61 ++++++- 12 files changed, 831 insertions(+), 239 deletions(-) create mode 100644 .claude/skills/open-pr/SKILL.md create mode 100644 src/__tests__/api/mentor-registration.test.ts create mode 100644 src/lib/__tests__/api.test.ts delete mode 100644 src/lib/api.test.ts create mode 100644 src/pages/api/mentor-registration.ts create mode 100644 src/schemas/__tests__/mentorSchema.test.ts diff --git a/.claude/skills/open-pr/SKILL.md b/.claude/skills/open-pr/SKILL.md new file mode 100644 index 0000000..42809e6 --- /dev/null +++ b/.claude/skills/open-pr/SKILL.md @@ -0,0 +1,69 @@ +--- +name: open-pr +description: Generate a PR title and description following the WCC repo template, infer change types from the diff, and print the GitHub URL to open the PR in the browser. Use when asked to open a PR or create a pull request. +disable-model-invocation: true +allowed-tools: Bash, Read, Glob +--- + +# Open PR Skill + +> **Canonical runbook**: `.ai/skills/open-pr.md` +> This file is the Claude Code adapter. The workflow logic is defined in the canonical skill +> so it can be shared with other agents (Codex, Copilot, Cursor). Any changes to the workflow +> should be made in `.ai/skills/open-pr.md` first. + +## Workflow + +1. Gather context in parallel: + - `git log main..HEAD --oneline` — commits in this PR + - `git diff main...HEAD --stat` — files changed + - `git branch --show-current` — current branch name + - Read `.github/PULL_REQUEST_TEMPLATE.md` + +2. Determine change types by mapping conventional-commit prefixes and the nature of the diff: + + | Conventional prefix | PR Change Type | + |---|---| + | `feat` | New Feature | + | `fix` | Bug Fix | + | `refactor` | Code Refactor | + | `docs` / `doc` | Documentation | + | `test` | Test | + | `chore` / `ci` / `build` | Other | + + Only keep the change types present in this PR — remove the rest from the template. + +3. Suggest a PR title using Conventional Commits format: `: ` (50–72 chars). When multiple types apply, use the most significant: `feat` > `fix` > `refactor` > `test` > `docs` > `chore`. + +4. Generate the filled-in PR description. Pre-check only the applicable change type boxes; remove the inapplicable ones entirely. Write all prose sections (Description, Related Issue) as flowing paragraphs — do NOT wrap lines at any character limit. Each sentence or logical thought should continue on the same line so that GitHub renders the text correctly without unwanted line breaks. + +5. **Screenshots section** — decide based on changed files: + - Frontend changes (`admin-wcc-app/**`, `*.tsx`, `*.css`, components, pages): include the section and list the specific screenshots needed (before/after UI, error states, label/title changes, GIF for interactions). Emphasise that screenshots are required for the reviewer to assess visual changes. + - Backend API changes (new/changed controllers or endpoints): include the section asking for Swagger UI screenshots (`/swagger-ui/index.html`). + - Only `test`, `docs`, `chore`, `ci`, or `refactor` with no endpoint changes: **omit the Screenshots section entirely**. + +6. **Pull request checklist** — always include the contributor guide checkbox. Include "I have tested my changes locally" only for `feat`, `fix`, `refactor`, or frontend changes — omit it for pure `docs`, `chore`, or `ci` changes. + +7. Derive the contributor's GitHub username from the `origin` remote: + ```bash + git remote get-url origin + # https://github.com//wcc-backend.git or git@github.com:/wcc-backend.git + ``` + Extract `` from the URL (path segment before `/wcc-backend`). + + Print: + - **Suggested PR title** (plain text) + - **PR description** (markdown code block) + - **GitHub compare URL**: `https://github.com/Women-Coding-Community/wcc-backend/compare/main...:` + - Remind the user to add screenshots before submitting if required + +## Rules + +- **Never hard-wrap prose** — description text must not have forced line breaks mid-sentence or mid-paragraph; let GitHub reflow the text naturally + +- Always target `main` on `Women-Coding-Community/wcc-backend` (upstream) +- Never use `gh pr create` — fork permissions require the user to open the PR through the browser +- If the issue number cannot be found in branch name or commits, use `#?` and ask the user +- Never include change type checkboxes for types not present in the diff +- Never include the Screenshots section for pure test/docs/chore/ci changes +- Never include "I have tested my changes locally" for pure docs/chore/ci changes \ No newline at end of file diff --git a/src/__tests__/api/mentee-registration.test.ts b/src/__tests__/api/mentee-registration.test.ts index 8ea3fc7..cac17c2 100644 --- a/src/__tests__/api/mentee-registration.test.ts +++ b/src/__tests__/api/mentee-registration.test.ts @@ -1,6 +1,13 @@ import { NextApiRequest, NextApiResponse } from 'next'; import handler from '../../pages/api/mentee-registration'; +import * as api from '../../lib/api'; + +jest.mock('../../lib/api', () => ({ + __esModule: true, + ...jest.requireActual('../../lib/api'), + proxyRequest: jest.fn(), +})); const makeReq = (overrides: Partial = {}): NextApiRequest => ({ @@ -20,18 +27,7 @@ const makeRes = (): NextApiResponse => { }; describe('mentee-registration API handler', () => { - const originalEnv = process.env; - - beforeEach(() => { - process.env = { - ...originalEnv, - API_BASE_URL: 'http://localhost:8080/api/cms/v1', - API_KEY: 'test-key', - }; - }); - afterEach(() => { - process.env = originalEnv; jest.resetAllMocks(); }); @@ -46,66 +42,35 @@ describe('mentee-registration API handler', () => { expect(res.json).toHaveBeenCalledWith({ error: 'Method GET Not Allowed' }); }); - it('returns 500 when API_BASE_URL is missing', async () => { - delete process.env.API_BASE_URL; - const req = makeReq(); - const res = makeRes(); - - await handler(req, res); - - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ - error: 'Server configuration error', - }); - }); - - it('returns 500 when API_KEY is missing', async () => { - delete process.env.API_KEY; - const req = makeReq(); - const res = makeRes(); - - await handler(req, res); - - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ - error: 'Server configuration error', - }); - }); - it('proxies POST to the platform endpoint and returns 201 on success', async () => { const responseBody = { id: 42 }; - globalThis.fetch = jest.fn().mockResolvedValue({ - ok: true, - status: 201, - json: jest.fn().mockResolvedValue(responseBody), - }); + (api.proxyRequest as jest.Mock).mockResolvedValue(responseBody); const req = makeReq(); const res = makeRes(); await handler(req, res); - expect(globalThis.fetch).toHaveBeenCalledWith( - 'http://localhost:8080/api/platform/v1/mentees', + expect(api.proxyRequest).toHaveBeenCalledWith( + 'mentees', expect.objectContaining({ method: 'POST', - headers: expect.objectContaining({ - 'X-API-KEY': 'test-key', - 'Content-Type': 'application/json', - }), + data: req.body, }), + true ); expect(res.status).toHaveBeenCalledWith(201); expect(res.json).toHaveBeenCalledWith(responseBody); }); it('forwards backend error status and message on failure', async () => { - const errorBody = { message: 'Email already registered' }; - globalThis.fetch = jest.fn().mockResolvedValue({ - ok: false, - status: 409, - json: jest.fn().mockResolvedValue(errorBody), - }); + const errorResponse = { + response: { + status: 409, + data: { message: 'Email already registered' }, + }, + }; + (api.proxyRequest as jest.Mock).mockRejectedValue(errorResponse); const req = makeReq(); const res = makeRes(); @@ -113,15 +78,11 @@ describe('mentee-registration API handler', () => { await handler(req, res); expect(res.status).toHaveBeenCalledWith(409); - expect(res.json).toHaveBeenCalledWith(errorBody); + expect(res.json).toHaveBeenCalledWith(errorResponse.response.data); }); - it('falls back to generic error when backend returns no body', async () => { - globalThis.fetch = jest.fn().mockResolvedValue({ - ok: false, - status: 500, - json: jest.fn().mockRejectedValue(new Error('no body')), - }); + it('returns 500 on unexpected error', async () => { + (api.proxyRequest as jest.Mock).mockRejectedValue(new Error('Network failure')); const req = makeReq(); const res = makeRes(); @@ -129,15 +90,11 @@ describe('mentee-registration API handler', () => { await handler(req, res); expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ - error: 'Registration failed. Please try again.', - }); + expect(res.json).toHaveBeenCalledWith({ error: 'Internal server error' }); }); - it('returns 500 on network error', async () => { - globalThis.fetch = jest - .fn() - .mockRejectedValue(new Error('Network failure')); + it('returns 500 when server configuration error occurs', async () => { + (api.proxyRequest as jest.Mock).mockRejectedValue(new Error('Server configuration error')); const req = makeReq(); const res = makeRes(); @@ -145,6 +102,6 @@ describe('mentee-registration API handler', () => { await handler(req, res); expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ error: 'Internal server error' }); + expect(res.json).toHaveBeenCalledWith({ error: 'Server configuration error' }); }); }); diff --git a/src/__tests__/api/mentor-registration.test.ts b/src/__tests__/api/mentor-registration.test.ts new file mode 100644 index 0000000..4b74150 --- /dev/null +++ b/src/__tests__/api/mentor-registration.test.ts @@ -0,0 +1,162 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import handler from '../../pages/api/mentor-registration'; +import * as api from '../../lib/api'; + +jest.mock('../../lib/api', () => ({ + __esModule: true, + ...jest.requireActual('../../lib/api'), + proxyRequest: jest.fn(), +})); + +const makeReq = (overrides: Partial = {}): NextApiRequest => + ({ + method: 'POST', + body: { + fullName: 'Jane Doe', + email: 'jane@example.com', + slackDisplayName: 'janedoe', + country: 'United Kingdom', + city: 'London', + position: 'Software Engineer', + companyName: 'Tech Co', + isLongTermMentor: true, + maxMentees: '2', + calendlyLink: 'https://calendly.com/janedoe', + menteeExpectations: 'Eager to learn and proactive.', + openToNonWomen: true, + languages: ['English'], + yearsExperience: '5', + bio: 'A passionate developer with 5 years of experience.', + technicalAreas: [{ technicalArea: 'Fullstack', proficiencyLevel: 'Advanced' }], + codeLanguages: [{ language: 'C++', proficiencyLevel: 'Advanced' }], + mentorshipFocusAreas: ['Career advice'], + linkedin: 'https://linkedin.com/in/janedoe', + identity: 'Woman', + pronouns: 'she/her', + socialHighlight: 'Yes', + termsAgreed: true, + }, + ...overrides, + }) as NextApiRequest; + +const makeRes = (): NextApiResponse => { + const res = { + status: jest.fn(), + json: jest.fn(), + setHeader: jest.fn(), + } as unknown as NextApiResponse; + (res.status as jest.Mock).mockReturnValue(res); + return res; +}; + +describe('mentor-registration API handler', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('returns 405 for non-POST methods', async () => { + const req = makeReq({ method: 'GET' }); + const res = makeRes(); + + await handler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Allow', ['POST']); + expect(res.status).toHaveBeenCalledWith(405); + }); + + it('proxies POST to the platform endpoint and correctly maps data', async () => { + const responseBody = { id: 123 }; + (api.proxyRequest as jest.Mock).mockResolvedValue(responseBody); + + const req = makeReq(); + const res = makeRes(); + + await handler(req, res); + + expect(api.proxyRequest).toHaveBeenCalledWith( + 'mentors', + expect.objectContaining({ + method: 'POST', + data: expect.objectContaining({ + fullName: 'Jane Doe', + pronounCategory: 'FEMININE', + country: { + countryCode: 'GB', + countryName: 'United Kingdom', + }, + skills: expect.objectContaining({ + yearsExperience: 5, + areas: [ + { technicalArea: 'FULLSTACK', proficiencyLevel: 'ADVANCED' }, + ], + languages: [ + { language: 'C_PLUS_PLUS', proficiencyLevel: 'ADVANCED' }, + ], + }), + menteeSection: expect.objectContaining({ + longTerm: expect.objectContaining({ + numMentee: 2, + }), + }), + }), + }), + true + ); + + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith(responseBody); + }); + + it('handles backend errors', async () => { + const errorResponse = { + response: { + status: 400, + data: { message: 'Invalid data' }, + }, + }; + (api.proxyRequest as jest.Mock).mockRejectedValue(errorResponse); + + const req = makeReq(); + const res = makeRes(); + + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith(errorResponse.response.data); + }); + + it('correctly maps adHocAvailability and handles empty values', async () => { + (api.proxyRequest as jest.Mock).mockResolvedValue({ id: 123 }); + + const req = makeReq({ + body: { + ...makeReq().body, + isAdHocMentor: true, + adHocAvailability: { + January: '2', + February: '', + March: undefined, + April: '3', + }, + }, + }); + const res = makeRes(); + + await handler(req, res); + + expect(api.proxyRequest).toHaveBeenCalledWith( + 'mentors', + expect.objectContaining({ + data: expect.objectContaining({ + menteeSection: expect.objectContaining({ + adHoc: [ + { month: 'JANUARY', hours: 2 }, + { month: 'APRIL', hours: 3 }, + ], + }), + }), + }), + true + ); + }); +}); diff --git a/src/lib/__tests__/api.test.ts b/src/lib/__tests__/api.test.ts new file mode 100644 index 0000000..9208500 --- /dev/null +++ b/src/lib/__tests__/api.test.ts @@ -0,0 +1,94 @@ +jest.mock('axios', () => { + const mockAxios = jest.fn(); + (mockAxios as any).create = jest.fn(() => mockAxios); + (mockAxios as any).get = jest.fn(); + (mockAxios as any).post = jest.fn(); + return { + __esModule: true, + default: mockAxios, + }; +}); + +describe('API Fetch Functions', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let fetchFooter: () => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let fetchData: (path: string) => Promise; + let mockedAxios: jest.Mock; + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { + ...originalEnv, + API_BASE_URL: 'http://localhost:8080/api/cms/v1', + API_KEY: 'test-key', + }; + // Re-require after setting env so module-level constants pick them up + // eslint-disable-next-line @typescript-eslint/no-require-imports + const api = require('../api'); + fetchFooter = api.fetchFooter; + fetchData = api.fetchData; + // eslint-disable-next-line @typescript-eslint/no-require-imports + mockedAxios = require('axios').default; + }); + + afterEach(() => { + process.env = originalEnv; + jest.clearAllMocks(); + }); + + describe('fetchFooter', () => { + it('should return footer data when API call is successful', async () => { + const mockFooterData = { footer: 'Footer content' }; + mockedAxios.mockResolvedValue({ + status: 200, + data: mockFooterData, + }); + + const result = await fetchFooter(); + expect(result).toEqual(mockFooterData); + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining('/footer'), + }), + ); + }); + + it('should return fallback data when API call fails', async () => { + mockedAxios.mockRejectedValue(new Error('API Error')); + + const result = await fetchFooter(); + expect(result).toEqual(expect.objectContaining({ title: 'Follow Us' })); + }); + }); + + describe('fetchData', () => { + it('should return data and footer when API calls are successful', async () => { + const mockData = { key: 'value' }; + const mockFooterData = { footer: 'Footer content' }; + + mockedAxios + .mockResolvedValueOnce({ status: 200, data: mockData }) + .mockResolvedValueOnce({ status: 200, data: mockFooterData }); + + const result = await fetchData('test-path'); + + expect(result).toEqual({ data: mockData, footer: mockFooterData }); + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining('/test-path'), + }), + ); + }); + + it('should return fallback data when fetchData API call fails', async () => { + mockedAxios.mockRejectedValue(new Error('API Error')); + + const result = await fetchData('landingPage'); + expect(result.data).toEqual( + expect.objectContaining({ id: 'page:LANDING_PAGE' }), + ); + }); + }); +}); diff --git a/src/lib/api.test.ts b/src/lib/api.test.ts deleted file mode 100644 index 352b5d2..0000000 --- a/src/lib/api.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import axios from 'axios'; - -import { fetchData, fetchFooter } from './api'; - -jest.mock('axios'); - -// @TODO the checks for failing API calls are skipped due to temporary fix whenever database is down - -describe('API Fetch Functions', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('fetchFooter', () => { - it('should return footer data when API call is successful', async () => { - const mockFooterData = { footer: 'Footer content' }; - (axios.get as jest.MockedFunction).mockResolvedValue({ - status: 200, - data: mockFooterData, - }); - - const result = await fetchFooter(); - expect(result).toEqual(mockFooterData); - expect(axios.get).toHaveBeenCalledWith( - `${process.env.API_BASE_URL}/footer`, - { - headers: { 'X-API-KEY': process.env.API_KEY }, - }, - ); - }); - - it.skip('should throw an error when API call fails', async () => { - (axios.get as jest.MockedFunction).mockRejectedValue( - new Error('API Error'), - ); - - await expect(fetchFooter()).rejects.toThrow('Failed to fetch footer'); - }); - }); - - describe('fetchData', () => { - it('should return data and footer when API calls are successful', async () => { - const mockData = { key: 'value' }; - const mockFooterData = { footer: 'Footer content' }; - - (axios.get as jest.MockedFunction) - .mockResolvedValueOnce({ status: 200, data: mockData }) - .mockResolvedValueOnce({ status: 200, data: mockFooterData }); - - const result = await fetchData('test-path'); - - expect(result).toEqual({ data: mockData, footer: mockFooterData }); - expect(axios.get).toHaveBeenCalledWith( - `${process.env.API_BASE_URL}/test-path`, - { - headers: { 'X-API-KEY': process.env.API_KEY }, - }, - ); - expect(axios.get).toHaveBeenCalledWith( - `${process.env.API_BASE_URL}/footer`, - { - headers: { 'X-API-KEY': process.env.API_KEY }, - }, - ); - }); - - it.skip('should throw an error when fetchData API call fails', async () => { - ( - axios.get as jest.MockedFunction - ).mockRejectedValueOnce(new Error('API Error')); - - await expect(fetchData('test-path')).rejects.toThrow( - 'Failed to fetch data', - ); - }); - }); -}); diff --git a/src/lib/api.ts b/src/lib/api.ts index eb6cbd3..2a07dfb 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosRequestConfig } from 'axios'; import { logger } from 'bs-logger'; import aboutUsPage from './responses/aboutUs.json'; @@ -21,6 +21,39 @@ const client = axios.create({ timeout: 5000, }); +/** + * Shared request handler for API routes to avoid duplication. + */ +export const proxyRequest = async ( + path: string, + options: AxiosRequestConfig = {}, + usePlatformApi = false, +) => { + if (!apiBaseUrl || !API_KEY) { + logger.error('Server configuration error: API_BASE_URL or API_KEY is missing'); + throw new Error('Server configuration error'); + } + + let url = `${apiBaseUrl}/${path}`; + + if (usePlatformApi) { + // Derive platform API host from CMS API URL (e.g., http://host/api/cms/v1 -> http://host/api/platform/v1) + const host = apiBaseUrl.split('/api/')[0]; + url = `${host}/api/platform/v1/${path}`; + } + + try { + const response = await client({ + url, + ...options, + }); + return response.data; + } catch (error: any) { + logger.error(`API request failed for ${url}: ${error.message}`); + throw error; + } +}; + const pageData = { landingPage: landingPageData, 'mentorship/overview': mentorShipPage, @@ -36,46 +69,23 @@ const pageData = { export const fetchData = async (path: string) => { try { - logger.info( - `Attempting to fetchData for ${apiBaseUrl}/${path} with ${API_KEY}`, - ); - const response = await client.get(`${apiBaseUrl}/${path}`, { - headers: { - 'X-API-KEY': API_KEY, - }, - }); - - const footerData = await fetchFooter(); - return { - data: response.data, - footer: footerData, - }; + const data = await proxyRequest(path); + const footer = await fetchFooter(); + return { data, footer }; } catch (error) { - logger.error( - `Failed to fetchData for ${path} with ${API_KEY}. Error: ${error}`, - ); - const footerData = await fetchFooter(); - + const footer = await fetchFooter(); return { //@ts-ignore data: pageData[path], - footer: footerData, + footer, }; } }; export const fetchFooter = async () => { try { - logger.debug(`Attempting to fetchFooter`); - const response = await client.get(`${apiBaseUrl}/footer`, { - headers: { 'X-API-KEY': API_KEY }, - }); - - return response.data; + return await proxyRequest('footer'); } catch (error) { - logger.error( - `Failed to fetchFooter, generating fallback footer. Error: ${error}`, - ); return footerData; } }; diff --git a/src/pages/api/mentee-registration.ts b/src/pages/api/mentee-registration.ts index 57667b4..40cc9a3 100644 --- a/src/pages/api/mentee-registration.ts +++ b/src/pages/api/mentee-registration.ts @@ -1,6 +1,7 @@ -import { logger } from 'bs-logger'; import { NextApiRequest, NextApiResponse } from 'next'; +import { proxyRequest } from '../../lib/api'; + export default async function handler( req: NextApiRequest, res: NextApiResponse, @@ -10,38 +11,20 @@ export default async function handler( return res.status(405).json({ error: `Method ${req.method} Not Allowed` }); } - const apiBaseUrl = process.env.API_BASE_URL; - const apiKey = process.env.API_KEY; - - if (!apiBaseUrl || !apiKey) { - return res.status(500).json({ error: 'Server configuration error' }); - } - - // TODO new env variable for platform apis or create api for cms only - const host = apiBaseUrl.split('/api/')[0]; - const url = `${host}/api/platform/v1/mentees`; - try { - const response = await fetch(url, { + const data = await proxyRequest('mentees', { method: 'POST', - headers: { - 'X-API-KEY': apiKey, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(req.body), - }); - - const data = await response.json().catch(() => null); + data: req.body, + }, true); - if (!response.ok) { - return res - .status(response.status) - .json(data ?? { error: 'Registration failed. Please try again.' }); + return res.status(201).json(data ?? {}); + } catch (error: any) { + if (error.response) { + return res.status(error.response.status).json(error.response.data); + } + if (error.message === 'Server configuration error') { + return res.status(500).json({ error: error.message }); } - - return res.status(response.status).json(data ?? {}); - } catch (error) { - logger.error('Mentee registration API error:', error); return res.status(500).json({ error: 'Internal server error' }); } } diff --git a/src/pages/api/mentor-registration.ts b/src/pages/api/mentor-registration.ts new file mode 100644 index 0000000..de03364 --- /dev/null +++ b/src/pages/api/mentor-registration.ts @@ -0,0 +1,112 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +import { MentorRegistrationData } from 'schemas/mentorSchema'; + +import { proxyRequest } from '../../lib/api'; +import { COUNTRIES } from '../../utils/mentorshipConstants'; + +/** + * Mapping from frontend MentorRegistrationData to backend MentorDto structure. + */ +function mapToBackendMentorDto(data: MentorRegistrationData) { + // Map social networks + const network = []; + if (data.linkedin) network.push({ type: 'LINKEDIN', link: data.linkedin }); + if (data.github) network.push({ type: 'GITHUB', link: data.github }); + if (data.instagram) network.push({ type: 'INSTAGRAM', link: data.instagram }); + if (data.medium) network.push({ type: 'MEDIUM', link: data.medium }); + if (data.website) network.push({ type: 'WEBSITE', link: data.website }); + if (data.otherSocial) network.push({ type: 'DEFAULT_LINK', link: data.otherSocial }); + + // Map PronounCategory + const pronounCategory = data.identity === 'Woman' ? 'FEMININE' : 'UNSPECIFIED'; + + // Map MenteeSection + const longTerm = data.isLongTermMentor + ? { + numMentee: parseInt(data.maxMentees || '0', 10), + hours: parseInt(data.maxMentees || '0', 10) * 2, // Backend requires min 2 hours per mentee + } + : null; + + const adHoc = data.isAdHocMentor && data.adHocAvailability + ? Object.entries(data.adHocAvailability) + .filter(([_, availability]) => availability !== undefined && availability !== null && availability !== '') + .map(([month, availability]) => ({ + month: month.toUpperCase(), + hours: parseInt(availability as string, 10) || 0, + })) + : []; + + // Find country code from name + const countryObj = COUNTRIES.find((c) => c.name === data.country); + const countryCode = countryObj ? countryObj.code : data.country; + + return { + fullName: data.fullName, + position: data.position, + email: data.email, + slackDisplayName: data.slackDisplayName, + country: { + countryCode, + countryName: data.country, + }, + city: data.city, + companyName: data.companyName, + spokenLanguages: data.languages, + bio: data.bio, + pronouns: data.pronouns, + pronounCategory, + isWomen: data.identity === 'Woman', + calendlyLink: data.calendlyLink, + acceptMale: data.openToNonWomen, + acceptPromotion: data.socialHighlight === 'Yes', + skills: { + yearsExperience: parseInt(data.yearsExperience, 10), + areas: data.technicalAreas.map((area: any) => ({ + technicalArea: (area.technicalArea || area.name)?.toUpperCase()?.replace(/\s+/g, '_').replace('C++', 'C_PLUS_PLUS').replace('C#', 'C_SHARP'), + proficiencyLevel: (area.proficiencyLevel || area.proficiency)?.toUpperCase(), + })), + languages: data.codeLanguages.map((lang: any) => ({ + language: (lang.language || lang.name)?.toUpperCase()?.replace(/\s+/g, '_').replace('C++', 'C_PLUS_PLUS').replace('C#', 'C_SHARP'), + proficiencyLevel: (lang.proficiencyLevel || lang.proficiency)?.toUpperCase(), + })), + mentorshipFocus: data.mentorshipFocusAreas.map((focus) => focus.toUpperCase()), + }, + menteeSection: { + idealMentee: data.menteeExpectations, + additional: data.mentorshipFocus || '', + longTerm, + adHoc, + }, + network, + }; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== 'POST') { + res.setHeader('Allow', ['POST']); + return res.status(405).json({ error: `Method ${req.method} Not Allowed` }); + } + + try { + const backendPayload = mapToBackendMentorDto(req.body); + const data = await proxyRequest('mentors', { + method: 'POST', + data: backendPayload, + }, true); + + return res.status(201).json(data ?? {}); + } catch (error: any) { + if (error.response) { + return res.status(error.response.status).json(error.response.data); + } + if (error.message === 'Server configuration error') { + return res.status(500).json({ error: error.message }); + } + return res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/src/pages/api/mentors.ts b/src/pages/api/mentors.ts index 368f410..78a7bb7 100644 --- a/src/pages/api/mentors.ts +++ b/src/pages/api/mentors.ts @@ -1,6 +1,7 @@ -import { logger } from 'bs-logger'; import { NextApiRequest, NextApiResponse } from 'next'; +import { proxyRequest } from '../../lib/api'; + export default async function handler( req: NextApiRequest, res: NextApiResponse, @@ -11,14 +12,7 @@ export default async function handler( } try { const { keyword, yearsExperience, areas, language, focus } = req.query; - const baseUrl = process.env.API_BASE_URL; - const apiKey = process.env.API_KEY; - - if (!baseUrl || !apiKey) { - return res.status(500).json({ error: 'Server configuration error' }); - } - // Build query string from all available params const params = new URLSearchParams(); if (keyword) params.append('keyword', Array.isArray(keyword) ? keyword[0] : keyword); @@ -35,25 +29,19 @@ export default async function handler( ); if (focus) params.append('focus', Array.isArray(focus) ? focus[0] : focus); - let url = `${baseUrl}/mentorship/mentors`; - if (params.toString()) { - url += `?${params.toString()}`; - } - - const response = await fetch(url, { + const data = await proxyRequest('mentorship/mentors', { method: 'GET', - headers: { - 'X-API-KEY': apiKey, - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - }, - cache: 'no-store', + params, }); - const data = await response.json(); return res.status(200).json(data); - } catch (error) { - logger.error('Mentors API error:', error); + } catch (error: any) { + if (error.response) { + return res.status(error.response.status).json(error.response.data); + } + if (error.message === 'Server configuration error') { + return res.status(500).json({ error: error.message }); + } return res.status(500).json({ error: 'Internal server error' }); } } diff --git a/src/pages/mentorship/mentor-registration.tsx b/src/pages/mentorship/mentor-registration.tsx index 2262b5a..ea3825f 100644 --- a/src/pages/mentorship/mentor-registration.tsx +++ b/src/pages/mentorship/mentor-registration.tsx @@ -6,9 +6,12 @@ import { Typography, Button, Stack, + Alert, + CircularProgress, useMediaQuery, useTheme, } from '@mui/material'; +import Link from 'next/link'; import React, { useState } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; @@ -20,6 +23,7 @@ import Step5Review from 'components/mentorship/Step5Review'; import { mentorRegistrationSchema, MentorRegistrationData, + mentorRegistrationDefaultValues, } from 'schemas/mentorSchema'; const validateStep1 = async (formMethods: any) => { @@ -86,6 +90,14 @@ const validateStep2 = async (formMethods: any) => { ]); }; +const validateStep3 = async (formMethods: any) => { + return await formMethods.trigger(['technicalAreas']); +}; + +const validateStep4 = async (formMethods: any) => { + return await formMethods.trigger(['codeLanguages', 'mentorshipFocusAreas']); +}; + const validateStep5 = async (formMethods: any) => { return await formMethods.trigger([ 'linkedin', @@ -100,12 +112,17 @@ const MentorRegistrationPage = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const formMethods = useForm({ + const formMethods = useForm({ resolver: zodResolver(mentorRegistrationSchema), + defaultValues: mentorRegistrationDefaultValues, mode: 'onChange', }); const [activeStep, setActiveStep] = useState(1); + const [submissionStatus, setSubmissionStatus] = useState< + 'idle' | 'loading' | 'success' | 'error' + >('idle'); + const [errorMessage, setErrorMessage] = useState(''); const totalSteps = 5; const handleNext = async () => { @@ -119,8 +136,10 @@ const MentorRegistrationPage = () => { isStepValid = await validateStep2(formMethods); break; case 3: + isStepValid = await validateStep3(formMethods); + break; case 4: - isStepValid = true; + isStepValid = await validateStep4(formMethods); break; case 5: isStepValid = await validateStep5(formMethods); @@ -139,10 +158,75 @@ const MentorRegistrationPage = () => { if (activeStep > 1) setActiveStep((prev) => prev - 1); }; - const onSubmit = (data: MentorRegistrationData) => { - console.log('Form Data Submitted:', data); + const onSubmit = async (data: MentorRegistrationData) => { + setSubmissionStatus('loading'); + setErrorMessage(''); + + try { + const response = await fetch('/api/mentor-registration', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || 'Failed to submit application'); + } + + setSubmissionStatus('success'); + window.scrollTo(0, 0); + } catch (error: any) { + setSubmissionStatus('error'); + setErrorMessage( + error.message || 'Something went wrong. Please try again.', + ); + window.scrollTo(0, 0); + } }; + const onInvalid = (_errors: any) => {}; + + if (submissionStatus === 'success') { + return ( + + + + Application Submitted! + + + Thank you for applying to be a mentor. Your application has been + received and is now being reviewed. We will get back to you soon. + + + + + + + ); + } + return ( { Step {activeStep} of {totalSteps} + {submissionStatus === 'error' && ( + + {errorMessage} + + )} + { + + {Object.keys(formMethods.formState.errors).length > 0 && ( + + + Please fix the following validation errors: + +
    + {Object.entries(formMethods.formState.errors).map( + ([key, error]: [string, any]) => { + const label = + { + fullName: 'Full Name', + email: 'Email', + slackDisplayName: 'Slack Name', + country: 'Country', + city: 'City', + position: 'Position', + companyName: 'Company Name', + calendlyLink: 'Calendly Link', + menteeExpectations: 'Mentee Expectations', + openToNonWomen: 'Open to non-women', + isLongTermMentor: 'Mentorship Format', + maxMentees: 'Max Mentees', + adHocAvailability: 'Ad-hoc Availability', + languages: 'Languages', + yearsExperience: 'Years of Experience', + bio: 'Bio', + technicalAreas: 'Technical Areas', + codeLanguages: 'Programming Languages', + mentorshipFocusAreas: 'Mentorship Focus Areas', + linkedin: 'LinkedIn', + identity: 'Identity', + pronouns: 'Pronouns', + socialHighlight: 'Social Highlight', + termsAgreed: 'Terms Agreement', + }[key] || key; + return ( +
  • + + {label}:{' '} + {error?.message || 'Invalid value'} + +
  • + ); + }, + )} +
+
+ )} +
+ {activeStep === totalSteps ? ( ) : ( diff --git a/src/schemas/__tests__/mentorSchema.test.ts b/src/schemas/__tests__/mentorSchema.test.ts index a6bf5b0..bff8e66 100644 --- a/src/schemas/__tests__/mentorSchema.test.ts +++ b/src/schemas/__tests__/mentorSchema.test.ts @@ -17,7 +17,9 @@ describe('mentorRegistrationSchema validation', () => { languages: ['English'], yearsExperience: '5', bio: 'A passionate developer with 5 years of experience.', - technicalAreas: [{ technicalArea: 'FRONTEND', proficiencyLevel: 'ADVANCED' }], + technicalAreas: [ + { technicalArea: 'FRONTEND', proficiencyLevel: 'ADVANCED' }, + ], codeLanguages: [{ language: 'TYPESCRIPT', proficiencyLevel: 'ADVANCED' }], mentorshipFocusAreas: ['GROW_MID_TO_SENIOR'], linkedin: 'https://linkedin.com/in/janedoe', @@ -69,7 +71,9 @@ describe('mentorRegistrationSchema validation', () => { const result = mentorRegistrationSchema.safeParse(data); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe('Please select availability for at least one month'); + expect(result.error.issues[0].message).toBe( + 'Please select availability for at least one month', + ); } }); diff --git a/src/schemas/mentorSchema.ts b/src/schemas/mentorSchema.ts index be94e19..6f78d2d 100644 --- a/src/schemas/mentorSchema.ts +++ b/src/schemas/mentorSchema.ts @@ -84,7 +84,11 @@ export type BasicInfoData = z.infer; export const profileSchema = z.object({ languages: z.array(z.string()).min(1, 'Please select at least one language'), - yearsExperience: z.string().min(1, 'Please select your years of experience'), + yearsExperience: z.coerce + .number() + .int('Years of experience must be a whole number') + .min(2, 'Minimum 2 years of experience required') + .max(50, 'Maximum 50 years of experience'), bio: z .string() .min(10, 'Please provide at least 10 characters for your bio') @@ -160,37 +164,38 @@ export const mentorRegistrationSchema = z export type MentorRegistrationData = z.infer; -export const mentorRegistrationDefaultValues: Partial = { - fullName: '', - email: '', - slackDisplayName: '', - country: '', - city: '', - position: '', - companyName: '', - isLongTermMentor: false, - isAdHocMentor: false, - maxMentees: '', - adHocAvailability: {}, - calendlyLink: '', - menteeExpectations: '', - openToNonWomen: false, - languages: [], - yearsExperience: '', - bio: '', - mentorshipFocus: '', - imageUrl: '', - technicalAreas: [], - codeLanguages: [], - mentorshipFocusAreas: [], - linkedin: '', - github: '', - instagram: '', - medium: '', - website: '', - otherSocial: '', - identity: '', - pronouns: '', - socialHighlight: '', - termsAgreed: false, -}; +export const mentorRegistrationDefaultValues: Partial = + { + fullName: '', + email: '', + slackDisplayName: '', + country: '', + city: '', + position: '', + companyName: '', + isLongTermMentor: false, + isAdHocMentor: false, + maxMentees: '', + adHocAvailability: {}, + calendlyLink: '', + menteeExpectations: '', + openToNonWomen: false, + languages: [], + + bio: '', + mentorshipFocus: '', + imageUrl: '', + technicalAreas: [], + codeLanguages: [], + mentorshipFocusAreas: [], + linkedin: '', + github: '', + instagram: '', + medium: '', + website: '', + otherSocial: '', + identity: '', + pronouns: '', + socialHighlight: '', + termsAgreed: false, + }; From b8a0ebf221fec84e67b107d89c820b77c8a96da7 Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Sun, 29 Mar 2026 19:04:19 +0200 Subject: [PATCH 5/5] fix: address PR #259 review comments and SonarCloud duplication warning - Extract repeated error-handling catch block into shared handleApiError helper in lib/api.ts, eliminating duplication across three API routes - Restore Cache-Control response header on GET /api/mentors to prevent stale mentor listings being served from CDN or Next.js cache - Replace hardcoded HOURS_PER_MENTEE inline constant with named module-level constant and explanatory comment - Remove any casts and dead fallbacks (area.name, lang.name, area.proficiency) from technicalAreas and codeLanguages mappings now that Zod schema defines the shape unambiguously - Simplify yearsExperience mapping: field is already a number after Zod coercion - Fix openToNonWomen pre-selected radio bug: replace z.enum+transform with z.boolean() and update RadioGroup onChange to propagate boolean values - Fix Resolver type mismatch caused by z.coerce.number() producing unknown input type in Zod v4; add explanatory cast comment - Update test fixtures: yearsExperience 5 as number, openToNonWomen as boolean --- src/__tests__/api/mentor-registration.test.ts | 2 +- src/components/mentorship/Step1BasicInfo.tsx | 2 +- src/lib/__tests__/api.test.ts | 14 +++--- src/lib/api.ts | 15 ++++++ src/lib/responses/landingPage.json | 28 +++++------ src/lib/responses/mentorship.json | 26 +++++------ src/pages/api/mentee-registration.ts | 12 ++--- src/pages/api/mentor-registration.ts | 46 ++++++++----------- src/pages/api/mentors.ts | 13 ++---- src/pages/mentorship/mentor-registration.tsx | 8 +++- src/schemas/__tests__/mentorSchema.test.ts | 2 +- src/schemas/mentorSchema.ts | 11 ++--- 12 files changed, 90 insertions(+), 89 deletions(-) diff --git a/src/__tests__/api/mentor-registration.test.ts b/src/__tests__/api/mentor-registration.test.ts index e97edeb..e672eb8 100644 --- a/src/__tests__/api/mentor-registration.test.ts +++ b/src/__tests__/api/mentor-registration.test.ts @@ -26,7 +26,7 @@ const makeReq = (overrides: Partial = {}): NextApiRequest => menteeExpectations: 'Eager to learn and proactive.', openToNonWomen: true, languages: ['English'], - yearsExperience: '5', + yearsExperience: 5, bio: 'A passionate developer with 5 years of experience.', technicalAreas: [ { technicalArea: 'Fullstack', proficiencyLevel: 'Advanced' }, diff --git a/src/components/mentorship/Step1BasicInfo.tsx b/src/components/mentorship/Step1BasicInfo.tsx index f0beb86..57ead8e 100644 --- a/src/components/mentorship/Step1BasicInfo.tsx +++ b/src/components/mentorship/Step1BasicInfo.tsx @@ -440,7 +440,7 @@ const Step1BasicInfo = () => { ? String(field.value) : '' } - onChange={(e) => field.onChange(e.target.value)} + onChange={(e) => field.onChange(e.target.value === 'true')} > { const mockAxios = jest.fn(); (mockAxios as any).create = jest.fn(() => mockAxios); @@ -10,23 +12,23 @@ jest.mock('axios', () => { }); describe('API Fetch Functions', () => { - let fetchFooter: () => Promise; - let fetchData: (_path: string) => Promise; + let fetchFooter: (typeof ApiModule)['fetchFooter']; + let fetchData: (typeof ApiModule)['fetchData']; let mockedAxios: jest.Mock; const originalEnv = process.env; - beforeEach(() => { + beforeEach(async () => { jest.resetModules(); process.env = { ...originalEnv, API_BASE_URL: 'http://localhost:8080/api/cms/v1', API_KEY: 'test-key', }; - // Re-require after setting env so module-level constants pick them up - const api = require('../api'); // eslint-disable-line @typescript-eslint/no-var-requires + // Re-import after setting env so module-level constants pick them up + const api = await import('../api'); fetchFooter = api.fetchFooter; fetchData = api.fetchData; - mockedAxios = require('axios').default; // eslint-disable-line @typescript-eslint/no-var-requires + mockedAxios = (await import('axios')).default as unknown as jest.Mock; }); afterEach(() => { diff --git a/src/lib/api.ts b/src/lib/api.ts index 9c9b4e2..50317f8 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,6 @@ import axios, { AxiosRequestConfig } from 'axios'; import { logger } from 'bs-logger'; +import { NextApiResponse } from 'next'; import aboutUsPage from './responses/aboutUs.json'; import aboutUsTeam from './responses/aboutUsTeam.json'; @@ -84,6 +85,20 @@ export const fetchData = async (path: string) => { } }; +export const handleApiError = (error: unknown, res: NextApiResponse) => { + const err = error as { + response?: { status: number; data: unknown }; + message?: string; + }; + if (err.response) { + return res.status(err.response.status).json(err.response.data); + } + if (err.message === 'Server configuration error') { + return res.status(500).json({ error: err.message }); + } + return res.status(500).json({ error: 'Internal server error' }); +}; + export const fetchFooter = async () => { try { return await proxyRequest('footer'); diff --git a/src/lib/responses/landingPage.json b/src/lib/responses/landingPage.json index 15c2a78..d6e18b5 100644 --- a/src/lib/responses/landingPage.json +++ b/src/lib/responses/landingPage.json @@ -184,28 +184,28 @@ "title": "What do our community members and supporters say about the WCC?", "feedbacks": [ { - "name": "Lucy Mentor", - "feedback": "It is great to be able to share my experience as a newbie in Tech with someone that has more years and experience in the industry. It has definitely made me feel more comfortable with been a completely beginner again and confident that, if a put the hours in, one day it will be pay off.", - "memberType": "Mentor", - "year": 2024 + "name": "Busra", + "feedback": "My session with my mentor exceeded my expectations. She's calm, caring, and incredibly knowledgeable. She offered valuable guidance not only on my career but also on life in general. My mentor truly embodies a growth mindset. I highly recommend her to any mentee seeking direction delivered with both compassion and wisdom.", + "memberType": "Mentee", + "year": 2025 }, { - "name": "Ana Mentee", - "feedback": "I am exciting with this mentorship program. This is really help me to pursue my goals. I got encouragement, insights, knowledge from my mentor that makes me back on the track and focus to what I have now, upgrade it, then reach out my goals.", + "name": "Zayna", + "feedback": "My mentor really took the time to comprehend my mental barriers and gave personalised structure advice to move past them. I appreciated the mentors vulnerability in explaining similar scenarios they had been in to give me a sense of guidance and belonging. Their kindness and warmth during the session has really helped me to progress in my career due to feeling safe to open up to them.", "memberType": "Mentee", - "year": 2024 + "year": 2025 }, { - "name": "Jane from SurrealDB", - "feedback": "Add text ...", - "memberType": "Partner", + "name": "Anonymous", + "feedback": "Just the simple act of presenting my profile and explaining my difficulties to a professional helped me structure the ideas and information in my head, and that alone was a big help. My mentor was caring, involved, and encouraging.", + "memberType": "Mentee", "year": 2025 }, { - "name": "JetBrains", - "feedback": "Add text ...", - "memberType": "Partner", - "year": 2025 + "name": "Hanna", + "feedback": "I am delighted to have had the opportunity to work with my mentor. Her explanations are always clear and thorough, and her enthusiasm and passion for our profession are truly contagious.\n Working with her has not only been educational but also incredibly motivating. Her guidance and support have given me a significant boost in my professional development. I am sincerely grateful to her for the valuable insights and encouragement she has provided.\nThank you!", + "memberType": "Mentee", + "year": 2024 } ] }, diff --git a/src/lib/responses/mentorship.json b/src/lib/responses/mentorship.json index c386bbd..67c93e9 100644 --- a/src/lib/responses/mentorship.json +++ b/src/lib/responses/mentorship.json @@ -39,26 +39,26 @@ "title": "What do participants think about our Mentorship Programme?", "feedbacks": [ { - "name": "Lucy", - "feedback": "It is great to be able to share my experience as a newbie in Tech with someone that has more years and experience in the industry. It has definitely made me feel more comfortable with been a completely beginner again and confident that, if a put the hours in, one day it will be pay off.", - "memberType": "Mentor", - "year": 2024 + "name": "Busra", + "feedback": "My session with my mentor exceeded my expectations. She's calm, caring, and incredibly knowledgeable. She offered valuable guidance not only on my career but also on life in general. My mentor truly embodies a growth mindset. I highly recommend her to any mentee seeking direction delivered with both compassion and wisdom.", + "memberType": "Mentee", + "year": 2025 }, { - "name": "Ana Smith", - "feedback": "I am exciting with this mentorship program. This is really help me to pursue my goals. I got encouragement, insights, knowledge from my mentor that makes me back on the track and focus to what I have now, upgrade it, then reach out my goals.", + "name": "Zayna", + "feedback": "My mentor really took the time to comprehend my mental barriers and gave personalised structure advice to move past them. I appreciated the mentors vulnerability in explaining similar scenarios they had been in to give me a sense of guidance and belonging. Their kindness and warmth during the session has really helped me to progress in my career due to feeling safe to open up to them.", "memberType": "Mentee", - "year": 2024 + "year": 2025 }, { - "name": "Jane", - "feedback": "My mentor has done an excellent job accommodating me and guiding me this year. This was my first experience as a mentee and it has taken time getting my footing. I entered this program because I felt isolated since I seemed to not have any clarity of my career and had a sense of general \"directionless\". Mentor has made me feel validated in my feeling and has helped me understand that my anxiety stems from not having a good understanding of the industry I hope to be a part of. Through our sessions and an exercise she gave me, I was gained a better perceptive on how exactly to direct my focus.", - "memberType": "Mentor", - "year": 2024 + "name": "Anonymous", + "feedback": "Just the simple act of presenting my profile and explaining my difficulties to a professional helped me structure the ideas and information in my head, and that alone was a big help. My mentor was caring, involved, and encouraging.", + "memberType": "Mentee", + "year": 2025 }, { - "name": "Anonymous", - "feedback": "It is an extremely helpful program and the mentor is quite open with matching my needs. She also actively encourages me with when and how to make job applications. She has helped with reviewing my CV and Linkedin profile. We have started work on my own personal project and collaboratively code on it, with the mentor asking questions to reinforce learning.", + "name": "Hanna", + "feedback": "I am delighted to have had the opportunity to work with my mentor. Her explanations are always clear and thorough, and her enthusiasm and passion for our profession are truly contagious.\n Working with her has not only been educational but also incredibly motivating. Her guidance and support have given me a significant boost in my professional development. I am sincerely grateful to her for the valuable insights and encouragement she has provided.\nThank you!", "memberType": "Mentee", "year": 2024 } diff --git a/src/pages/api/mentee-registration.ts b/src/pages/api/mentee-registration.ts index 980ae13..67184a4 100644 --- a/src/pages/api/mentee-registration.ts +++ b/src/pages/api/mentee-registration.ts @@ -1,6 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { proxyRequest } from '../../lib/api'; +import { handleApiError, proxyRequest } from '../../lib/api'; export default async function handler( req: NextApiRequest, @@ -22,13 +22,7 @@ export default async function handler( ); return res.status(201).json(data ?? {}); - } catch (error: any) { - if (error.response) { - return res.status(error.response.status).json(error.response.data); - } - if (error.message === 'Server configuration error') { - return res.status(500).json({ error: error.message }); - } - return res.status(500).json({ error: 'Internal server error' }); + } catch (error: unknown) { + return handleApiError(error, res); } } diff --git a/src/pages/api/mentor-registration.ts b/src/pages/api/mentor-registration.ts index 306fe1b..f7ed5b0 100644 --- a/src/pages/api/mentor-registration.ts +++ b/src/pages/api/mentor-registration.ts @@ -3,7 +3,10 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { COUNTRIES } from '@utils/mentorshipConstants'; import { MentorRegistrationData } from 'schemas/mentorSchema'; -import { proxyRequest } from '../../lib/api'; +import { handleApiError, proxyRequest } from '../../lib/api'; + +// Minimum hours per mentee for long-term mentorship to calculate total hours based on number of mentees. +const HOURS_PER_MENTEE = 2; /** * Mapping from frontend MentorRegistrationData to backend MentorDto structure. @@ -22,10 +25,11 @@ function mapToBackendMentorDto(data: MentorRegistrationData) { const pronounCategory = data.identity === 'Yes' ? 'FEMININE' : 'UNSPECIFIED'; // Map MenteeSection + const numMentee = parseInt(data.maxMentees || '0', 10); const longTerm = data.isLongTermMentor ? { - numMentee: parseInt(data.maxMentees || '0', 10), - hours: parseInt(data.maxMentees || '0', 10) * 2, // Backend requires total hours, e.g. 2h per mentee + numMentee, + hours: numMentee * HOURS_PER_MENTEE, } : null; @@ -85,26 +89,22 @@ function mapToBackendMentorDto(data: MentorRegistrationData) { acceptMale: data.openToNonWomen, acceptPromotion: data.socialHighlight === 'Yes', skills: { - yearsExperience: parseInt(data.yearsExperience as any as string, 10) || 0, - areas: data.technicalAreas.map((area: any) => ({ - technicalArea: (area.technicalArea || area.name) - ?.toUpperCase() - ?.replace(/\s+/g, '_') + yearsExperience: data.yearsExperience || 0, + areas: data.technicalAreas.map((area) => ({ + technicalArea: area.technicalArea + .toUpperCase() + .replace(/\s+/g, '_') .replace('C++', 'C_PLUS_PLUS') .replace('C#', 'C_SHARP'), - proficiencyLevel: ( - area.proficiencyLevel || area.proficiency - )?.toUpperCase(), + proficiencyLevel: area.proficiencyLevel.toUpperCase(), })), - languages: data.codeLanguages.map((lang: any) => ({ - language: (lang.language || lang.name) - ?.toUpperCase() - ?.replace(/\s+/g, '_') + languages: data.codeLanguages.map((lang) => ({ + language: lang.language + .toUpperCase() + .replace(/\s+/g, '_') .replace('C++', 'C_PLUS_PLUS') .replace('C#', 'C_SHARP'), - proficiencyLevel: ( - lang.proficiencyLevel || lang.proficiency - )?.toUpperCase(), + proficiencyLevel: lang.proficiencyLevel.toUpperCase(), })), mentorshipFocus: data.mentorshipFocusAreas.map((focus) => focus.toUpperCase(), @@ -141,13 +141,7 @@ export default async function handler( ); return res.status(201).json(data ?? {}); - } catch (error: any) { - if (error.response) { - return res.status(error.response.status).json(error.response.data); - } - if (error.message === 'Server configuration error') { - return res.status(500).json({ error: error.message }); - } - return res.status(500).json({ error: 'Internal server error' }); + } catch (error: unknown) { + return handleApiError(error, res); } } diff --git a/src/pages/api/mentors.ts b/src/pages/api/mentors.ts index 78a7bb7..ab1eba1 100644 --- a/src/pages/api/mentors.ts +++ b/src/pages/api/mentors.ts @@ -1,6 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { proxyRequest } from '../../lib/api'; +import { handleApiError, proxyRequest } from '../../lib/api'; export default async function handler( req: NextApiRequest, @@ -34,14 +34,9 @@ export default async function handler( params, }); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); return res.status(200).json(data); - } catch (error: any) { - if (error.response) { - return res.status(error.response.status).json(error.response.data); - } - if (error.message === 'Server configuration error') { - return res.status(500).json({ error: error.message }); - } - return res.status(500).json({ error: 'Internal server error' }); + } catch (error: unknown) { + return handleApiError(error, res); } } diff --git a/src/pages/mentorship/mentor-registration.tsx b/src/pages/mentorship/mentor-registration.tsx index 894640c..6b758ee 100644 --- a/src/pages/mentorship/mentor-registration.tsx +++ b/src/pages/mentorship/mentor-registration.tsx @@ -13,7 +13,7 @@ import { } from '@mui/material'; import Link from 'next/link'; import React, { useState } from 'react'; -import { useForm, FormProvider } from 'react-hook-form'; +import { useForm, FormProvider, type Resolver } from 'react-hook-form'; import Step1BasicInfo from 'components/mentorship/Step1BasicInfo'; import Step2Skills from 'components/mentorship/Step2Skills'; @@ -113,7 +113,11 @@ const MentorRegistrationPage = () => { const isMobile = useMediaQuery(theme.breakpoints.down('md')); const formMethods = useForm({ - resolver: zodResolver(mentorRegistrationSchema), + // Cast needed: z.coerce fields in Zod v4 produce `unknown` input types, + // causing a Resolver mismatch; the runtime output is MentorRegistrationData. + resolver: zodResolver( + mentorRegistrationSchema, + ) as Resolver, defaultValues: mentorRegistrationDefaultValues, mode: 'onChange', }); diff --git a/src/schemas/__tests__/mentorSchema.test.ts b/src/schemas/__tests__/mentorSchema.test.ts index bff8e66..9d680dc 100644 --- a/src/schemas/__tests__/mentorSchema.test.ts +++ b/src/schemas/__tests__/mentorSchema.test.ts @@ -13,7 +13,7 @@ describe('mentorRegistrationSchema validation', () => { maxMentees: '2', calendlyLink: 'https://calendly.com/janedoe', menteeExpectations: 'Eager to learn and proactive.', - openToNonWomen: 'true', + openToNonWomen: true, languages: ['English'], yearsExperience: '5', bio: 'A passionate developer with 5 years of experience.', diff --git a/src/schemas/mentorSchema.ts b/src/schemas/mentorSchema.ts index 6f78d2d..e4657aa 100644 --- a/src/schemas/mentorSchema.ts +++ b/src/schemas/mentorSchema.ts @@ -35,11 +35,9 @@ export const basicInfoObj = z.object({ 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'), + openToNonWomen: z.boolean({ + message: 'Please select an option', + }), }); const validateBasicInfo = ( @@ -179,9 +177,8 @@ export const mentorRegistrationDefaultValues: Partial = adHocAvailability: {}, calendlyLink: '', menteeExpectations: '', - openToNonWomen: false, languages: [], - + yearsExperience: 0, bio: '', mentorshipFocus: '', imageUrl: '',