diff --git a/src/components/HGNHelpSkillsDashboard/SkillsOverviewPage.jsx b/src/components/HGNHelpSkillsDashboard/SkillsOverviewPage.jsx index 191480bc0d2..e6e3cc341a2 100644 --- a/src/components/HGNHelpSkillsDashboard/SkillsOverviewPage.jsx +++ b/src/components/HGNHelpSkillsDashboard/SkillsOverviewPage.jsx @@ -1,5 +1,9 @@ -import { useState } from 'react'; +import jwtDecode from 'jwt-decode'; +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; +import config from '~/config.json'; +import httpService from '~/services/httpService'; +import { ENDPOINTS } from '~/utils/URL'; import RadarChart from '../HGNSkillsDashboard/SkillsProfilePage/components/RadarChart'; import Accordion from './Accordion'; import { PreferenceFilterButtons, SkillFilterButtons } from './FilterButtons'; @@ -12,11 +16,48 @@ function SkillsOverviewPage() { const [selectedPreferences, setSelectedPreferences] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const darkMode = useSelector(state => state.theme.darkMode); - const userProfile = useSelector(state => state.userProfile); + const [profileData, setProfileData] = useState(null); + const [loadingProfile, setLoadingProfile] = useState(true); + + // The skills survey lives behind its own endpoint and is not part of state.userProfile, + // so this page has to fetch it itself rather than read it from the store. + useEffect(() => { + const token = localStorage.getItem(config.tokenKey); + let userId; + try { + userId = token ? jwtDecode(token)?.userid : undefined; + } catch (_) { + userId = undefined; + } + + if (!userId) { + setLoadingProfile(false); + return; + } + + httpService.setjwt(token); + httpService + .get(ENDPOINTS.SKILLS_PROFILE(userId)) + // isPlaceholder means the user has not filled in the survey yet + .then(({ data }) => setProfileData(data?.isPlaceholder ? null : data)) + .catch(() => setProfileData(null)) + .finally(() => setLoadingProfile(false)); + }, []); const hasFilters = selectedSkills.length > 0 || selectedPreferences.length > 0 || searchQuery.trim().length > 0; + let radarContent; + if (loadingProfile) { + radarContent =

Loading your skills radar...

; + } else if (profileData?.skillInfo) { + radarContent = ; + } else { + radarContent = ( +

Complete the skills survey to view your radar chart.

+ ); + } + return (

Skills Overview

@@ -24,13 +65,7 @@ function SkillsOverviewPage() { {/* Radar Chart for logged-in user */}

Your Skills Radar

-
- {userProfile?.skillInfo ? ( - - ) : ( -

Complete the skills survey to view your radar chart.

- )} -
+
{radarContent}
{/* Search Bar */} diff --git a/src/components/HGNSkillsDashboard/SkillsProfilePage/components/RadarChart.jsx b/src/components/HGNSkillsDashboard/SkillsProfilePage/components/RadarChart.jsx index 29ad3f0b503..d2b3248c67d 100644 --- a/src/components/HGNSkillsDashboard/SkillsProfilePage/components/RadarChart.jsx +++ b/src/components/HGNSkillsDashboard/SkillsProfilePage/components/RadarChart.jsx @@ -24,6 +24,7 @@ const SKILL_MAPPINGS = [ }, { label: 'Leadership Skills', + shortLabel: 'Leadership Skl', value: (general, frontend, backend) => general?.leadership_skills || 0, description: 'Skills in team leadership and management', }, @@ -153,6 +154,7 @@ const SKILL_MAPPINGS = [ }, { label: 'Markdown & Graphs', + shortLabel: 'Markdown/Graphs', value: (general, frontend, backend) => frontend?.Documentation || general?.markdown_graphs || 0, description: 'Markdown writing and data visualization', }, @@ -234,7 +236,10 @@ function RadarChart({ profileData, compact = true, onSkillsDataReady }) { const { general = {}, frontend = {}, backend = {} } = profileData?.skillInfo || {}; const chartData = { - labels: skillsData.map(skill => (compact ? skill.shortLabel || skill.label : skill.label)), + // Always prefer the abbreviated label: with 28 dimensions, the wrap-around + // labels at the top (last <-> first) sit within a ~26deg arc and collide + // regardless of chart size, so shortening text is what actually buys room. + labels: skillsData.map(skill => skill.shortLabel || skill.label), datasets: [ { label: 'Skills', @@ -300,6 +305,7 @@ function RadarChart({ profileData, compact = true, onSkillsDataReady }) { }, pointLabels: { + centerPointLabels: true, font: { size: function(context) { const w = context.chart.width; @@ -321,7 +327,9 @@ function RadarChart({ profileData, compact = true, onSkillsDataReady }) { }, ticks: { stepSize: 2, - display: compact ? false : true, + // Always shown: the radial scale numbers are information, not chrome, + // so (like labels above) they shouldn't be gated behind compact. + display: true, color: darkMode ? '#dcdcdc' : '#666', font: { size: 10 }, backdropColor: darkMode ? '#1f1f1f' : '#ffffff', diff --git a/src/components/HGNSkillsDashboard/SkillsProfilePage/components/__tests__/RadarChart.test.jsx b/src/components/HGNSkillsDashboard/SkillsProfilePage/components/__tests__/RadarChart.test.jsx new file mode 100644 index 00000000000..8a05d2d18ed --- /dev/null +++ b/src/components/HGNSkillsDashboard/SkillsProfilePage/components/__tests__/RadarChart.test.jsx @@ -0,0 +1,55 @@ +import { render } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import RadarChart from '../RadarChart'; + +// react-chartjs-2 renders to a real , which jsdom doesn't implement. +// Capture the props chart.js would receive instead of rendering the chart. +let lastRadarProps; +vi.mock('react-chartjs-2', () => ({ + Radar: props => { + lastRadarProps = props; + return null; + }, +})); + +const mockStore = configureMockStore([]); + +const profileData = { + skillInfo: { + general: { leadership_experience: 5, leadership_skills: 6, markdown_graphs: 7 }, + frontend: {}, + backend: {}, + }, +}; + +const renderChart = compact => { + const store = mockStore({ theme: { darkMode: false } }); + render( + + + , + ); +}; + +describe('RadarChart', () => { + it('uses abbreviated labels even when compact is false, to avoid overlap at high dimension density', () => { + renderChart(false); + expect(lastRadarProps.data.labels).toContain('Leadership Skl'); + expect(lastRadarProps.data.labels).toContain('Markdown/Graphs'); + expect(lastRadarProps.data.labels).not.toContain('Leadership Skills'); + }); + + it('centers point labels between grid lines to reduce crowding near the wrap-around seam', () => { + renderChart(true); + expect(lastRadarProps.options.scales.r.pointLabels.centerPointLabels).toBe(true); + }); + + it('shows the radial scale numbers regardless of compact, so /hgn/profile/skills matches skills-overview', () => { + renderChart(true); + expect(lastRadarProps.options.scales.r.ticks.display).toBe(true); + + renderChart(false); + expect(lastRadarProps.options.scales.r.ticks.display).toBe(true); + }); +});