Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions src/components/HGNHelpSkillsDashboard/SkillsOverviewPage.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,25 +16,56 @@ 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 = <p className={styles.noDataMsg}>Loading your skills radar...</p>;
} else if (profileData?.skillInfo) {
radarContent = <RadarChart profileData={profileData} compact={false} />;
} else {
radarContent = (
<p className={styles.noDataMsg}>Complete the skills survey to view your radar chart.</p>
);
}

return (
<div className={`${styles.container} ${darkMode ? styles.darkMode : ''}`}>
<h1 className={styles.title}>Skills Overview</h1>

{/* Radar Chart for logged-in user */}
<div className={`${styles.radarSection} ${darkMode ? styles.radarDark : ''}`}>
<h2 className={styles.sectionTitle}>Your Skills Radar</h2>
<div className={styles.radarWrapper}>
{userProfile?.skillInfo ? (
<RadarChart profileData={userProfile} compact={false} />
) : (
<p className={styles.noDataMsg}>Complete the skills survey to view your radar chart.</p>
)}
</div>
<div className={styles.radarWrapper}>{radarContent}</div>
</div>

{/* Search Bar */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down Expand Up @@ -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',
},
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -300,6 +305,7 @@ function RadarChart({ profileData, compact = true, onSkillsDataReady }) {
},

pointLabels: {
centerPointLabels: true,
font: {
size: function(context) {
const w = context.chart.width;
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <canvas>, 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(
<Provider store={store}>
<RadarChart profileData={profileData} compact={compact} />
</Provider>,
);
};

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);
});
});
Loading