diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9fa70e9a1d..040fd642da 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -62,4 +62,4 @@ jobs:
domain: ${{ vars.SURGE_DOMAIN }}
project: './build'
login: ${{ secrets.SURGE_LOGIN }}
- token: ${{ secrets.SURGE_TOKEN }}
\ No newline at end of file
+ token: ${{ secrets.SURGE_TOKEN }}
diff --git a/.github/workflows/pull_request_test.yml b/.github/workflows/pull_request_test.yml
index 6cb5c6778e..bd1a3b0c5f 100644
--- a/.github/workflows/pull_request_test.yml
+++ b/.github/workflows/pull_request_test.yml
@@ -55,4 +55,4 @@ jobs:
- name: Run Unit Tests for Changed Files Only
run: yarn run test:changed
- name: Run Lint
- run: yarn run lint
\ No newline at end of file
+ run: yarn run lint
diff --git a/.gitignore b/.gitignore
index c8d7c61956..111aac6af3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
# dependencies
/node_modules
+package-lock.json
# testing
/coverage
*.code-snippets
diff --git a/Dockerfile b/Dockerfile
index bd987a421f..042d7883ed 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Define a imagem base
-FROM node:14-alpine
+FROM node:20-alpine
# Set the working directory to /app
WORKDIR /app
# Copy the package.json and yarn.lock files to the container
diff --git a/package.json b/package.json
index c97267a1d2..0fa4ff8ec9 100644
--- a/package.json
+++ b/package.json
@@ -173,6 +173,7 @@
"@vitejs/plugin-react": "^4.5.0",
"@vitest/ui": "3.2.2",
"babel-jest": "^29.7.0",
+ "baseline-browser-mapping": "^2.9.17",
"cross-env": "^5.2.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^5.1.0",
@@ -189,6 +190,7 @@
"joi-browser": "^13.4.0",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.5",
+ "mdn-data": "^2.26.0",
"msw": "^2.10.4",
"prettier": "^1.19.1",
"redux-mock-store": "^1.5.4",
diff --git a/public/index.css b/public/index.css
index 1358e8c5cf..4e7b5eed7f 100644
--- a/public/index.css
+++ b/public/index.css
@@ -1,3 +1,5 @@
+/* public/index.css */
+
#root {
background-color: #ffffff;
}
@@ -179,3 +181,95 @@ body:not(.dark-mode) textarea {
transform: translateY(-4px);
opacity: 0.9;
}
+
+ /* Allow the page content to scroll horizontally */
+ .container-fluid {
+ overflow-x: auto;
+ }
+
+ /* Hide the horizontal scrollbar */
+ .container-fluid::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* Explicit targeting for all input types and selects */
+ body.dark-mode .form-control,
+ body.bm-dashboard-dark .form-control,
+ body.dark-mode select,
+ body.bm-dashboard-dark select,
+ body.dark-mode input[type="text"] {
+ background-color: #1e293b !important;
+ color: #ffffff !important;
+ border: 1px solid #334155 !important;
+ }
+
+ /* Fix for the Project and Tool dropdown arrows and internal padding */
+ body.dark-mode select.form-control,
+ body.bm-dashboard-dark select.form-control {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e") !important;
+ background-repeat: no-repeat !important;
+ background-position: right 0.75rem center !important;
+ background-size: 16px 12px !important;
+ }
+
+ body.dark-mode .form-control::placeholder {
+ color: #94a3b8 !important;
+ }
+
+ body.dark-mode option,
+ body.bm-dashboard-dark option {
+ background-color: #1e293b !important;
+ color: #ffffff !important;
+ }
+
+ body.dark-mode .modal-content,
+ body.bm-dashboard-dark .modal-content {
+ background-color: #1b2a41 !important;
+ border: 1px solid #2e3d55 !important;
+ color: #ffffff !important;
+ }
+
+ body.dark-mode .modal-header,
+ body.dark-mode .modal-body,
+ body.dark-mode .modal-footer,
+ body.bm-dashboard-dark .modal-header,
+ body.bm-dashboard-dark .modal-body,
+ body.bm-dashboard-dark .modal-footer {
+ background-color: #1b2a41 !important;
+ color: #ffffff !important;
+ border-color: #2e3d55 !important;
+ }
+
+ body.dark-mode .form-control,
+ body.dark-mode select,
+ body.dark-mode input {
+ background-color: #1e293b !important;
+ color: #ffffff !important;
+ border: 1px solid #334155 !important;
+ }
+
+ body.dark-mode select.form-control {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e") !important;
+ }
+
+ body.dark-mode .modal-header,
+ body.bm-dashboard-dark .modal-header {
+ background-color: #24344d !important;
+ border-bottom: 1px solid #334155 !important;
+ color: #ffffff !important;
+ padding: 1rem 1.5rem;
+ }
+
+ body.dark-mode .modal-title,
+ body.bm-dashboard-dark .modal-title {
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ }
+
+ body.dark-mode .modal-content .table thead th {
+ background-color: #2d3d5a !important;
+ color: #ffffff !important;
+ border-bottom: 2px solid #334155 !important;
+ font-size: 0.85rem;
+ letter-spacing: 1px;
+ }
diff --git a/src/actions/allTeamsAction.js b/src/actions/allTeamsAction.js
index f47a507cbd..785225a10b 100644
--- a/src/actions/allTeamsAction.js
+++ b/src/actions/allTeamsAction.js
@@ -251,7 +251,11 @@ export const updateTeamMemeberVisibility = (teamId, userId, visibility) => {
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
- toast.error('Error updating visibility:', error.response.data);
+ const msg =
+ error.response?.data?.message ||
+ error.response?.data?.error ||
+ (typeof error.response?.data === 'string' ? error.response.data : 'Unknown error');
+ toast.error(`Error updating visibility: ${msg}`);
} else if (error.request) {
// The request was made but no response was received
toast.error('Error updating visibility: No response received');
diff --git a/src/actions/blueSquareEmailBCCAction.js b/src/actions/blueSquareEmailBCCAction.js
index 4872f97162..035f7d3c14 100644
--- a/src/actions/blueSquareEmailBCCAction.js
+++ b/src/actions/blueSquareEmailBCCAction.js
@@ -61,7 +61,11 @@ export const deleteBlueSquareEmailAssignement = id => {
try {
const response = await axios.delete(url);
if (response.status === 200) {
- toast.info(response.data);
+ const msg =
+ typeof response.data === 'string'
+ ? response.data
+ : response.data?.message || JSON.stringify(response.data);
+ toast.info(msg);
dispatch(deleteBlueSquareEmailBcc(response.data.id));
} else {
dispatch(blueSquareEmailBccError(response.data));
diff --git a/src/actions/rolePermissionPresets.js b/src/actions/rolePermissionPresets.js
index fc35458ee9..a80c35602b 100644
--- a/src/actions/rolePermissionPresets.js
+++ b/src/actions/rolePermissionPresets.js
@@ -46,7 +46,7 @@ export const createNewPreset = newPreset => {
}
return 0;
} catch (error) {
- toast.error(error);
+ toast.error(error?.message || String(error));
return 1;
}
};
@@ -60,7 +60,7 @@ export const updatePresetById = updatedPreset => {
dispatch(updatePreset(updatedPreset));
}
} catch (err) {
- toast.info(err);
+ toast.info(err?.message || String(err));
}
};
};
@@ -75,7 +75,7 @@ export const deletePresetById = presetId => {
}
return 1;
} catch (error) {
- toast.info(error);
+ toast.info(error?.message || String(error));
return 1;
}
};
diff --git a/src/actions/task.js b/src/actions/task.js
index aaa1fb3899..d08dae0b81 100644
--- a/src/actions/task.js
+++ b/src/actions/task.js
@@ -190,7 +190,7 @@ export const deleteChildrenTasks = taskId => {
try {
await axios.post(ENDPOINTS.DELETE_CHILDREN(taskId));
} catch (error) {
- toast.info(error);
+ toast.info(error?.message || String(error));
}
};
};
@@ -257,7 +257,7 @@ export const updateTask = (taskId, updatedTask, hasPermission, prevTask) => asyn
}
} catch (error) {
// dispatch(fetchTeamMembersTaskError());
- toast.info(error);
+ toast.info(error?.message || String(error));
status = 400;
}
// TODO: DISPATCH TO TASKEDITSUGGESETIONS REDUCER TO UPDATE STATE
diff --git a/src/actions/team.js b/src/actions/team.js
index 748c59ba34..5c5ee80b5b 100644
--- a/src/actions/team.js
+++ b/src/actions/team.js
@@ -106,7 +106,7 @@ export const fetchAllManagingTeams = (userId, managingTeams) => async dispatch =
await dispatch(setTeamsStart());
dispatch(setTeams(allManagingTeams));
} catch (err) {
- toast.error(err);
+ toast.error(err?.message || String(err));
dispatch(setTeamsError(err));
}
};
diff --git a/src/actions/timeOffRequestAction.js b/src/actions/timeOffRequestAction.js
index d430e4aa02..855d0d9871 100644
--- a/src/actions/timeOffRequestAction.js
+++ b/src/actions/timeOffRequestAction.js
@@ -209,7 +209,7 @@ export const addTimeOffRequestThunk = request => async dispatch => {
const AddedRequest = response.data;
dispatch(addTimeOffRequest(AddedRequest));
} catch (error) {
- toast.info(error);
+ toast.info(error?.message || String(error));
}
};
@@ -220,7 +220,7 @@ export const updateTimeOffRequestThunk = (id, data) => async dispatch => {
const updatedRequest = response.data;
dispatch(updateTimeOffRequest(updatedRequest));
} catch (error) {
- toast.info(error);
+ toast.info(error?.message || String(error));
}
};
@@ -230,6 +230,6 @@ export const deleteTimeOffRequestThunk = id => async dispatch => {
const deletedRequest = response.data;
dispatch(deleteTimeOffRequest(deletedRequest));
} catch (error) {
- toast.info(error);
+ toast.info(error?.message || String(error));
}
};
\ No newline at end of file
diff --git a/src/actions/userProfile.js b/src/actions/userProfile.js
index 4dd37d83b2..fd87c5ce0d 100644
--- a/src/actions/userProfile.js
+++ b/src/actions/userProfile.js
@@ -77,7 +77,7 @@ export const getUserTasks = userId => {
toast.info(`Get user task request status is not 200, status message: ${res.statusText}`);
}
} catch (error) {
- toast.error(error);
+ toast.error(error?.message || String(error));
}
};
};
diff --git a/src/components/ApplicantSourceDonutChart/ApplicantSourceDonutChart.jsx b/src/components/ApplicantSourceDonutChart/ApplicantSourceDonutChart.jsx
new file mode 100644
index 0000000000..e2c95afa98
--- /dev/null
+++ b/src/components/ApplicantSourceDonutChart/ApplicantSourceDonutChart.jsx
@@ -0,0 +1,669 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import httpService from '../../services/httpService';
+import { PieChart, Pie, Cell, Tooltip, Legend, Label, ResponsiveContainer } from 'recharts';
+import DatePicker from 'react-datepicker';
+import Select from 'react-select';
+import 'react-datepicker/dist/react-datepicker.css';
+import { useSelector } from 'react-redux';
+import { ENDPOINTS } from '../../utils/URL';
+import config from '../../config.json';
+import styles from './ApplicantSourceDonutChart.module.css';
+
+const calculateTotal = payload => {
+ if (!Array.isArray(payload)) return 0;
+ return payload.reduce((sum, item) => sum + (item?.value ?? 0), 0);
+};
+
+const calculatePercentage = (value, total) => {
+ if (!total || total <= 0) return '0.0';
+ return ((value / total) * 100).toFixed(1);
+};
+
+const getTooltipStyles = darkMode => ({
+ backgroundColor: darkMode ? '#1e293b' : '#ffffff',
+ border: `1px solid ${darkMode ? '#475569' : '#e5e7eb'}`,
+ borderRadius: '6px',
+ padding: '10px 12px',
+ boxShadow: darkMode
+ ? '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2)'
+ : '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
+ color: darkMode ? '#e2e8f0' : '#1f2937',
+});
+
+const CustomTooltip = ({ active, payload, darkMode }) => {
+ if (!active || !payload?.length) return null;
+
+ const data = payload[0];
+ const name = data?.name ?? 'Unknown';
+ const value = data?.value ?? 0;
+ const total = calculateTotal(payload);
+ const percentage = calculatePercentage(value, total);
+
+ return (
+
+
+ {name}
+
+
+ Value: {value}
+
+
+ Percentage:{' '}
+ {percentage}%
+
+
+ );
+};
+
+CustomTooltip.propTypes = {
+ active: PropTypes.bool,
+ payload: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string,
+ value: PropTypes.number,
+ }),
+ ),
+ darkMode: PropTypes.bool,
+};
+
+CustomTooltip.defaultProps = {
+ active: false,
+ payload: [],
+ darkMode: false,
+};
+
+const COLORS = ['#FF4D4F', '#FFC107', '#1890FF', '#00C49F', '#8884D8'];
+const toDateOnlyString = date => (date ? date.toISOString().split('T')[0] : null);
+
+const COMPARISON_TYPE_OPTIONS = [
+ { label: 'This same week last year', value: 'week' },
+ { label: 'This same month last year', value: 'month' },
+ { label: 'This same year', value: 'year' },
+ { label: 'Custom Dates (no comparison)', value: '' },
+];
+
+const ROLE_OPTIONS = [
+ 'Project Manager',
+ 'Frontend Developer',
+ 'Backend Developer',
+ 'Full Stack Developer',
+ 'DevOps Engineer',
+ 'API Engineer',
+ 'QA Engineer',
+ 'Test Analyst',
+ 'Support Engineer',
+ 'Tech Lead',
+ 'Architect',
+ 'Junior Developer',
+ 'Intern',
+].map(label => ({ label, value: label }));
+
+const getDarkModeSelectStyles = isMulti => ({
+ control: provided => ({
+ ...provided,
+ backgroundColor: '#1f2937',
+ borderColor: '#3b82f6',
+ color: '#e5e7eb',
+ }),
+ menu: provided => ({
+ ...provided,
+ backgroundColor: '#111827',
+ color: '#e5e7eb',
+ }),
+ singleValue: provided => ({
+ ...provided,
+ color: '#e5e7eb',
+ }),
+ option: (provided, state) => ({
+ ...provided,
+ backgroundColor: state.isFocused ? '#2563eb' : '#111827',
+ color: '#e5e7eb',
+ }),
+ ...(isMulti && {
+ multiValue: provided => ({
+ ...provided,
+ backgroundColor: '#2563eb',
+ }),
+ multiValueLabel: provided => ({
+ ...provided,
+ color: '#e5e7eb',
+ }),
+ multiValueRemove: provided => ({
+ ...provided,
+ color: '#bfdbfe',
+ ':hover': {
+ backgroundColor: '#1d4ed8',
+ color: '#e5e7eb',
+ },
+ }),
+ }),
+});
+
+const resetDataState = (setData, setComparisonText) => {
+ setData([]);
+ setComparisonText('');
+};
+
+const validateLocalStorage = () => {
+ return typeof localStorage !== 'undefined' && localStorage !== null;
+};
+
+const getToken = () => {
+ if (!validateLocalStorage()) return null;
+ const token = localStorage.getItem(config?.tokenKey || 'token');
+ return token && typeof token === 'string' ? token : null;
+};
+
+const validateDates = (startDate, endDate) => {
+ if (!startDate || !endDate) return true;
+ if (!(startDate instanceof Date) || !(endDate instanceof Date)) return true;
+ return startDate.getTime() <= endDate.getTime();
+};
+
+const buildRoleParams = filterRoles => {
+ if (!Array.isArray(filterRoles) || filterRoles.length === 0) return null;
+ const roleValues = filterRoles
+ .map(r => {
+ if (typeof r === 'object' && r !== null && r.value !== undefined) {
+ return r.value;
+ }
+ return typeof r === 'string' ? r : '';
+ })
+ .filter(val => val !== '');
+ return roleValues.length > 0 ? roleValues.join(',') : null;
+};
+
+const buildRequestParams = (filterStartDate, filterEndDate, filterRoles, filterComparisonType) => {
+ const params = {};
+ if (filterStartDate) params.startDate = toDateOnlyString(filterStartDate);
+ if (filterEndDate) params.endDate = toDateOnlyString(filterEndDate);
+ const roleParam = buildRoleParams(filterRoles);
+ if (roleParam) params.roles = roleParam;
+ if (filterComparisonType && filterComparisonType !== '') {
+ params.comparisonType = filterComparisonType;
+ }
+ return params;
+};
+
+const formatSourceItem = item => {
+ if (!item || typeof item !== 'object') return null;
+ const value = Number(item.value ?? item.count ?? 0);
+ if (Number.isNaN(value) || !Number.isFinite(value) || value < 0) return null;
+ return {
+ name: item.name || item.source || 'Unknown',
+ value,
+ };
+};
+
+const formatSources = sources => {
+ if (!Array.isArray(sources)) return [];
+ return sources.map(formatSourceItem).filter(item => item !== null);
+};
+
+const calculatePastDate = (today, comparisonType) => {
+ const pastDate = new Date(today);
+ if (comparisonType === 'week') {
+ pastDate.setDate(today.getDate() - 7);
+ } else if (comparisonType === 'month') {
+ pastDate.setMonth(today.getMonth() - 1);
+ } else if (comparisonType === 'year') {
+ pastDate.setFullYear(today.getFullYear() - 1);
+ }
+ return pastDate;
+};
+
+const ApplicantSourceDonutChart = () => {
+ const [data, setData] = useState([]);
+ const [startDate, setStartDate] = useState(null);
+ const [endDate, setEndDate] = useState(null);
+ const [selectedRoles, setSelectedRoles] = useState([]);
+ const [comparisonText, setComparisonText] = useState('');
+ const [comparisonType, setComparisonType] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const darkMode = useSelector(state => state.theme?.darkMode);
+ const chartWrapperRef = useRef(null);
+ const [chartWidth, setChartWidth] = useState(0);
+
+ const fetchDataWithFilters = useCallback(
+ async ({
+ startDate: filterStartDate,
+ endDate: filterEndDate,
+ roles: filterRoles,
+ comparisonType: filterComparisonType,
+ }) => {
+ setLoading(true);
+ setError('');
+
+ try {
+ if (!validateLocalStorage()) {
+ setError('LocalStorage is not available. Please log in again.');
+ setLoading(false);
+ resetDataState(setData, setComparisonText);
+ return;
+ }
+
+ const token = getToken();
+ if (!token) {
+ setError('Please log in to view applicant source data.');
+ setLoading(false);
+ resetDataState(setData, setComparisonText);
+ return;
+ }
+
+ if (httpService?.setjwt) {
+ httpService.setjwt(token);
+ }
+
+ if (!validateDates(filterStartDate, filterEndDate)) {
+ setError('Start date cannot be greater than end date');
+ setLoading(false);
+ resetDataState(setData, setComparisonText);
+ return;
+ }
+
+ const url = ENDPOINTS?.APPLICANT_SOURCES;
+ if (!url || typeof url !== 'string') {
+ throw new Error('Invalid API endpoint configuration');
+ }
+
+ const params = buildRequestParams(
+ filterStartDate,
+ filterEndDate,
+ filterRoles,
+ filterComparisonType,
+ );
+
+ if (!httpService?.get) {
+ throw new Error('HTTP service is not available');
+ }
+
+ const response = await httpService.get(url, { params });
+
+ if (!response || typeof response !== 'object') {
+ throw new Error('Invalid response from server');
+ }
+
+ const result = response.data || {};
+ const formattedSources = formatSources(result.sources);
+
+ setData(formattedSources);
+ setComparisonText(typeof result.comparisonText === 'string' ? result.comparisonText : '');
+ } catch (err) {
+ if (err?.response?.status === 401) {
+ setError('Authentication required. Please log in to view applicant source data.');
+ } else {
+ setError(
+ err?.response?.data?.message ||
+ err?.message ||
+ 'Error fetching data. Please try again later.',
+ );
+ }
+ resetDataState(setData, setComparisonText);
+ } finally {
+ setLoading(false);
+ }
+ },
+ [],
+ );
+
+ useEffect(() => {
+ fetchDataWithFilters({ startDate: null, endDate: null, roles: [], comparisonType: '' });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ const updateDimensions = () => {
+ if (chartWrapperRef.current?.offsetWidth) {
+ const width = chartWrapperRef.current.offsetWidth;
+ if (Number.isFinite(width) && width > 0) {
+ setChartWidth(width);
+ }
+ }
+ };
+
+ updateDimensions();
+
+ let observer;
+ const globalWindow = globalThis.window;
+ if (globalWindow?.ResizeObserver && chartWrapperRef.current) {
+ try {
+ observer = new globalWindow.ResizeObserver(updateDimensions);
+ observer.observe(chartWrapperRef.current);
+ } catch {
+ if (globalWindow.addEventListener) {
+ globalWindow.addEventListener('resize', updateDimensions);
+ }
+ }
+ } else if (globalWindow?.addEventListener) {
+ globalWindow.addEventListener('resize', updateDimensions);
+ }
+
+ return () => {
+ if (observer?.disconnect) {
+ try {
+ observer.disconnect();
+ } catch {
+ // Ignore cleanup errors
+ }
+ } else if (globalWindow?.removeEventListener) {
+ globalWindow.removeEventListener('resize', updateDimensions);
+ }
+ };
+ }, []);
+
+ const handleStartDateChange = date => {
+ setStartDate(date);
+ if (comparisonType === '') {
+ fetchDataWithFilters({
+ startDate: date || null,
+ endDate: endDate || null,
+ roles: selectedRoles || [],
+ comparisonType: comparisonType || '',
+ });
+ }
+ };
+
+ const handleEndDateChange = date => {
+ setEndDate(date);
+ if (comparisonType === '') {
+ fetchDataWithFilters({
+ startDate: startDate || null,
+ endDate: date || null,
+ roles: selectedRoles || [],
+ comparisonType: comparisonType || '',
+ });
+ }
+ };
+
+ const handleRoleChange = roles => {
+ const safeRoles = Array.isArray(roles) ? roles : [];
+ setSelectedRoles(safeRoles);
+ fetchDataWithFilters({
+ startDate: startDate || null,
+ endDate: endDate || null,
+ roles: safeRoles,
+ comparisonType: comparisonType || '',
+ });
+ };
+
+ const handleComparisonTypeChange = option => {
+ const newComparisonType =
+ option && typeof option === 'object' && option.value !== undefined ? option.value : '';
+ setComparisonType(newComparisonType);
+
+ if (newComparisonType === '') {
+ fetchDataWithFilters({
+ startDate: startDate || null,
+ endDate: endDate || null,
+ roles: selectedRoles || [],
+ comparisonType: '',
+ });
+ return;
+ }
+
+ const today = new Date();
+ if (!(today instanceof Date) || !Number.isFinite(today.getTime())) {
+ setError('Invalid date configuration. Please refresh the page.');
+ return;
+ }
+
+ const pastDate = calculatePastDate(today, newComparisonType);
+ if (!(pastDate instanceof Date) || !Number.isFinite(pastDate.getTime())) {
+ setError('Invalid date calculation. Please try again.');
+ return;
+ }
+
+ fetchDataWithFilters({
+ startDate: pastDate,
+ endDate: today,
+ roles: selectedRoles || [],
+ comparisonType: newComparisonType,
+ });
+ };
+
+ const total = useMemo(() => {
+ return data.reduce((sum, item) => {
+ const value = item?.value ?? 0;
+ return sum + (Number.isFinite(value) && value >= 0 ? value : 0);
+ }, 0);
+ }, [data]);
+
+ const renderCenterText = ({ viewBox }) => {
+ if (!viewBox || typeof viewBox !== 'object') {
+ return null;
+ }
+ const { cx, cy } = viewBox;
+ if (
+ typeof cx !== 'number' ||
+ typeof cy !== 'number' ||
+ !Number.isFinite(cx) ||
+ !Number.isFinite(cy)
+ ) {
+ return null;
+ }
+ const lines = comparisonText ? comparisonText.split('\n') : [];
+
+ if (!lines.length) {
+ return null;
+ }
+
+ // Adjust font sizes for mobile
+ const isMobile = chartWidth < 640;
+ const baseFontSize = isMobile ? 12 : 16;
+ const secondaryFontSize = isMobile ? 10 : 12;
+ const lineSpacing = isMobile ? 12 : 16;
+
+ return (
+
+ {lines.map((line, index) => {
+ // Word wrap for long lines on mobile
+ const words = line.split(' ');
+ const chunks = [];
+ if (isMobile && line.length > 20) {
+ let currentChunk = '';
+ words.forEach(word => {
+ const testChunk = currentChunk ? `${currentChunk} ${word}` : word;
+ if (testChunk.length <= 20) {
+ currentChunk = testChunk;
+ } else {
+ if (currentChunk) chunks.push(currentChunk);
+ currentChunk = word;
+ }
+ });
+ if (currentChunk) chunks.push(currentChunk);
+ } else {
+ chunks.push(line);
+ }
+
+ return chunks.map((chunk, chunkIndex) => (
+
+ {chunk}
+
+ ));
+ })}
+
+ );
+ };
+
+ const pageClassName = `${styles.page} ${darkMode ? styles.pageDark : ''}`;
+ const headingClassName = `${styles.heading} ${darkMode ? styles.darkHeading : ''}`;
+ const containerClassName = `${styles.applicantChartContainer} ${
+ darkMode ? styles.darkContainer : ''
+ }`;
+ const filterRowClassName = `${styles.filterRow} ${darkMode ? styles.dark : ''}`;
+ const chartWrapperClassName = `${styles.chartWrapper} ${darkMode ? styles.dark : ''}`;
+
+ const computedOuterRadius = useMemo(() => {
+ if (!chartWidth) return 150;
+ const proposed = chartWidth / 3.2;
+ return Math.max(100, Math.min(proposed, 160));
+ }, [chartWidth]);
+
+ const computedInnerRadius = useMemo(() => Math.round(computedOuterRadius * 0.75), [
+ computedOuterRadius,
+ ]);
+ const chartHeight = useMemo(() => {
+ if (!chartWidth) return 400;
+ const base = Math.max(360, Math.min(chartWidth * 0.85, 520));
+ return base + (chartWidth < 640 ? 130 : 0);
+ }, [chartWidth]);
+ const showSliceLabels = chartWidth > 640;
+ const legendLayout = chartWidth < 640 ? 'vertical' : 'horizontal';
+ const legendVerticalAlign = 'bottom';
+ const legendAlign = 'center';
+ const legendWrapperStyle = useMemo(() => {
+ if (chartWidth < 640) {
+ return {
+ paddingTop: 40,
+ margin: '24px auto 0',
+ width: '100%',
+ maxWidth: 260,
+ textAlign: 'left',
+ display: 'flex',
+ flexDirection: 'column',
+ rowGap: 6,
+ alignItems: 'flex-start',
+ };
+ }
+
+ return {
+ paddingTop: 8,
+ margin: '24px auto 0',
+ };
+ }, [chartWidth]);
+
+ return (
+
+
+
+ Source of Applicants
+
+
+ {/* Filters */}
+
+ {comparisonType === '' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ {/* Chart */}
+
+ {loading &&
Loading data...
}
+ {error && !loading && (
+
{error}
+ )}
+ {data.length === 0 && !loading && !error && (
+
No data available for selected filters.
+ )}
+ {data.length > 0 && !loading && !error && (
+
+
+ {
+ const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
+ return `${name}: ${percentage}% (${value})`;
+ }
+ : undefined
+ }
+ labelLine={showSliceLabels}
+ >
+ {data.map((entry, index) => (
+ |
+ ))}
+ {comparisonText && }
+
+ } />
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default ApplicantSourceDonutChart;
diff --git a/src/components/ApplicantSourceDonutChart/ApplicantSourceDonutChart.module.css b/src/components/ApplicantSourceDonutChart/ApplicantSourceDonutChart.module.css
new file mode 100644
index 0000000000..7208b34309
--- /dev/null
+++ b/src/components/ApplicantSourceDonutChart/ApplicantSourceDonutChart.module.css
@@ -0,0 +1,131 @@
+.page {
+ min-height: 100vh;
+ padding: 2rem 0;
+ background-color: #f9fafb;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.pageDark {
+ background-color: #0f172a;
+}
+
+.applicantChartContainer {
+ padding: 2rem 3rem;
+ width: 100%;
+ max-width: 1440px;
+ margin: 0 auto;
+ box-sizing: border-box;
+}
+
+.filterRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+}
+
+.filterInput {
+ min-width: 180px;
+ flex: 1 1 180px;
+}
+
+.chartWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 400px;
+}
+
+.dark {
+ color: #e5e8f0;
+}
+
+.darkContainer {
+ background-color: #111827;
+ border-radius: 12px;
+ box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.08);
+ padding-bottom: 2rem;
+}
+
+.darkHeading {
+ color: #9cbaff;
+}
+
+.heading {
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+}
+
+.darkInput {
+ background-color: #1f2937 !important;
+ color: #e5e7eb !important;
+ border-color: #3b82f6 !important;
+}
+
+@media (max-width: 1024px) {
+ .applicantChartContainer {
+ padding: 1.5rem 1.75rem;
+ max-width: 100%;
+ }
+}
+
+@media (max-width: 768px) {
+ .page {
+ padding: 1.5rem 0.75rem;
+ }
+
+ .applicantChartContainer {
+ padding: 1.5rem 1.25rem 3rem;
+ min-height: calc(100vh - 8rem);
+ }
+
+ .filterRow {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0.5rem;
+ }
+
+ .filterInput {
+ width: 100%;
+ min-width: 0;
+ flex: 1 1 auto;
+ }
+
+ .chartWrapper {
+ padding: 3.5rem 0 2.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+/* Dark mode tooltip styles */
+.dark :global(.recharts-tooltip-wrapper) {
+ z-index: 1000;
+}
+
+.dark :global(.recharts-default-tooltip) {
+ background-color: #1e293b !important;
+ border: 1px solid #475569 !important;
+ border-radius: 6px !important;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2) !important;
+}
+
+.dark :global(.recharts-tooltip-label) {
+ color: #f1f5f9 !important;
+ font-weight: 600 !important;
+ margin-bottom: 4px !important;
+}
+
+.dark :global(.recharts-tooltip-item) {
+ color: #e2e8f0 !important;
+}
+
+.dark :global(.recharts-tooltip-item-name),
+.dark :global(.recharts-tooltip-item-separator),
+.dark :global(.recharts-tooltip-item-value) {
+ color: #e2e8f0 !important;
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx
index 1b1c5878a6..489e6061b4 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx
@@ -1,4 +1,3 @@
-/* eslint-disable no-alert */
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
@@ -30,6 +29,7 @@ const CustomTooltip = ({ active, payload, total, darkMode }) => {
export default function DistributionLaborHours() {
const darkMode = useSelector(state => state.theme.darkMode);
+
const [originalData, setOriginalData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [dateRange, setDateRange] = useState({ from: '', to: '' });
@@ -65,12 +65,12 @@ export default function DistributionLaborHours() {
const totalHours = filteredData.reduce((sum, item) => sum + item.value, 0);
return (
-
+
Distribution of Labor Hours
{/* Filters */}
-
{/* Chart + Legend */}
@@ -132,7 +134,19 @@ export default function DistributionLaborHours() {
cy="50%"
outerRadius={100}
labelLine={false}
- label={({ value }) => `${((value / totalHours) * 100).toFixed(1)}%`}
+ label={({ x, y, value }) => (
+
+ {`${((value / totalHours) * 100).toFixed(1)}%`}
+
+ )}
>
{filteredData.map((entry, index) => (
|
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css
index 7b3bb8d252..65f79e38fb 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css
@@ -1,20 +1,27 @@
.container {
- background-color: var(--bg-color, white);
border-radius: 8px;
padding: 16px;
- box-shadow: var(--shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
- color: var(--text-color, #000);
+ transition: background-color 0.3s ease, color 0.3s ease;
}
.title {
font-size: large;
- color: var(--title-color, black);
font-weight: bold;
margin-bottom: 8px;
+ color: #2563eb;
+}
+
+/* Keep title blue in dark mode */
+:global(.dark) .title,
+:global(.dark-mode) .title,
+:global(.bm-dashboard-dark) .title,
+:global([data-theme='dark']) .title {
+ color: #2563eb !important;
}
.filters {
@@ -29,22 +36,31 @@
display: flex;
flex-direction: column;
font-size: 0.9rem;
- color: var(--label-color, #000);
}
-.filters input,
-.filters select {
- background-color: var(--input-bg, #fff);
- color: var(--text-color, #000);
- border: 1px solid var(--border-color, #ccc);
- border-radius: 4px;
- padding: 4px 8px;
+/* Dark mode: make filter labels readable */
+:global(.dark) .filters label,
+:global(.dark-mode) .filters label,
+:global([data-theme='dark']) .filters label {
+ color: #ffffff;
+}
+
+/* Dark mode: style inputs/selects (including date picker) */
+:global(.dark) .filters input,
+:global(.dark-mode) .filters input,
+:global([data-theme='dark']) .filters input,
+:global(.dark) .filters select,
+:global(.dark-mode) .filters select,
+:global([data-theme='dark']) .filters select {
+ background-color: #1f2937;
+ color: #f9fafb;
+ border: 1px solid #374151;
}
-.filters input:focus,
-.filters select:focus {
- border-color: var(--focus-border-color, #3b82f6);
- outline: none;
+:global(.dark) .filters input[type='date'],
+:global(.dark-mode) .filters input[type='date'],
+:global([data-theme='dark']) .filters input[type='date'] {
+ color-scheme: dark;
}
.chartWrapper {
@@ -66,7 +82,6 @@
align-items: center;
margin-bottom: 8px;
font-size: 0.85rem;
- color: var(--text-color, #000);
}
.colorBox {
@@ -77,26 +92,54 @@
}
.tooltip {
- background-color: var(--tooltip-bg, white);
- border: 1px solid var(--border-color, #ccc);
+ background-color: #f3f4f6;
+ color: #111827; /* darker percentage text in light mode */
padding: 8px;
- font-size: 0.85rem;
- border-radius: 4px;
- color: var(--text-color, #000);
+ border-radius: 6px;
+}
+
+/* Dark mode: ensure tooltip text is white on hover */
+:global(.dark) .tooltip,
+:global(.dark-mode) .tooltip,
+:global([data-theme='dark']) .tooltip {
+ color: #ffffff;
}
+:global(.dark) .tooltip:hover,
+:global(.dark-mode) .tooltip:hover,
+:global([data-theme='dark']) .tooltip:hover {
+ color: #ffffff;
+}
+
+/* Dark mode: force ALL tooltip text to white */
+:global(.dark) .tooltip *,
+:global(.dark-mode) .tooltip *,
+:global([data-theme='dark']) .tooltip * {
+ color: #ffffff !important;
+}
+
+/* Submit button – light & dark mode safe */
.button {
- background-color: var(--button-bg, white);
- border: 1px solid var(--button-border, black);
- border-radius: 10%;
- padding: 10px 20px;
+ background-color: #2563eb; /* blue */
+ color: #ffffff; /* white text */
+ border: none;
+ border-radius: 6px;
+ padding: 8px 16px;
cursor: pointer;
transition: background-color 0.3s ease;
- color: var(--button-text, #000);
}
+/* Hover */
.button:hover {
- background-color: var(--button-hover-bg, #fff2f0);
+ background-color: #1d4ed8;
+}
+
+/* Dark mode – keep same button color (no brightness jump) */
+:global(.dark) .button,
+:global(.dark-mode) .button,
+:global([data-theme='dark']) .button {
+ background-color: #2563eb;
+ color: #ffffff;
}
.pieChartContainer {
@@ -127,20 +170,3 @@
width: 100%;
}
}
-
-/* ============ DARK MODE ============ */
-.container.darkMode {
- --bg-color: #2b3e59;
- --text-color: #ffffff;
- --title-color: #ffffff;
- --label-color: #e0e0e0;
- --input-bg: #3a506b;
- --border-color: #4a5a77;
- --focus-border-color: #e8a71c;
- --tooltip-bg: #1b2a41;
- --button-bg: #3a506b;
- --button-border: #4a5a77;
- --button-text: #ffffff;
- --button-hover-bg: #495b7a;
- --shadow: 0 1px 3px rgba(255, 255, 255, 0.1);
-}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
index 7dd21705c6..a2b7aec91f 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
@@ -409,7 +409,7 @@ function WeeklyProjectSummary() {
{
title: 'Labor and Time Tracking',
key: 'Labor and Time Tracking',
- className: 'half',
+ className: 'full',
content: [1, 2].map((_, index) => {
const uniqueId = uuidv4();
return (
@@ -417,7 +417,7 @@ function WeeklyProjectSummary() {
key={uniqueId}
className={`${styles.weeklyProjectSummaryCard} ${styles.normalCard}`}
>
- {index === 1 ?
: '📊 Card'}
+ {index === 1 ?
:
}
);
}),
diff --git a/src/components/CommunityPortal/Activities/NoShow/NoShowList.jsx b/src/components/CommunityPortal/Activities/NoShow/NoShowList.jsx
index 14a3ee8e66..e207c3e61e 100644
--- a/src/components/CommunityPortal/Activities/NoShow/NoShowList.jsx
+++ b/src/components/CommunityPortal/Activities/NoShow/NoShowList.jsx
@@ -99,7 +99,11 @@ function NoShowListModal({ isOpen, toggle, mockData }) {
});
if (response.status === 200) {
- toast.success(response.data.message, {
+ const msg =
+ typeof response.data?.message === 'string'
+ ? response.data.message
+ : response.data?.message?.message || 'Success';
+ toast.success(msg, {
position: 'top-right',
autoClose: 3000,
});
diff --git a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx
index 1351092ed4..0c7b277859 100644
--- a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx
+++ b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx
@@ -1,43 +1,100 @@
-import React, { useState, useMemo } from 'react';
+import { useState } from 'react';
+import { useSelector } from 'react-redux';
import RankedUserList from './RankedUserList';
+import styles from './style/CommunityMembersPage.module.css';
+import { availableSkills, availablePreferences, formatSkillName } from './FilerData.js';
-const availableSkills = ['React', 'Redux', 'HTML', 'CSS', 'MongoDB', 'Database', 'Agile'];
+function Accordion({ title, children, defaultOpen = false, darkMode }) {
+ const [open, setOpen] = useState(defaultOpen);
+ return (
+
+
setOpen(prev => !prev)}
+ onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && setOpen(prev => !prev)}
+ className={`${styles.accordionHeader} ${darkMode ? styles.dark : ''}`}
+ >
+ {title}
+ {open ? '−' : '+'}
+
+ {open &&
{children}
}
+
+ );
+}
function CommunityMembersPage() {
const [selectedSkills, setSelectedSkills] = useState([]);
+ const [selectedPreferences, setSelectedPreferences] = useState([]);
+ const darkMode = useSelector(state => state.theme.darkMode);
- const handleCheckboxChange = skill => {
- setSelectedSkills(prev =>
- prev.includes(skill) ? prev.filter(s => s !== skill) : [...prev, skill],
+ const toggleItem = (item, selectedArray, setSelectedArray) => {
+ setSelectedArray(prev =>
+ prev.includes(item) ? prev.filter(i => i !== item) : [...prev, item],
);
};
- // EFFECTIVE SKILLS = what we pass to RankedUserList
- const effectiveSkills = useMemo(() => {
- return selectedSkills.length > 0 ? selectedSkills : availableSkills;
- }, [selectedSkills]);
+ const renderSkillButtons = () => (
+
+ {availableSkills.map(skillKey => {
+ const formattedName = formatSkillName(skillKey);
+ const isSelected = selectedSkills.includes(skillKey);
+ return (
+
+ );
+ })}
+
+ );
+
+ const renderPreferenceButtons = () => (
+
+ {availablePreferences.map(pref => {
+ const isSelected = selectedPreferences.includes(pref);
+ return (
+
+ );
+ })}
+
+ );
return (
-
-
Community Members
-
-
-
Filter by skills:
-
- {availableSkills.map(skill => (
-
- handleCheckboxChange(skill)}
- />
- {skill}
-
- ))}
-
-
+
+
Community Member Filters
-
+
+ {renderSkillButtons()}
+
+
+
+ {renderPreferenceButtons()}
+
+
+
+ {selectedSkills.length > 0 || selectedPreferences.length > 0 ? (
+
+ ) : (
+
+ Select skills or preferences above to see filtered members.
+
+ )}
+
);
}
diff --git a/src/components/HGNHelpSkillsDashboard/FilerData.js b/src/components/HGNHelpSkillsDashboard/FilerData.js
new file mode 100644
index 0000000000..2fbfc36c33
--- /dev/null
+++ b/src/components/HGNHelpSkillsDashboard/FilerData.js
@@ -0,0 +1,69 @@
+export const availableSkills = [
+ 'combined_frontend_backend',
+ 'mern_skills',
+ 'leadership_skills',
+ 'HTML',
+ 'Bootstrap',
+ 'CSS',
+ 'React',
+ 'Redux',
+ 'WebSocketCom',
+ 'ResponsiveUI',
+ 'UnitTest',
+ 'Documentation',
+ 'UIUXTools',
+ 'Database',
+ 'MongoDB',
+ 'MongoDB_Advanced',
+ 'TestDrivenDev',
+ 'Deployment',
+ 'VersionControl',
+ 'CodeReview',
+ 'EnvironmentSetup',
+ 'AdvancedCoding',
+ 'AgileDevelopment',
+];
+
+export const availablePreferences = [
+ 'Design',
+ 'Backend',
+ 'Frontend',
+ 'Management',
+ 'Testing',
+ 'Deployment',
+ 'No Preference',
+];
+
+export const formatSkillName = key => {
+ switch (key) {
+ case 'combined_frontend_backend':
+ return 'Frontend/Backend';
+ case 'mern_skills':
+ return 'MERN';
+ case 'leadership_skills':
+ return 'Leadership';
+ case 'MongoDB_Advanced':
+ return 'Advanced MongoDB';
+ case 'UIUXTools':
+ return 'UI/UX';
+ case 'TestDrivenDev':
+ return 'TDD';
+ case 'ResponsiveUI':
+ return 'Responsive UI';
+ case 'MongoDB':
+ case 'HTML':
+ case 'CSS':
+ return key;
+ default:
+ let formatted = key.replace(/([A-Z])/g, ' $1').trim();
+ formatted = formatted.replace(/_/g, ' ');
+ formatted = formatted
+ .toLowerCase()
+ .split(' ')
+ .map(word => (word.length === 0 ? '' : word.charAt(0).toUpperCase() + word.slice(1)))
+ .join(' ');
+ formatted = formatted.replace('Web Socket Com', 'WebSocket Comm');
+ formatted = formatted.replace('Unit Test', 'Unit Testing');
+ return formatted;
+ }
+};
diff --git a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx
index fe41907f68..efecb8a8ea 100644
--- a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx
+++ b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx
@@ -1,59 +1,52 @@
import { useEffect, useState } from 'react';
import axios from 'axios';
import UserCard from './UserCard';
-import './style/UserCard.module.css';
+import { useSelector } from 'react-redux';
+import styles from './style/RankedUserList.module.css';
-function RankedUserList({ selectedSkills = [] }) {
+function RankedUserList({ selectedSkills, selectedPreferences }) {
const [rankedUsers, setRankedUsers] = useState([]);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
+ const darkMode = useSelector(state => state.theme.darkMode);
useEffect(() => {
- let canceled = false;
+ if (
+ (!selectedSkills || selectedSkills.length === 0) &&
+ (!selectedPreferences || selectedPreferences.length === 0)
+ )
+ return;
const fetchRankedUsers = async () => {
setLoading(true);
- setError(null);
-
try {
const params = {};
- if (Array.isArray(selectedSkills) && selectedSkills.length > 0) {
- params.skills = selectedSkills.join(',');
- }
-
- const response = await axios.get('http://localhost:4500/api/hgnform/ranked', { params });
+ if (selectedSkills.length > 0) params.skills = selectedSkills.join(',');
+ if (selectedPreferences.length > 0) params.preferences = selectedPreferences.join(',');
- if (!canceled) {
- const data = response?.data || [];
- setRankedUsers(Array.isArray(data) ? data : []);
- }
+ const response = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/hgnform/ranked`, {
+ params,
+ });
+ setRankedUsers(response.data);
} catch (err) {
- console.error('Failed to fetch ranked users', err);
- if (!canceled) {
- setError(err);
- setRankedUsers([]);
- }
} finally {
- if (!canceled) setLoading(false);
+ setLoading(false);
}
};
fetchRankedUsers();
+ }, [selectedSkills, selectedPreferences]);
- return () => {
- canceled = true;
- };
- }, [selectedSkills]);
-
- if (loading) return
Loading ranked users...
;
- if (error) return
Failed to load users.
;
- if (!rankedUsers.length) return
No members found.
;
+ if (loading) return
Loading ranked users...
;
+ if (!rankedUsers.length) return
No users found.
;
return (
-
- {rankedUsers.map(user => (
-
- ))}
+
+
+ {rankedUsers.map(user => (
+
+
+
+ ))}
+
);
}
diff --git a/src/components/HGNHelpSkillsDashboard/UserCard.jsx b/src/components/HGNHelpSkillsDashboard/UserCard.jsx
index dfbe5f19b2..2adee3226a 100644
--- a/src/components/HGNHelpSkillsDashboard/UserCard.jsx
+++ b/src/components/HGNHelpSkillsDashboard/UserCard.jsx
@@ -1,27 +1,22 @@
-import React from 'react';
import styles from './style/UserCard.module.css';
import avatar from './style/avatar.png';
import emailIcon from './style/email_icon.png';
import slackIcon from './style/slack_icon.png';
-
import { useSelector } from 'react-redux';
function UserCard({ user }) {
- const darkMode = useSelector(state => state.theme?.darkMode);
const { name, email, slack, score, topSkills } = user;
-
+ const darkMode = useSelector(state => state.theme.darkMode);
const getScoreColor = userScore => {
if (userScore >= 5) return '#00754A';
return '#D93D3D';
};
return (
-
+
-
- {name}
-
+
{name}
{email && (

@@ -47,9 +42,7 @@ function UserCard({ user }) {
Top Skills:
-
- {Array.isArray(topSkills) ? topSkills.join(', ') : topSkills || ''}
-
+
{topSkills.join(', ')}
diff --git a/src/components/HGNHelpSkillsDashboard/UserProfilePage.jsx b/src/components/HGNHelpSkillsDashboard/UserProfilePage.jsx
index 016889fff2..7f8a01756b 100644
--- a/src/components/HGNHelpSkillsDashboard/UserProfilePage.jsx
+++ b/src/components/HGNHelpSkillsDashboard/UserProfilePage.jsx
@@ -25,209 +25,36 @@ import {
Tooltip,
} from 'recharts';
import { ENDPOINTS } from '~/utils/URL';
-import styles from './style/UserCard.module.css';
-import pageStyles from './style/UserProfilePage.module.css';
-
-// Sample data for skills
-const mockSkillsData = {
- Frontend: [
- {
- id: 'fe1',
- name: 'UX/UI Design',
- score: 8,
- question: 'How comfortable are you with UX/UI Design principles and implementation?',
- },
- {
- id: 'fe2',
- name: 'Bootstrap',
- score: 4,
- question: 'Rate your proficiency with the Bootstrap framework',
- },
- {
- id: 'fe3',
- name: 'Advanced React',
- score: 10,
- question:
- 'How would you rate your expertise with advanced React concepts like hooks, context API, and optimizations?',
- },
- {
- id: 'fe4',
- name: 'Overall Frontend',
- score: 3,
- question: 'Rate your overall frontend development skills',
- },
- {
- id: 'fe5',
- name: 'Web Sockets',
- score: 1,
- question: 'How comfortable are you integrating web sockets in frontend applications?',
- },
- {
- id: 'fe6',
- name: 'HTML Semantics',
- score: 2,
- question: 'Rate your knowledge of semantic HTML structure and accessibility',
- },
- {
- id: 'fe7',
- name: 'CSS Advanced',
- score: 9,
- question:
- 'How would you rate your expertise with CSS preprocessing, animations, and layouts?',
- },
- {
- id: 'fe8',
- name: 'Redux',
- score: 7,
- question: 'Rate your proficiency with Redux state management and middleware',
- },
- {
- id: 'fe9',
- name: 'Responsive UI',
- score: 6,
- question:
- 'How proficient are you with implementing responsive design across different devices?',
- },
- {
- id: 'fe10',
- name: 'Figma',
- score: 5,
- question: 'Rate your proficiency with Figma for UI/UX design',
- },
- ],
- Backend: [
- {
- id: 'be1',
- name: 'Backend',
- score: 6,
- question: 'Rate your overall backend development skills',
- },
- {
- id: 'be2',
- name: 'TDD Backend',
- score: 5,
- question: 'How comfortable are you with Test-Driven Development for backend?',
- },
- {
- id: 'be3',
- name: 'Database',
- score: 8,
- question: 'Rate your knowledge of database design and management',
- },
- {
- id: 'be4',
- name: 'MongoDB',
- score: 7,
- question: 'How would you rate your expertise with MongoDB?',
- },
- {
- id: 'be5',
- name: 'Mock MongoDB',
- score: 4,
- question: 'Rate your proficiency with mocking MongoDB for testing',
- },
- {
- id: 'be6',
- name: 'MERN Stack',
- score: 9,
- question: 'How comfortable are you working with the complete MERN stack?',
- },
- ],
- DevOps: [
- {
- id: 'devops1',
- name: 'Deployment',
- score: 5,
- question: 'How comfortable are you with deploying applications to production?',
- },
- {
- id: 'devops2',
- name: 'Version Control',
- score: 8,
- question: 'Rate your proficiency with version control systems like Git',
- },
- {
- id: 'devops3',
- name: 'Env Setup',
- score: 3,
- question: 'How would you rate your expertise with setting up development environments?',
- },
- {
- id: 'devops4',
- name: 'Testing',
- score: 7,
- question: 'Rate your knowledge of different testing methodologies',
- },
- ],
- SWPractices: [
- {
- id: 'sp1',
- name: 'Agile',
- score: 7,
- question: 'How comfortable are you working in an Agile environment?',
- },
- {
- id: 'sp2',
- name: 'Code Review',
- score: 9,
- question: 'Rate your proficiency with conducting thorough code reviews',
- },
- {
- id: 'sp3',
- name: 'Advanced Coding',
- score: 6,
- question: 'How would you rate your expertise with advanced coding practices?',
- },
- {
- id: 'sp4',
- name: 'Documentation',
- score: 8,
- question: 'Rate your skill with creating clear and comprehensive documentation',
- },
- {
- id: 'sp5',
- name: 'Markdown & Graphs',
- score: 5,
- question: 'How comfortable are you with markdown and creating graphs/charts?',
- },
- {
- id: 'sp6',
- name: 'Leadership Skills',
- score: 7,
- question: 'Rate your leadership skills within a development team',
- },
- ],
-};
+import styles from './style/UserProfile.module.css';
+// Custom tooltip for RadarChart
function CustomTooltip({ active, payload }) {
if (active && payload && payload.length) {
+ const data = payload[0].payload;
return (
-
-
{payload[0].payload.name}
-
+
+
{data.name}
+
Score:{' '}
-
- {payload[0].value}
+
+ {data.value}
-
{payload[0].payload.question}
+
{data.question}
);
}
return null;
}
+// Single skill card
function SkillItem({ item }) {
return (
-