From 43dc58e31ff248aa243d510462b854828df8c487 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 5 Dec 2025 22:06:42 -0500 Subject: [PATCH 01/10] Fixed job questionnaire data --- .../Collaboration/JobApplicationForm.css | 72 +++++++++++++++++++ .../Collaboration/JobApplicationForm.jsx | 69 +++++++++++++++++- .../Collaboration/JobApplicationPage.jsx | 29 ++++++-- src/utils/authInit.js | 21 ++++-- 4 files changed, 176 insertions(+), 15 deletions(-) diff --git a/src/components/Collaboration/JobApplicationForm.css b/src/components/Collaboration/JobApplicationForm.css index 5a0af1dd36..1a7459f4ab 100644 --- a/src/components/Collaboration/JobApplicationForm.css +++ b/src/components/Collaboration/JobApplicationForm.css @@ -62,6 +62,78 @@ html, body { margin: 0 0 16px; line-height: 1.5; } + +/* Requirements Section */ +.ja-requirements-section { + margin-bottom: 32px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border); + transition: border-color 0.3s ease; +} + +.ja-requirements-title { + font-size: 18px; + font-weight: 600; + margin: 0 0 16px; + color: var(--text); + transition: color 0.3s ease; +} + +.ja-requirements-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.ja-requirement-item { + display: flex; + align-items: flex-start; +} + +.ja-requirement-checkbox { + display: flex; + align-items: center; + gap: 10px; + cursor: default; + margin: 0; + font-size: 14px; + line-height: 1.5; +} + +.ja-requirement-checkbox-input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.ja-requirement-checkbox-custom { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + min-width: 20px; + border: 2px solid var(--border); + border-radius: 4px; + background-color: var(--bg); + transition: all 0.2s ease; + flex-shrink: 0; +} + +.ja-requirement-checkbox-custom.checked { + background-color: var(--primary); + border-color: var(--primary); +} + +.ja-requirement-checkbox-custom svg { + display: block; +} + +.ja-requirement-checkbox span:last-child { + color: var(--text); + transition: color 0.3s ease; +} + .ja-form{ background: var(--bg); border: 1px solid var(--border); diff --git a/src/components/Collaboration/JobApplicationForm.jsx b/src/components/Collaboration/JobApplicationForm.jsx index f46349c9b0..1e0f4078c1 100644 --- a/src/components/Collaboration/JobApplicationForm.jsx +++ b/src/components/Collaboration/JobApplicationForm.jsx @@ -1,9 +1,11 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import './JobApplicationForm.css'; export default function JobApplicationForm({ jobTitle = 'Job', jobDescription = '', + requirements = [], + referralToken = '', darkMode = false, }) { const [form, setForm] = useState({ @@ -34,6 +36,7 @@ export default function JobApplicationForm({ termsAccepted: false, }); const [errors, setErrors] = useState({}); + const [requirementStatus, setRequirementStatus] = useState({}); const degreeOptions = useMemo( () => ['High School / GED', 'Associate', "Bachelor's", "Master's", 'Ph.D.', 'Other'], @@ -41,6 +44,19 @@ export default function JobApplicationForm({ ); const resumeAcceptMimes = useMemo(() => ['application/pdf', 'image/jpeg', 'image/jpg'], []); + // Check requirement satisfaction (placeholder - can be enhanced with actual user data) + useEffect(() => { + if (requirements && requirements.length > 0) { + const status = {}; + requirements.forEach(req => { + // TODO: Replace with actual logic to check if user satisfies requirement + // For now, all are unchecked (user view - requirements not satisfied) + status[req] = false; + }); + setRequirementStatus(status); + } + }, [requirements]); + const update = (k, v) => setForm(f => ({ ...f, [k]: v })); const validate = () => { @@ -86,6 +102,57 @@ export default function JobApplicationForm({ onSubmit={submit} noValidate > + {/* Requirements Section */} + {requirements && requirements.length > 0 && ( +
+

+ Requirements: +

+
+ {requirements.map((requirement, index) => ( +
+ +
+ ))} +
+
+ )} +
s.theme.darkMode); const [loading, setLoading] = useState(true); - const [job, setJob] = useState({ title: 'Job', description: '' }); + const [job, setJob] = useState({ title: 'Job', description: '', requirements: [] }); useEffect(() => { let active = true; (async () => { setLoading(true); try { - let j = { title: 'Job', description: '' }; + let j = { title: 'Job', description: '', requirements: [] }; + + // Check if requirements were passed via location.state (from SuggestedJobsList) + const requirementsFromState = location.state?.requirements || []; + try { const res = await fetch(`${ApiEndpoint}/jobs/${jobId}`, { method: 'GET' }); if (res.ok) { const jd = await res.json(); const source = jd?.job || jd || {}; - j.title = source.title || 'Job'; - j.description = source.description || ''; + j.title = source.title || location.state?.jobTitle || 'Job'; + j.description = source.description || location.state?.jobDescription || ''; + j.requirements = source.requirements || requirementsFromState || []; + } else { + // Fallback to location.state if API fails + j.title = location.state?.jobTitle || 'Job'; + j.description = location.state?.jobDescription || ''; + j.requirements = requirementsFromState; } } catch { + // Use location.state as fallback j = { - title: 'Software Developer', + title: location.state?.jobTitle || 'Software Developer', description: + location.state?.jobDescription || 'We would like to have multiple contributors and are open to exploring win-win relationships with people of varying skill levels to help develop our Highest Good Network Application (HGNA). The app is built using the MERN stack...', + requirements: requirementsFromState, }; } if (active) { - setJob({ title: j.title, description: j.description }); + setJob({ title: j.title, description: j.description, requirements: j.requirements }); } } finally { if (active) setLoading(false); @@ -49,7 +62,7 @@ export default function JobApplicationPage() { return () => { active = false; }; - }, [jobId, referralToken]); + }, [jobId, referralToken, location.state]); if (loading) { return ( @@ -68,6 +81,8 @@ export default function JobApplicationPage() { diff --git a/src/utils/authInit.js b/src/utils/authInit.js index 9bf1f1d240..36090e9297 100644 --- a/src/utils/authInit.js +++ b/src/utils/authInit.js @@ -11,14 +11,21 @@ export default function initAuth() { const token = localStorage.getItem(config.tokenKey); if (!token) return; - const decoded = jwtDecode(token); - const nowSec = Date.now() / 1000; - const expirySec = new Date(decoded.expiryTimestamp).getTime() / 1000; + try { + const decoded = jwtDecode(token); + const nowSec = Date.now() / 1000; + const expirySec = new Date(decoded.expiryTimestamp).getTime() / 1000; - if (expirySec - TOKEN_LIFETIME_BUFFER < nowSec) { + if (expirySec - TOKEN_LIFETIME_BUFFER < nowSec) { + store.dispatch(logoutUser()); + } else { + httpService.setjwt(token); + store.dispatch(setCurrentUser(decoded)); + } + } catch (error) { + // Token is invalid, expired, or malformed - clear it and log out + console.warn('Invalid token detected, logging out user:', error.message); + localStorage.removeItem(config.tokenKey); store.dispatch(logoutUser()); - } else { - httpService.setjwt(token); - store.dispatch(setCurrentUser(decoded)); } } \ No newline at end of file From 342f30f933606ec876b3a6e65bd4a2fd2f07961d Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 5 Dec 2025 22:12:00 -0500 Subject: [PATCH 02/10] Fix linting errors: remove console/alert, fix test import, add eslint-disable comments --- config-overrides.js | 1 + src/components/Collaboration/JobApplicationForm.jsx | 4 ++-- .../PermissionsManagement/__tests__/UserRoleTab.test.jsx | 2 +- src/utils/authInit.js | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config-overrides.js b/config-overrides.js index afa2c65e4a..5642ee368a 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-extraneous-dependencies const webpack = require('webpack'); module.exports = function override(config) { diff --git a/src/components/Collaboration/JobApplicationForm.jsx b/src/components/Collaboration/JobApplicationForm.jsx index 1e0f4078c1..fbdeed0614 100644 --- a/src/components/Collaboration/JobApplicationForm.jsx +++ b/src/components/Collaboration/JobApplicationForm.jsx @@ -80,8 +80,8 @@ export default function JobApplicationForm({ ev.preventDefault(); const e = validate(); if (Object.keys(e).length) return; - console.log('Form OK →', form); - alert('Application validated (mock submit).'); + // TODO: Submit form data to backend + // Form validation passed - ready for submission }; return ( diff --git a/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx b/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx index d68a5a5170..9ee3a427fc 100644 --- a/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx +++ b/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx @@ -1,7 +1,7 @@ // eslint-disable-next-line no-unused-vars import React from 'react'; // eslint-disable-next-line no-unused-vars -import { render, screen, fireEvent, waitFor, act, wait } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import thunk from 'redux-thunk'; import mockAdminState from '__tests__/mockAdminState'; diff --git a/src/utils/authInit.js b/src/utils/authInit.js index 36090e9297..92ab0544fa 100644 --- a/src/utils/authInit.js +++ b/src/utils/authInit.js @@ -24,6 +24,7 @@ export default function initAuth() { } } catch (error) { // Token is invalid, expired, or malformed - clear it and log out + // eslint-disable-next-line no-console console.warn('Invalid token detected, logging out user:', error.message); localStorage.removeItem(config.tokenKey); store.dispatch(logoutUser()); From 0a95fae61b3792c4a155696250e1b80e31b20d1c Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 10 Dec 2025 23:05:43 -0500 Subject: [PATCH 03/10] Fix job application form and job listing page Fixes: 1. Fixed 'Apply Now' button to show application form instead of redirecting to external URL 2. Fixed job listings to load automatically on page load (removed requirement for search/category filter) 3. Fixed HTML tags displaying in job descriptions - added HTML stripping function 4. Fixed job card layout to match design: - Reduced icon size from 150px to 60px for compact display - Adjusted card padding and sizing for better proportions - Fixed font sizes for title and description - Made Apply Now button full-width at bottom 5. Fixed search section layout - vertical stacking in green navbar 6. Fixed job description on application page - strips HTML tags for clean display 7. Improved error handling in JobApplicationPage for better reliability 8. Matched styling with provided branch implementation while keeping improvements --- .../Collaboration/JobApplicationForm.jsx | 82 ++---------- .../Collaboration/JobApplicationPage.jsx | 28 ++--- .../Collaboration/SuggestedJobsList.css | 64 ++++++---- .../Collaboration/SuggestedJobsList.jsx | 118 ++++++++++-------- 4 files changed, 125 insertions(+), 167 deletions(-) diff --git a/src/components/Collaboration/JobApplicationForm.jsx b/src/components/Collaboration/JobApplicationForm.jsx index fbdeed0614..a410ea87cb 100644 --- a/src/components/Collaboration/JobApplicationForm.jsx +++ b/src/components/Collaboration/JobApplicationForm.jsx @@ -1,11 +1,9 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState } from 'react'; import './JobApplicationForm.css'; export default function JobApplicationForm({ jobTitle = 'Job', jobDescription = '', - requirements = [], - referralToken = '', darkMode = false, }) { const [form, setForm] = useState({ @@ -36,7 +34,6 @@ export default function JobApplicationForm({ termsAccepted: false, }); const [errors, setErrors] = useState({}); - const [requirementStatus, setRequirementStatus] = useState({}); const degreeOptions = useMemo( () => ['High School / GED', 'Associate', "Bachelor's", "Master's", 'Ph.D.', 'Other'], @@ -44,18 +41,14 @@ export default function JobApplicationForm({ ); const resumeAcceptMimes = useMemo(() => ['application/pdf', 'image/jpeg', 'image/jpg'], []); - // Check requirement satisfaction (placeholder - can be enhanced with actual user data) - useEffect(() => { - if (requirements && requirements.length > 0) { - const status = {}; - requirements.forEach(req => { - // TODO: Replace with actual logic to check if user satisfies requirement - // For now, all are unchecked (user view - requirements not satisfied) - status[req] = false; - }); - setRequirementStatus(status); - } - }, [requirements]); + // Helper function to strip HTML tags and clean text + const stripHtml = html => { + if (!html) return ''; + const doc = new DOMParser().parseFromString(html, 'text/html'); + const text = doc.body.textContent || doc.body.innerText || ''; + // Clean up extra whitespace and decode HTML entities + return text.replace(/\s+/g, ' ').trim(); + }; const update = (k, v) => setForm(f => ({ ...f, [k]: v })); @@ -80,8 +73,8 @@ export default function JobApplicationForm({ ev.preventDefault(); const e = validate(); if (Object.keys(e).length) return; - // TODO: Submit form data to backend - // Form validation passed - ready for submission + console.log('Form OK →', form); + alert('Application validated (mock submit).'); }; return ( @@ -92,7 +85,7 @@ export default function JobApplicationForm({ {jobDescription && (

- {jobDescription} + {stripHtml(jobDescription)}

)} @@ -102,57 +95,6 @@ export default function JobApplicationForm({ onSubmit={submit} noValidate > - {/* Requirements Section */} - {requirements && requirements.length > 0 && ( -
-

- Requirements: -

-
- {requirements.map((requirement, index) => ( -
- -
- ))} -
-
- )} -
s.theme.darkMode); const [loading, setLoading] = useState(true); - const [job, setJob] = useState({ title: 'Job', description: '', requirements: [] }); + const [job, setJob] = useState({ title: 'Job', description: '' }); useEffect(() => { let active = true; (async () => { setLoading(true); try { - let j = { title: 'Job', description: '', requirements: [] }; - - // Check if requirements were passed via location.state (from SuggestedJobsList) - const requirementsFromState = location.state?.requirements || []; + let j = { title: 'Job', description: '' }; try { const res = await fetch(`${ApiEndpoint}/jobs/${jobId}`, { method: 'GET' }); if (res.ok) { const jd = await res.json(); const source = jd?.job || jd || {}; - j.title = source.title || location.state?.jobTitle || 'Job'; - j.description = source.description || location.state?.jobDescription || ''; - j.requirements = source.requirements || requirementsFromState || []; - } else { - // Fallback to location.state if API fails - j.title = location.state?.jobTitle || 'Job'; - j.description = location.state?.jobDescription || ''; - j.requirements = requirementsFromState; + j.title = source.title || 'Job'; + j.description = source.description || ''; } } catch { - // Use location.state as fallback j = { - title: location.state?.jobTitle || 'Software Developer', + title: 'Software Developer', description: - location.state?.jobDescription || 'We would like to have multiple contributors and are open to exploring win-win relationships with people of varying skill levels to help develop our Highest Good Network Application (HGNA). The app is built using the MERN stack...', - requirements: requirementsFromState, }; } if (active) { - setJob({ title: j.title, description: j.description, requirements: j.requirements }); + setJob({ title: j.title, description: j.description }); } } finally { if (active) setLoading(false); @@ -62,7 +50,7 @@ export default function JobApplicationPage() { return () => { active = false; }; - }, [jobId, referralToken, location.state]); + }, [jobId, referralToken]); if (loading) { return ( @@ -81,8 +69,6 @@ export default function JobApplicationPage() { diff --git a/src/components/Collaboration/SuggestedJobsList.css b/src/components/Collaboration/SuggestedJobsList.css index d08a28ad7c..b6834ce72f 100644 --- a/src/components/Collaboration/SuggestedJobsList.css +++ b/src/components/Collaboration/SuggestedJobsList.css @@ -160,16 +160,17 @@ input.dark-mode-placeholder::placeholder { .job-ad { background: #fff; border: 1px solid #ccc; - padding: 30px 25px; /* Increased padding for bigger box */ + padding: 20px; border-radius: 12px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; justify-content: space-between; - flex: 0 1 350px; /* Bigger min width */ - /* Max width to keep uniform size */ + flex: 0 1 300px; + min-width: 280px; + max-width: 350px; transition: transform 0.3s, box-shadow 0.3s; - margin-bottom: 30px; + margin-bottom: 0; } .job-ad:hover { @@ -178,19 +179,20 @@ input.dark-mode-placeholder::placeholder { } .job-role-name { - font-size: 2rem; + font-size: 1.25rem; font-weight: bold; - margin-bottom: 1rem; + margin-bottom: 0.75rem; text-align: center; color: #0069d9; } .job-details { - font-size: 1.1rem; + font-size: 0.95rem; color: #555; - line-height: 1.6; - margin-bottom: 1.5rem; + line-height: 1.5; + margin-bottom: 1rem; text-align: center; + flex-grow: 1; } .job-requirements h4 { @@ -211,14 +213,16 @@ input.dark-mode-placeholder::placeholder { .apply-now-btn { background-color: #007bff; color: white; - padding: 0.75rem 1.5rem; + padding: 0.5rem 1rem; border: none; cursor: pointer; - font-size: 1.1rem; - border-radius: 6px; + font-size: 1rem; + border-radius: 4px; text-align: center; - align-self: center; /* Center button */ - width: fit-content; + text-decoration: none; + display: inline-block; + width: 100%; + margin-top: auto; transition: background-color 0.3s; } @@ -230,13 +234,15 @@ input.dark-mode-placeholder::placeholder { display: flex; flex-wrap: wrap; justify-content: center; - gap: 30px; /* More gap for bigger cards */ + gap: 20px; margin: 0 auto; + max-width: 1200px; + padding: 0 20px; } .category-icon { - width: 150px; - height: 150px; + width: 60px; + height: 60px; object-fit: contain; display: block; margin: 0 auto 15px; @@ -244,8 +250,18 @@ input.dark-mode-placeholder::placeholder { @media (max-width: 600px) { .category-icon { - width: 100px; - height: 100px; + width: 50px; + height: 50px; + } +} + +@media (max-width: 600px) { + .job-ad { + flex: 0 1 100%; + max-width: 100%; + } + .job-navbar { + padding: 15px; } } @@ -254,6 +270,12 @@ input.dark-mode-placeholder::placeholder { justify-content: center; } .job-ad { - flex: 0 1 400px; + flex: 0 1 300px; + } +} + +@media screen and (min-width: 1024px) { + .job-ad { + flex: 0 1 320px; } } diff --git a/src/components/Collaboration/SuggestedJobsList.jsx b/src/components/Collaboration/SuggestedJobsList.jsx index e8b8fbef53..993a382a2c 100644 --- a/src/components/Collaboration/SuggestedJobsList.jsx +++ b/src/components/Collaboration/SuggestedJobsList.jsx @@ -16,6 +16,25 @@ function SuggestedJobsList() { const [hasSearched, setHasSearched] = useState(false); const adsPerPage = 3; const darkMode = useSelector(state => state.theme.darkMode); + + // Helper function to strip HTML tags and truncate text + const stripHtmlAndTruncate = (html, maxLength = 150) => { + if (!html) return 'No detailed description available.'; + + // Create a temporary DOM element to parse HTML + const doc = new DOMParser().parseFromString(html, 'text/html'); + const text = doc.body.textContent || doc.body.innerText || ''; + + // Clean up extra whitespace + const cleaned = text.replace(/\s+/g, ' ').trim(); + + // Truncate if needed + if (cleaned.length > maxLength) { + return cleaned.substring(0, maxLength) + '...'; + } + + return cleaned || 'No detailed description available.'; + }; // Fetch categories on mount useEffect(() => { const fetchCategories = async () => { @@ -34,24 +53,25 @@ function SuggestedJobsList() { // Fetch job ads whenever query, category or page changes useEffect(() => { - if (!query && !category) { - setJobAds([]); // Clear jobs if no filters selected - setTotalPages(0); - return; // Skip fetching - } - const fetchJobAds = async () => { try { const url = `${ApiEndpoint}/jobs?page=${currentPage}&limit=${adsPerPage}&search=${encodeURIComponent( - query, - )}&category=${encodeURIComponent(category)}`; + query || '', + )}&category=${encodeURIComponent(category || '')}`; const response = await fetch(url, { method: 'GET' }); if (!response.ok) throw new Error(`Failed to fetch jobs: ${response.statusText}`); const data = await response.json(); - setJobAds(data.jobs); - setTotalPages(data.pagination.totalPages); + setJobAds(data.jobs || []); + setTotalPages(data.pagination?.totalPages || 0); + // If we have jobs, mark as searched + if (data.jobs && data.jobs.length > 0) { + setHasSearched(true); + } } catch (error) { + console.error('Error fetching jobs:', error); toast.error('Error fetching jobs'); + setJobAds([]); + setTotalPages(0); } }; @@ -72,13 +92,7 @@ function SuggestedJobsList() { const selectedValue = e.target.value; setCategory(selectedValue); setCurrentPage(1); // Reset page to 1 on category change - - // 👇 Reset hasSearched based on input - if (selectedValue === '' && query.trim() === '') { - setHasSearched(false); - } else { - setHasSearched(true); - } + setHasSearched(true); // Mark as searched when category is selected }; // Pagination controls @@ -161,21 +175,10 @@ function SuggestedJobsList() {
{`${ad.category}

@@ -204,20 +207,9 @@ function SuggestedJobsList() {

- {ad.description || 'No detailed description available.'} + {stripHtmlAndTruncate(ad.description)}

- {ad.requirements && ad.requirements.length > 0 && ( -
-

Requirements:

-
    - {ad.requirements.map(req => ( -
  • {req}
  • - ))} -
-
- )} -
- {['Engineering', 'Marketing', 'Design', 'Finance'].map(cat => ( - - ))} + {categories.length > 0 + ? categories.slice(0, 4).map(cat => ( + + )) + : ['Engineering', 'Marketing', 'Design', 'Finance'].map(cat => ( + + ))}
)} From 38fb147f9db8702f64bc24c1abc7bbc7d16f6c65 Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 13 Dec 2025 18:45:59 -0500 Subject: [PATCH 04/10] Update JobApplicationForm: Add admin view with requirements checkboxes, update styling to match design - Add isAdmin prop and requirements checking logic - Create RequirementsSection component with two-column layout - Update button styling to green (#69B369) scoped to component only - Update header styling (light gray background, green title text) - Add admin view detection in JobApplicationPage - Add padding below form for better spacing --- .../Collaboration/JobApplicationForm.css | 111 +++++++++++++--- .../Collaboration/JobApplicationForm.jsx | 119 ++++++++++++++++-- .../Collaboration/JobApplicationPage.jsx | 3 + 3 files changed, 209 insertions(+), 24 deletions(-) diff --git a/src/components/Collaboration/JobApplicationForm.css b/src/components/Collaboration/JobApplicationForm.css index 1a7459f4ab..a8407808fe 100644 --- a/src/components/Collaboration/JobApplicationForm.css +++ b/src/components/Collaboration/JobApplicationForm.css @@ -8,9 +8,10 @@ --muted: #6b7280; --bg: #fff; --bg-soft: #f8fafc; - --primary:#007bff; + --primary:#8cc63f; --danger:#b91c1c; - --focus:#2563eb; + --focus:#6fa832; + --green-header: #8cc63f; } * { box-sizing: border-box; @@ -40,14 +41,36 @@ html, body { .ja-wrap{ max-width: var(--ja-max); margin: 0 auto; - padding: 24px var(--pad); + padding: 0 0 48px 0; color: var(--text); transition: background-color 0.3s ease, color 0.3s ease; + background: #f5f5f5; + min-height: 100vh; } .ja-header{ - margin-bottom: 24px; + margin-bottom: 0; text-align: center; + background: #f5f5f5; + padding: 16px var(--pad); + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.ja-header .ja-title { + margin-bottom: 8px; + color: #000; + font-weight: 700; +} + +.ja-header .ja-desc { + color: #000; + background: #f5f5f5; + margin: 12px auto 0; + max-width: 90%; + text-align: left; + line-height: 1.6; } .ja-title{ @@ -80,8 +103,8 @@ html, body { } .ja-requirements-list { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 1fr 1fr; gap: 12px; } @@ -139,6 +162,9 @@ html, body { border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; + padding-bottom: 48px; + margin: 24px var(--pad); + margin-bottom: 0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; } @@ -232,6 +258,40 @@ textarea:focus { align-items: center; gap: 10px; cursor: pointer; +} + +.ja-checkbox input[type="checkbox"] { + width: 20px; + height: 20px; + min-width: 20px; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + border: 1px solid var(--border); + border-radius: 4px; + background-color: var(--bg); + cursor: pointer; + position: relative; + margin: 0; + padding: 0; + flex-shrink: 0; +} + +.ja-checkbox input[type="checkbox"]:checked { + background-color: var(--primary); + border-color: var(--primary); +} + +.ja-checkbox input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #fff; + font-size: 14px; + font-weight: bold; + line-height: 1; } .ja-consent{ @@ -258,27 +318,32 @@ textarea:focus { transition: border-color 0.3s ease; } -.btn-primary{ - background: var(--primary); - color: #fff; - border: none; - border-radius: 8px; +.ja-actions .btn-primary { + background: #69B369 !important; + color: #fff !important; + border: 1px solid #5A945A !important; + border-radius: 12px; padding: 12px 24px; font-weight: 600; font-size: 16px; cursor: pointer; transition: all 0.2s; min-width: 200px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -.btn-primary:hover{ - background: #0056b3; +.ja-actions .btn-primary:hover { + background: #5A945A !important; + color: #fff !important; transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } -.btn-primary:active{ +.ja-actions .btn-primary:active { transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background: #5A945A !important; + color: #fff !important; } .ja-loading{ @@ -335,14 +400,19 @@ textarea:focus { } .ja-wrap { + padding: 0; + } + + .ja-header { padding: 16px 12px; } .ja-form { padding: 20px 16px; + margin: 24px 12px; } - .ja-reqs-grid { + .ja-requirements-list { grid-template-columns: 1fr; } @@ -366,6 +436,15 @@ textarea:focus { } .ja-wrap { + padding: 0; + } + + .ja-header { padding: 12px 8px; } + + .ja-form { + margin: 16px 8px; + padding: 16px 12px; + } } diff --git a/src/components/Collaboration/JobApplicationForm.jsx b/src/components/Collaboration/JobApplicationForm.jsx index a410ea87cb..24e5d584b6 100644 --- a/src/components/Collaboration/JobApplicationForm.jsx +++ b/src/components/Collaboration/JobApplicationForm.jsx @@ -5,6 +5,7 @@ export default function JobApplicationForm({ jobTitle = 'Job', jobDescription = '', darkMode = false, + isAdmin = false, }) { const [form, setForm] = useState({ firstName: '', @@ -52,6 +53,39 @@ export default function JobApplicationForm({ const update = (k, v) => setForm(f => ({ ...f, [k]: v })); + // Check if requirements are satisfied (for admin view) + const checkRequirements = () => { + const requirements = { + reactExperience: false, + twoMonthsCommitment: false, + javascriptExperience: false, + timeZoneLocation: false, + tenHoursPerWeek: false, + }; + + // Check 1+ years of Full-Time ReactJS Experience + const reactKeywords = ['react', 'reactjs', 'react.js']; + const roleSkillsLower = (form.roleSkills || '').toLowerCase(); + requirements.reactExperience = + (form.fullTimeYears && parseFloat(form.fullTimeYears) >= 1) || + reactKeywords.some(keyword => roleSkillsLower.includes(keyword)); + + // Check Minimum of 2 Months Commitment + requirements.twoMonthsCommitment = + form.monthsVolunteer && parseFloat(form.monthsVolunteer) >= 2; + + // Check 1+ years of Full-Time JavaScript Experience + requirements.javascriptExperience = form.fullTimeYears && parseFloat(form.fullTimeYears) >= 1; + + // Check Time Zone and Location Matches + requirements.timeZoneLocation = !!(form.locationTz && form.locationTz.trim()); + + // Check Minimum of 10 hours of work a week + requirements.tenHoursPerWeek = form.hoursPerWeek && parseFloat(form.hoursPerWeek) >= 10; + + return requirements; + }; + const validate = () => { const e = {}; if (!form.firstName.trim()) e.firstName = 'First name is required'; @@ -80,14 +114,8 @@ export default function JobApplicationForm({ return (
-

- Job Application – {jobTitle} -

- {jobDescription && ( -

- {stripHtml(jobDescription)} -

- )} +

Job Application – {jobTitle}

+ {jobDescription &&

{stripHtml(jobDescription)}

}
+ {/* Requirements Section - Shows checkboxes only in admin view */} + {isAdmin && } +
+

Requirements Status

+
+ {requirementList.map(req => ( +
+ +
+ ))} +
+
+ ); +} + /* Helper functions */ function Field({ id, diff --git a/src/components/Collaboration/JobApplicationPage.jsx b/src/components/Collaboration/JobApplicationPage.jsx index 2952e13452..b338c6b769 100644 --- a/src/components/Collaboration/JobApplicationPage.jsx +++ b/src/components/Collaboration/JobApplicationPage.jsx @@ -13,6 +13,8 @@ export default function JobApplicationPage() { const referralToken = qs.get('token') || ''; const darkMode = useSelector(s => s.theme.darkMode); + const userRole = useSelector(s => s.auth?.user?.role); + const isAdmin = userRole === 'Administrator' || userRole === 'Owner'; const [loading, setLoading] = useState(true); const [job, setJob] = useState({ title: 'Job', description: '' }); @@ -70,6 +72,7 @@ export default function JobApplicationPage() { jobTitle={job.title} jobDescription={job.description} darkMode={darkMode} + isAdmin={isAdmin} /> {/* Bottom banner (full width) */} From 8bb6747d715deee4696dc2617334941ad629405f Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 18 Dec 2025 22:37:04 -0500 Subject: [PATCH 05/10] Fix linting errors and accessibility issues --- .../Collaboration/Collaboration.jsx | 41 +++++++++++++------ .../JobApplicationForm/JobApplicationForm.jsx | 34 ++++++++++----- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/components/Collaboration/Collaboration.jsx b/src/components/Collaboration/Collaboration.jsx index f9ac1ac5c9..0fe9008936 100644 --- a/src/components/Collaboration/Collaboration.jsx +++ b/src/components/Collaboration/Collaboration.jsx @@ -245,11 +245,7 @@ function Collaboration() { - - @@ -384,8 +376,7 @@ function Collaboration() { const jobTitle = ad.title || 'Untitled Position'; const jobCategory = ad.category || 'General'; const jobImageUrl = - ad.imageUrl || - `/api/placeholder/640/480?text=${encodeURIComponent(jobCategory)}`; + ad.imageUrl || `/api/placeholder/640/480?text=${encodeURIComponent(jobCategory)}`; return (
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + try { + if (history && typeof history.push === 'function') { + history.push({ + pathname: '/job-application', + state: { + jobId: ad._id, + jobTitle: jobTitle, + jobDescription: ad.description || '', + requirements: Array.isArray(ad.requirements) ? ad.requirements : [], + category: jobCategory, + }, + }); + } else { + window.location.href = `/job-application`; + } + } catch (error) { + console.error('Error navigating to job application:', error); + toast.error('Error opening job application'); + } + } + }} + role="button" + tabIndex={0} style={{ cursor: 'pointer' }} > f.title?.toLowerCase() === jobDataFromRedirect.jobTitle?.toLowerCase() + f => f.title?.toLowerCase() === jobDataFromRedirect.jobTitle?.toLowerCase(), ); if (matchedForm) { setSelectedJob(matchedForm.title); @@ -78,7 +78,7 @@ function JobApplicationForm() { return; } } - + const firstWithQuestions = formsArr.find(f => f.questions && f.questions.length > 0); if (firstWithQuestions) { setSelectedJob(firstWithQuestions.title); @@ -162,8 +162,7 @@ function JobApplicationForm() { reactKeywords.some(keyword => roleSkillsLower.includes(keyword)); // Check Minimum of 2 Months Commitment - requirements.twoMonthsCommitment = - monthsVolunteer && parseFloat(monthsVolunteer) >= 2; + requirements.twoMonthsCommitment = monthsVolunteer && parseFloat(monthsVolunteer) >= 2; // Check 1+ years of Full-Time JavaScript Experience requirements.javascriptExperience = fullTimeYears && parseFloat(fullTimeYears) >= 1; @@ -185,7 +184,10 @@ function JobApplicationForm() { const text = doc.body.textContent || doc.body.innerText || ''; return text.replace(/\s+/g, ' ').trim(); } catch (error) { - return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); + return html + .replace(/<[^>]*>/g, '') + .replace(/\s+/g, ' ') + .trim(); } }; @@ -266,7 +268,9 @@ function JobApplicationForm() {
-

Job Application – {selectedJob || 'General Position'}

+

+ Job Application – {selectedJob || 'General Position'} +

{(jobDataFromRedirect?.jobDescription || filteredForm?.description) && (

{stripHtml(jobDataFromRedirect?.jobDescription || filteredForm?.description || '')} @@ -291,8 +295,10 @@ function JobApplicationForm() { )}

{/* Requirements Section - Shows checkboxes only in admin view */} - {isAdmin && } - + {isAdmin && ( + + )} +
Here is a questionnaire to apply to work with us. To complete your application and schedule a Zoom interview, please answer the pre-interview questions below. @@ -385,7 +391,9 @@ function JobApplicationForm() { />
-

6. For how long do you wish to volunteer with us? (Enter your answer in months)

+

+ 6. For how long do you wish to volunteer with us? (Enter your answer in months) +

- + {req.satisfied && ( Date: Fri, 19 Dec 2025 15:08:30 -0500 Subject: [PATCH 06/10] Update job listings and application form: Add category-based view with proper images, referral link support, and user requirements section --- .../Collaboration/Collaboration.jsx | 280 +++++++++++++----- .../Collaboration/Collaboration.module.css | 120 ++++---- .../JobApplicationForm/JobApplicationForm.jsx | 189 +++++++++++- .../JobApplicationForm.module.css | 25 +- 4 files changed, 473 insertions(+), 141 deletions(-) diff --git a/src/components/Collaboration/Collaboration.jsx b/src/components/Collaboration/Collaboration.jsx index 0fe9008936..cc442e9d81 100644 --- a/src/components/Collaboration/Collaboration.jsx +++ b/src/components/Collaboration/Collaboration.jsx @@ -58,6 +58,97 @@ function Collaboration() { return columns * rows; }; + // Get category-specific image - using high-quality relevant images + const getCategoryImage = category => { + const categoryLower = (category || 'General').toLowerCase(); + + // High-quality, relevant images for each job category + const categoryImages = { + // Software & IT - Modern laptop/technology workspace + software: + 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', + it: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', + programming: + 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', + + // Engineering & Technical Design - Blueprint/technical drawing + engineering: + 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', + technical: + 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', + design: + 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', + + // Administrative & Support - Office workspace + administrative: + 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', + support: + 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', + admin: + 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', + + // Electric Engineer - Electrical work/panel + electric: + 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=640&h=480&fit=crop&q=80', + electrical: + 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=640&h=480&fit=crop&q=80', + + // Plumbing Engineer - Plumbing work + plumbing: + 'https://images.unsplash.com/photo-1621905252507-b35492cc74b4?w=640&h=480&fit=crop&q=80', + + // Culinary Chef - Professional kitchen/cooking + culinary: + 'https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=640&h=480&fit=crop&q=80', + chef: 'https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=640&h=480&fit=crop&q=80', + + // Civil Engineer - Construction site + civil: + 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=640&h=480&fit=crop&q=80', + construction: + 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=640&h=480&fit=crop&q=80', + + // Nutritionist - Healthy food/wellness + nutrition: + 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=640&h=480&fit=crop&q=80', + diet: + 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=640&h=480&fit=crop&q=80', + + // Mechanical Engineer - Mechanical/industrial work + mechanical: + 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=640&h=480&fit=crop&q=80', + }; + + // Try to find matching category + for (const [key, imageUrl] of Object.entries(categoryImages)) { + if (categoryLower.includes(key)) { + return imageUrl; + } + } + + // Default General category - Professional workspace + return 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=640&h=480&fit=crop&q=80'; + }; + + // Group jobs by category + const getUniqueCategories = () => { + const categoryMap = new Map(); + jobAds.forEach(ad => { + if (ad && ad.category) { + const cat = ad.category; + if (!categoryMap.has(cat)) { + categoryMap.set(cat, { + category: cat, + count: 0, + firstJob: ad, + }); + } + categoryMap.get(cat).count++; + } + }); + return Array.from(categoryMap.values()); + }; + const fetchJobAds = async () => { const adsPerPage = calculateAdsPerPage(); @@ -336,7 +427,7 @@ function Collaboration() {
@@ -354,7 +445,7 @@ function Collaboration() {
+ + {req.satisfied && ( + + + + )} + + {req.label} + +
+ ))} +
+
+ ); +} + export default JobApplicationForm; diff --git a/src/components/Collaboration/JobApplicationForm/JobApplicationForm.module.css b/src/components/Collaboration/JobApplicationForm/JobApplicationForm.module.css index 768cf02f78..4bf21f3532 100644 --- a/src/components/Collaboration/JobApplicationForm/JobApplicationForm.module.css +++ b/src/components/Collaboration/JobApplicationForm/JobApplicationForm.module.css @@ -93,11 +93,12 @@ } .jaTitle { - font-size: 1.8em; - font-weight: bold; + font-size: 2.5em; + font-weight: 700; margin-bottom: 15px; text-align: left; color: #333; + line-height: 1.2; } .jaDesc { @@ -444,6 +445,19 @@ min-height: 50px; } +/* --- User Requirements Section --- */ +.userRequirementsSection { + margin: 20px 0; + padding: 15px; + border-top: 2px solid #98cb03; + border-bottom: 2px solid #98cb03; + background-color: #f0f0f0; + border-radius: 8px; + display: block; + visibility: visible; + min-height: 50px; +} + .requirementsTitle { font-size: 1.1rem; font-weight: 600; @@ -527,6 +541,13 @@ border-bottom-color: #98cb03; } +/* Dark mode for user requirements */ +.darkMode .userRequirementsSection { + background-color: #1c2541; + border-top-color: #98cb03; + border-bottom-color: #98cb03; +} + .darkMode .requirementsTitle { color: #e0e0e0; } From 571ee1d7f1518335090c49e95dad122e2061d4ae Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 20 Dec 2025 21:40:53 -0500 Subject: [PATCH 07/10] Fix SonarCloud issues: Reduce code duplication and add input validation for security --- .../Collaboration/Collaboration.jsx | 113 ++++++++--------- .../JobApplicationForm/JobApplicationForm.jsx | 120 ++++++++---------- 2 files changed, 105 insertions(+), 128 deletions(-) diff --git a/src/components/Collaboration/Collaboration.jsx b/src/components/Collaboration/Collaboration.jsx index cc442e9d81..7396545369 100644 --- a/src/components/Collaboration/Collaboration.jsx +++ b/src/components/Collaboration/Collaboration.jsx @@ -62,67 +62,58 @@ function Collaboration() { const getCategoryImage = category => { const categoryLower = (category || 'General').toLowerCase(); - // High-quality, relevant images for each job category - const categoryImages = { - // Software & IT - Modern laptop/technology workspace - software: - 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', - it: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', - programming: - 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', - - // Engineering & Technical Design - Blueprint/technical drawing - engineering: - 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', - technical: - 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', - design: - 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', - - // Administrative & Support - Office workspace - administrative: - 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', - support: - 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', - admin: - 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', - - // Electric Engineer - Electrical work/panel - electric: - 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=640&h=480&fit=crop&q=80', - electrical: - 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=640&h=480&fit=crop&q=80', - - // Plumbing Engineer - Plumbing work - plumbing: - 'https://images.unsplash.com/photo-1621905252507-b35492cc74b4?w=640&h=480&fit=crop&q=80', - - // Culinary Chef - Professional kitchen/cooking - culinary: - 'https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=640&h=480&fit=crop&q=80', - chef: 'https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=640&h=480&fit=crop&q=80', - - // Civil Engineer - Construction site - civil: - 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=640&h=480&fit=crop&q=80', - construction: - 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=640&h=480&fit=crop&q=80', - - // Nutritionist - Healthy food/wellness - nutrition: - 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=640&h=480&fit=crop&q=80', - diet: - 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=640&h=480&fit=crop&q=80', - - // Mechanical Engineer - Mechanical/industrial work - mechanical: - 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=640&h=480&fit=crop&q=80', - }; - - // Try to find matching category - for (const [key, imageUrl] of Object.entries(categoryImages)) { - if (categoryLower.includes(key)) { - return imageUrl; + // Category to image URL mapping (grouped by image to reduce duplication) + const categoryImageMap = [ + { + keywords: ['software', 'it', 'programming'], + url: + 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['engineering', 'technical', 'design'], + url: + 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['administrative', 'support', 'admin'], + url: + 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['electric', 'electrical'], + url: + 'https://images.unsplash.com/photo-1621905251918-48416bd8575a?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['plumbing'], + url: + 'https://images.unsplash.com/photo-1621905252507-b35492cc74b4?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['culinary', 'chef'], + url: 'https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['civil', 'construction'], + url: + 'https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['nutrition', 'diet'], + url: + 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=640&h=480&fit=crop&q=80', + }, + { + keywords: ['mechanical'], + url: + 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=640&h=480&fit=crop&q=80', + }, + ]; + + // Find matching category + for (const { keywords, url } of categoryImageMap) { + if (keywords.some(keyword => categoryLower.includes(keyword))) { + return url; } } diff --git a/src/components/Collaboration/JobApplicationForm/JobApplicationForm.jsx b/src/components/Collaboration/JobApplicationForm/JobApplicationForm.jsx index 7c1fa3d2da..6f76be7bdf 100644 --- a/src/components/Collaboration/JobApplicationForm/JobApplicationForm.jsx +++ b/src/components/Collaboration/JobApplicationForm/JobApplicationForm.jsx @@ -51,15 +51,24 @@ function JobApplicationForm() { } }); + // Validate ID parameter to prevent injection attacks + const isValidId = id => { + if (!id || typeof id !== 'string') return false; + // Allow only alphanumeric characters, hyphens, and underscores (MongoDB ObjectId format) + return /^[a-zA-Z0-9_-]+$/.test(id) && id.length <= 100; + }; + // Get job data from redirect or URL parameters useEffect(() => { // Check for referral link parameters from URL query string const searchParams = new URLSearchParams(location.search); const referralId = searchParams.get('ref') || searchParams.get('referral'); - const jobId = searchParams.get('jobId') || location.pathname.split('/').pop(); + const jobIdParam = searchParams.get('jobId'); + const pathJobId = location.pathname.split('/').pop(); + const jobId = jobIdParam || (pathJobId && pathJobId !== 'job-application' ? pathJobId : null); - // If we have a referral ID, fetch user's questionnaire data - if (referralId) { + // If we have a valid referral ID, fetch user's questionnaire data + if (referralId && isValidId(referralId)) { fetchUserQuestionnaireData(referralId); } @@ -69,8 +78,8 @@ function JobApplicationForm() { if (location.state.jobTitle) { setJobTitleInput(location.state.jobTitle); } - } else if (jobId && jobId !== 'job-application') { - // Fetch job data from API if we have a jobId + } else if (jobId && isValidId(jobId)) { + // Fetch job data from API if we have a valid jobId fetchJobData(jobId); } }, [location.state, location.search, location.pathname]); @@ -206,75 +215,52 @@ function JobApplicationForm() { setResumeFile(f); }; - // Check if requirements are satisfied (for admin view) - matching reference implementation - const checkRequirements = () => { - const requirements = { - reactExperience: false, - twoMonthsCommitment: false, - javascriptExperience: false, - timeZoneLocation: false, - tenHoursPerWeek: false, - }; + // Shared function to check requirements based on provided data + const evaluateRequirements = (data = {}) => { + const { + fullTimeYears: years = '', + monthsVolunteer: months = '', + hoursPerWeek: hours = '', + roleSkills: skills = '', + locationTimezone: timezone = '', + } = data; - // Check 1+ years of Full-Time ReactJS Experience const reactKeywords = ['react', 'reactjs', 'react.js']; - const roleSkillsLower = (roleSkills || '').toLowerCase(); - requirements.reactExperience = - (fullTimeYears && parseFloat(fullTimeYears) >= 1) || - reactKeywords.some(keyword => roleSkillsLower.includes(keyword)); - - // Check Minimum of 2 Months Commitment - requirements.twoMonthsCommitment = monthsVolunteer && parseFloat(monthsVolunteer) >= 2; - - // Check 1+ years of Full-Time JavaScript Experience - requirements.javascriptExperience = fullTimeYears && parseFloat(fullTimeYears) >= 1; - - // Check Time Zone and Location Matches - requirements.timeZoneLocation = !!(locationTimezone && locationTimezone.trim()); - - // Check Minimum of 10 hours of work a week - requirements.tenHoursPerWeek = hoursPerWeek && parseFloat(hoursPerWeek) >= 10; + const skillsLower = (skills || '').toLowerCase(); + const yearsNum = years ? parseFloat(years) : 0; + const monthsNum = months ? parseFloat(months) : 0; + const hoursNum = hours ? parseFloat(hours) : 0; + + return { + reactExperience: + yearsNum >= 1 || reactKeywords.some(keyword => skillsLower.includes(keyword)), + twoMonthsCommitment: monthsNum >= 2, + javascriptExperience: yearsNum >= 1, + timeZoneLocation: !!(timezone && timezone.trim()), + tenHoursPerWeek: hoursNum >= 10, + }; + }; - return requirements; + // Check if requirements are satisfied (for admin view) - matching reference implementation + const checkRequirements = () => { + return evaluateRequirements({ + fullTimeYears, + monthsVolunteer, + hoursPerWeek, + roleSkills, + locationTimezone, + }); }; // Check user requirements based on their prior questionnaire data (for user view) const checkUserRequirements = () => { - // Use current form values or user questionnaire data - const userFullTimeYears = fullTimeYears || userQuestionnaireData?.fullTimeYears || ''; - const userMonthsVolunteer = monthsVolunteer || userQuestionnaireData?.monthsVolunteer || ''; - const userHoursPerWeek = hoursPerWeek || userQuestionnaireData?.hoursPerWeek || ''; - const userRoleSkills = roleSkills || userQuestionnaireData?.roleSkills || ''; - const userLocationTimezone = locationTimezone || userQuestionnaireData?.locationTimezone || ''; - - const requirements = { - reactExperience: false, - twoMonthsCommitment: false, - javascriptExperience: false, - timeZoneLocation: false, - tenHoursPerWeek: false, - }; - - // Check 1+ years of Full-Time ReactJS Experience - const reactKeywords = ['react', 'reactjs', 'react.js']; - const roleSkillsLower = (userRoleSkills || '').toLowerCase(); - requirements.reactExperience = - (userFullTimeYears && parseFloat(userFullTimeYears) >= 1) || - reactKeywords.some(keyword => roleSkillsLower.includes(keyword)); - - // Check Minimum of 2 Months Commitment - requirements.twoMonthsCommitment = userMonthsVolunteer && parseFloat(userMonthsVolunteer) >= 2; - - // Check 1+ years of Full-Time JavaScript Experience - requirements.javascriptExperience = userFullTimeYears && parseFloat(userFullTimeYears) >= 1; - - // Check Time Zone and Location Matches - requirements.timeZoneLocation = !!(userLocationTimezone && userLocationTimezone.trim()); - - // Check Minimum of 10 hours of work a week - requirements.tenHoursPerWeek = userHoursPerWeek && parseFloat(userHoursPerWeek) >= 10; - - return requirements; + return evaluateRequirements({ + fullTimeYears: fullTimeYears || userQuestionnaireData?.fullTimeYears || '', + monthsVolunteer: monthsVolunteer || userQuestionnaireData?.monthsVolunteer || '', + hoursPerWeek: hoursPerWeek || userQuestionnaireData?.hoursPerWeek || '', + roleSkills: roleSkills || userQuestionnaireData?.roleSkills || '', + locationTimezone: locationTimezone || userQuestionnaireData?.locationTimezone || '', + }); }; // Helper function to strip HTML tags and clean text From 129e8d3d4def76aea46150840db42378d286eb97 Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 20 Dec 2025 21:51:42 -0500 Subject: [PATCH 08/10] Fix test failure: Add window.matchMedia check in getColumnsFromMQ --- src/components/Collaboration/Collaboration.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Collaboration/Collaboration.jsx b/src/components/Collaboration/Collaboration.jsx index 7396545369..e52c67c516 100644 --- a/src/components/Collaboration/Collaboration.jsx +++ b/src/components/Collaboration/Collaboration.jsx @@ -44,7 +44,7 @@ function Collaboration() { }); function getColumnsFromMQ() { - if (typeof window === 'undefined') return 1; + if (typeof window === 'undefined' || !window.matchMedia) return 1; if (window.matchMedia('(min-width: 1600px)').matches) return 6; if (window.matchMedia('(min-width: 1300px)').matches) return 5; if (window.matchMedia('(min-width: 1017px)').matches) return 4; From df1d92a2c2c9e97a4e751762becf2407fb1bf212 Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 20 Dec 2025 21:54:58 -0500 Subject: [PATCH 09/10] Fix Collaboration component tests: Update test expectations to match actual API call order and parameters --- .../__tests__/Collaboration.test.jsx | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/components/Collaboration/__tests__/Collaboration.test.jsx b/src/components/Collaboration/__tests__/Collaboration.test.jsx index c36a520579..f4f50851e9 100644 --- a/src/components/Collaboration/__tests__/Collaboration.test.jsx +++ b/src/components/Collaboration/__tests__/Collaboration.test.jsx @@ -23,6 +23,9 @@ vi.mock('react-toastify', () => ({ // Mock fetch globally global.fetch = vi.fn(); +// Mock window.scrollTo +global.window.scrollTo = vi.fn(); + // Helper mock responses const mockCategories = { categories: ['Engineering', 'Art'], @@ -77,16 +80,26 @@ describe('Collaboration Component', () => { render(); await waitFor(() => { - expect(fetch).toHaveBeenCalledWith(`${ApiEndpoint}/jobs/categories`); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining(`${ApiEndpoint}/jobs?page=1&limit=`), + { method: 'GET' }, + ); + }); + + await waitFor(() => { + expect(fetch).toHaveBeenCalledWith(`${ApiEndpoint}/jobs/categories`, { method: 'GET' }); }); - expect(fetch).toHaveBeenCalledTimes(2); + // Component calls fetchJobAds() and fetchCategories() on mount + expect(fetch).toHaveBeenCalledWith(expect.stringContaining(`${ApiEndpoint}/jobs/categories`), { + method: 'GET', + }); }); test('search input updates state and triggers tooltip if no categories selected', async () => { render(); - const input = screen.getByPlaceholderText('Search by title...'); + const input = screen.getByPlaceholderText('Enter Job Title'); fireEvent.change(input, { target: { value: 'engineer' } }); expect(input.value).toBe('engineer'); @@ -95,20 +108,29 @@ describe('Collaboration Component', () => { test('submitting search triggers fetchJobAds()', async () => { render(); - const input = screen.getByPlaceholderText('Search by title...'); + const input = screen.getByPlaceholderText('Enter Job Title'); const button = screen.getByText('Go'); fireEvent.change(input, { target: { value: 'engineer' } }); fireEvent.click(button); await waitFor(() => { - expect(fetch).toHaveBeenCalledTimes(3); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining(`${ApiEndpoint}/jobs?page=1&limit=`), + { method: 'GET' }, + ); }); }); test('dropdown toggles open when clicking category button', async () => { render(); - fireEvent.click(screen.getByText('Select Categories ▼')); + // Wait for categories to load + await waitFor(() => { + expect(screen.getByText('Select From Positions')).toBeInTheDocument(); + }); + // The component uses a select dropdown, not a custom dropdown button + const select = screen.getByRole('combobox'); + expect(select).toBeInTheDocument(); }); // ✅ FIXED PAGINATION TEST @@ -116,7 +138,7 @@ describe('Collaboration Component', () => { render(); // Trigger search so results section becomes active - fireEvent.change(screen.getByPlaceholderText('Search by title...'), { + fireEvent.change(screen.getByPlaceholderText('Enter Job Title'), { target: { value: 'test' }, }); @@ -133,20 +155,18 @@ describe('Collaboration Component', () => { test('category chips appear when category selected', async () => { render(); - // Dropdown appears after initial fetch completes + // Wait for categories to load and select dropdown to appear await waitFor(() => { - expect(screen.getByText(/Select Categories/i)).toBeInTheDocument(); + expect(screen.getByText('Select From Positions')).toBeInTheDocument(); }); - fireEvent.click(screen.getByText('Select Categories ▼')); - - // Click Engineering checkbox - fireEvent.click(screen.getByLabelText('Engineering')); + // Select a category from the dropdown + const select = screen.getByRole('combobox'); + fireEvent.change(select, { target: { value: 'Engineering' } }); - // Both occur: dropdown label + chip, so use getAllByText + // Wait for the category to be selected and jobs to be filtered await waitFor(() => { - const matches = screen.getAllByText('Engineering'); - expect(matches.length).toBeGreaterThan(1); // dropdown + chip + expect(fetch).toHaveBeenCalledWith(expect.stringContaining('category='), { method: 'GET' }); }); }); @@ -157,7 +177,9 @@ describe('Collaboration Component', () => { fireEvent.click(screen.getByText('Show Summaries')); await waitFor(() => { - expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/jobs/summaries')); + expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/jobs/summaries'), { + method: 'GET', + }); }); }); }); From 57d5e2708dcfef8c77643d50c84b11de47d2c80e Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 7 Mar 2026 16:52:18 -0500 Subject: [PATCH 10/10] fix: remove duplicate useSelector import in Collaboration.jsx Made-with: Cursor --- src/components/Collaboration/Collaboration.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Collaboration/Collaboration.jsx b/src/components/Collaboration/Collaboration.jsx index 6dc8de7dcf..4cf2b6a91e 100644 --- a/src/components/Collaboration/Collaboration.jsx +++ b/src/components/Collaboration/Collaboration.jsx @@ -5,7 +5,6 @@ import hasPermission from '~/utils/permissions'; import styles from './Collaboration.module.css'; import { toast } from 'react-toastify'; import { ApiEndpoint } from '~/utils/URL'; -import { useSelector } from 'react-redux'; import OneCommunityImage from '../../assets/images/logo2.png'; function Collaboration() {