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 === '' && ( + <> +
+ +
+
+ +
+ + )} +
+ opt.value === comparisonType) + : null + } + onChange={handleComparisonTypeChange} + placeholder="Comparison Type" + classNamePrefix={darkMode ? 'hgn-select-dark' : 'hgn-select'} + styles={darkMode ? getDarkModeSelectStyles(false) : undefined} + /> +
+
+ + {/* 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 => ( - - ))} -
-
+
+

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 ( -
+
Avatar
-
- {name} -
+
{name}
{email && (
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 ( -
-
+
+
{item.score}
-
{item.name}
+
{item.name}
{item.question} @@ -235,17 +62,17 @@ function SkillItem({ item }) { ); } -// Separate Skills Tabbed Section component +// Skills tabbed section function SkillsTabbedSection({ skillsData }) { const [activeTab, setActiveTab] = useState('Dashboard'); + const allSkills = [ - ...skillsData.Frontend, - ...skillsData.Backend, - ...skillsData.DevOps, - ...skillsData.SWPractices, + ...(skillsData.Frontend || []), + ...(skillsData.Backend || []), + ...(skillsData.DevOps || []), + ...(skillsData.SWPractices || []), ]; - // Format all skills for the radar chart const radarData = allSkills.map(skill => ({ name: skill.name, value: skill.score, @@ -256,40 +83,24 @@ function SkillsTabbedSection({ skillsData }) { if (activeTab !== tab) setActiveTab(tab); }; - const renderScoreItems = items => { - return ( - - {items.map(item => ( - - - - ))} - - ); - }; - - const renderRadarChart = () => { - return ( - - - - - - - } /> - - - ); - }; + const renderScoreItems = items => ( + + {items.map(item => ( + + + + ))} + + ); return ( - -
Skills
- - {/* Tabs */} - -