diff --git a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx index 94c7f11df2..ba68f1e2f4 100644 --- a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx +++ b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx @@ -10,10 +10,12 @@ function CommunityMembersPage() { const [selectedSkills, setSelectedSkills] = useState([]); const [selectedPreferences, setSelectedPreferences] = useState([]); const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState('name'); + const [sortOrder, setSortOrder] = useState('asc'); const darkMode = useSelector(state => state.theme.darkMode); - const hasFilters = - selectedSkills.length > 0 || selectedPreferences.length > 0 || searchQuery.trim().length > 0; + const handleSortByChange = event => setSortBy(event.target.value); + const handleSortOrderChange = event => setSortOrder(event.target.value); return (
@@ -21,6 +23,34 @@ function CommunityMembersPage() { +
+ + + + + +
+ @@ -33,17 +63,13 @@ function CommunityMembersPage() {
- {hasFilters ? ( - - ) : ( -

- Search or select skills and preferences above to see filtered members. -

- )} +
); diff --git a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx index 27c288b031..b3edd73ae2 100644 --- a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx +++ b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx @@ -5,7 +5,64 @@ import { useSelector } from 'react-redux'; import styles from './style/RankedUserList.module.css'; import UserCard from './UserCard'; -function RankedUserList({ selectedSkills, selectedPreferences, searchQuery }) { +const extractSkillEntries = skillData => { + if (!skillData || typeof skillData !== 'object') return []; + + if (Array.isArray(skillData)) { + return skillData.flatMap(skill => { + if (typeof skill === 'string') return [{ name: skill, rating: undefined }]; + if (skill && typeof skill === 'object') { + const name = skill.name || skill.skill || skill.label || skill.type; + return name ? [{ name, rating: skill.rating ?? skill.score ?? undefined }] : []; + } + return []; + }); + } + + return Object.values(skillData).flatMap(section => { + if (!section || typeof section !== 'object') return []; + if (Array.isArray(section)) { + return section.flatMap(skill => { + if (typeof skill === 'string') return [{ name: skill, rating: undefined }]; + if (skill && typeof skill === 'object') { + const name = skill.name || skill.skill || skill.label || skill.type; + return name ? [{ name, rating: skill.rating ?? skill.score ?? undefined }] : []; + } + return []; + }); + } + + return Object.entries(section).map(([skillName, rating]) => ({ + name: skillName, + rating: typeof rating === 'number' ? rating : undefined, + })); + }); +}; + +const normalizeUser = user => { + if (Array.isArray(user.topSkills) && user.topSkills.length > 0) return user; + + const rawSkills = user.skills; + const skillEntries = extractSkillEntries(rawSkills); + + const uniqueSkills = Array.from( + new Map( + skillEntries.filter(entry => entry.name).map(entry => [entry.name.toLowerCase(), entry]), + ).values(), + ); + + const sortedSkills = uniqueSkills + .sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)) + .map(entry => entry.name); + + return { + ...user, + topSkills: sortedSkills, + skills: sortedSkills, + }; +}; + +function RankedUserList({ selectedSkills, selectedPreferences, searchQuery, sortBy, sortOrder }) { const [allUsers, setAllUsers] = useState([]); const [loading, setLoading] = useState(true); const darkMode = useSelector(state => state.theme.darkMode); @@ -15,14 +72,28 @@ function RankedUserList({ selectedSkills, selectedPreferences, searchQuery }) { setLoading(true); try { const params = {}; + const hasFilters = + (selectedSkills && selectedSkills.length > 0) || + (selectedPreferences && selectedPreferences.length > 0) || + (searchQuery && searchQuery.trim().length > 0); + if (selectedSkills && selectedSkills.length > 0) params.skills = selectedSkills.join(','); if (selectedPreferences && selectedPreferences.length > 0) params.preferences = selectedPreferences.join(','); + if (searchQuery && searchQuery.trim().length > 0) params.search = searchQuery.trim(); - const response = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/hgnform/ranked`, { + const endpoint = hasFilters + ? `${process.env.REACT_APP_APIENDPOINT}/hgnform/ranked` + : `${process.env.REACT_APP_APIENDPOINT}/hgnHelp/community`; + + if (!hasFilters && sortBy === 'name' && sortOrder) { + params.sortOrder = sortOrder; + } + + const response = await axios.get(endpoint, { params, }); - setAllUsers(response.data); + setAllUsers(response.data.map(normalizeUser)); } catch (err) { // error handled silently } finally { @@ -31,8 +102,7 @@ function RankedUserList({ selectedSkills, selectedPreferences, searchQuery }) { }; fetchUsers(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedSkills, selectedPreferences]); + }, [selectedSkills, selectedPreferences, searchQuery, sortOrder]); // Client-side filter by searchQuery on top of API results const filteredUsers = searchQuery @@ -45,13 +115,33 @@ function RankedUserList({ selectedSkills, selectedPreferences, searchQuery }) { }) : allUsers; + const sortedUsers = [...filteredUsers].sort((a, b) => { + if (sortBy === 'score') { + const scoreA = typeof a.score === 'number' ? a.score : -Infinity; + const scoreB = typeof b.score === 'number' ? b.score : -Infinity; + if (scoreA < scoreB) return sortOrder === 'desc' ? 1 : -1; + if (scoreA > scoreB) return sortOrder === 'desc' ? -1 : 1; + const nameA = (a.name || '').toLowerCase(); + const nameB = (b.name || '').toLowerCase(); + if (nameA < nameB) return -1; + if (nameA > nameB) return 1; + return 0; + } + + const nameA = (a.name || '').toLowerCase(); + const nameB = (b.name || '').toLowerCase(); + if (nameA < nameB) return sortOrder === 'desc' ? 1 : -1; + if (nameA > nameB) return sortOrder === 'desc' ? -1 : 1; + return 0; + }); + if (loading) return

Loading ranked users...

; - if (!filteredUsers.length) return

No users found.

; + if (!sortedUsers.length) return

No users found.

; return (
- {filteredUsers.map(user => ( + {sortedUsers.map(user => (
@@ -65,6 +155,8 @@ RankedUserList.propTypes = { selectedSkills: PropTypes.arrayOf(PropTypes.string), selectedPreferences: PropTypes.arrayOf(PropTypes.string), searchQuery: PropTypes.string, + sortBy: PropTypes.string, + sortOrder: PropTypes.string, }; export default RankedUserList; diff --git a/src/components/HGNHelpSkillsDashboard/UserCard.jsx b/src/components/HGNHelpSkillsDashboard/UserCard.jsx index eb7363bdb8..b97b7ccd6a 100644 --- a/src/components/HGNHelpSkillsDashboard/UserCard.jsx +++ b/src/components/HGNHelpSkillsDashboard/UserCard.jsx @@ -5,9 +5,20 @@ import slackIcon from './style/slack_icon.png'; import { useSelector } from 'react-redux'; function UserCard({ user }) { - const { name, email, slack, score, topSkills } = user; + const { name, email, slack, score, topSkills, skills } = user; const darkMode = useSelector(state => state.theme.darkMode); + const normalizedSkills = Array.isArray(topSkills) + ? topSkills + : Array.isArray(skills) + ? skills + .map(skill => { + if (typeof skill === 'string') return skill; + return skill.name || skill.skill || skill.label || skill.type || ''; + }) + .filter(Boolean) + : []; + return (
Avatar @@ -40,7 +51,7 @@ function UserCard({ user }) {
Top Skills:
-
{topSkills.join(', ')}
+
{normalizedSkills.join(', ')}
diff --git a/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css index 42eb579511..13fb9b9bd7 100644 --- a/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css +++ b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css @@ -113,16 +113,17 @@ .darkMode .preferenceButton:hover { background-color: #374151; } + .darkMode .skillButton.selected { background-color: #3b82f6; border-color: #3b82f6; - color: #ffffff; + color: #fff; } .darkMode .preferenceButton.selected { background-color: #22c55e; border-color: #22c55e; - color: #ffffff; + color: #fff; } .searchContainer { @@ -144,6 +145,33 @@ color: #000; } +.toolbar { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; + justify-content: flex-end; +} + +.sortLabel { + font-size: 0.95rem; + font-weight: 600; +} + +.sortSelect { + border: 1px solid #d1d5db; + border-radius: 0.5rem; + padding: 0.4rem 0.75rem; + background: white; + color: #111827; +} + +.darkMode .sortSelect { + background: #1f2937; + color: #f9fafb; + border-color: #4b5563; +} + .clearButton { background: none; border: none;