diff --git a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx index 44dbe0d734..bd56c2f0d0 100644 --- a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx +++ b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx @@ -1,10 +1,39 @@ -import { useState } from 'react'; -import RankedUserList from './RankedUserList'; // wherever your RankedUserList is +import { useEffect, useMemo, useState } from 'react'; +import axios from 'axios'; +import RankedUserList from './RankedUserList'; +import styles from './style/CommunityMembersPage.module.css'; const availableSkills = ['React', 'Redux', 'HTML', 'CSS', 'MongoDB', 'Database', 'Agile']; +const RANKED_USERS_ENDPOINT = 'http://localhost:4500/api/hgnform/ranked'; function CommunityMembersPage() { const [selectedSkills, setSelectedSkills] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [sortOrder, setSortOrder] = useState('asc'); + const [showFilters, setShowFilters] = useState(false); + const [rankedUsers, setRankedUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRankedUsers = async () => { + setLoading(true); + try { + const params = { + skills: (selectedSkills.length ? selectedSkills : availableSkills).join(','), + }; + const response = await axios.get(RANKED_USERS_ENDPOINT, { params }); + setRankedUsers(response.data); + setError(null); + } catch (err) { + setError('Unable to load community members right now. Please try again later.'); + } finally { + setLoading(false); + } + }; + + fetchRankedUsers(); + }, [selectedSkills]); const handleCheckboxChange = skill => { setSelectedSkills(prev => @@ -12,23 +41,114 @@ function CommunityMembersPage() { ); }; + const toggleSortOrder = () => { + setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc')); + }; + + const clearFilters = () => { + setSelectedSkills([]); + }; + + const filteredUsers = useMemo(() => { + const normalizedSearch = searchTerm.trim().toLowerCase(); + const normalizedSelectedSkills = selectedSkills.map(skill => skill.toLowerCase()); + let result = rankedUsers; + + if (normalizedSearch) { + result = rankedUsers.filter(user => { + const nameMatches = user.name?.toLowerCase().includes(normalizedSearch); + const skillMatches = Array.isArray(user.topSkills) + ? user.topSkills.some(skill => skill.toLowerCase().includes(normalizedSearch)) + : false; + return nameMatches || skillMatches; + }); + } + + if (normalizedSelectedSkills.length) { + result = result.filter(user => { + if (!Array.isArray(user.topSkills) || user.topSkills.length === 0) return false; + return user.topSkills.some(skill => + normalizedSelectedSkills.includes((skill || '').toLowerCase()), + ); + }); + } + + return [...result].sort((a, b) => { + const first = a.name || ''; + const second = b.name || ''; + return sortOrder === 'asc' ? first.localeCompare(second) : second.localeCompare(first); + }); + }, [rankedUsers, searchTerm, sortOrder, selectedSkills]); + + const emptyMessage = + searchTerm || selectedSkills.length + ? 'No community members match your current filters.' + : 'No community members available yet.'; + return ( -
-

Select Skills to Filter Community Members

-
- {availableSkills.map(skill => ( - - ))} +
+

One Community Members

+
+
+ setSearchTerm(event.target.value)} + placeholder="Search by team member name or skills" + className={styles.searchInput} + aria-label="Search community members" + /> +
+ + + {(selectedSkills.length > 0 || searchTerm) && ( + + )}
- {selectedSkills.length > 0 && } + {showFilters && ( +
+ {availableSkills.map(skill => ( + + ))} +
+ )} + +

+ When multiple filters are selected, the score represents the average value, and the options + are ranked based on their scoring. Click each profile to learn more details. +

+ +
); } diff --git a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx index b7017b7715..50e75c1527 100644 --- a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx +++ b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx @@ -1,41 +1,38 @@ -import { useEffect, useState } from 'react'; -import axios from 'axios'; +import PropTypes from 'prop-types'; import UserCard from './UserCard'; -import './style/UserCard.module.css'; +import styles from './style/UserCard.module.css'; -function RankedUserList({ selectedSkills }) { - const [rankedUsers, setRankedUsers] = useState([]); - const [loading, setLoading] = useState(true); +function RankedUserList({ users, loading, error, emptyMessage }) { + if (loading) { + return

Loading community members...

; + } - useEffect(() => { - if (!selectedSkills || selectedSkills.length === 0) return; + if (error) { + return

{error}

; + } - const fetchRankedUsers = async () => { - setLoading(true); - try { - const response = await axios.get('http://localhost:4500/api/hgnform/ranked', { - params: { skills: selectedSkills.join(',') }, - }); - setRankedUsers(response.data); - } catch (err) { - // console.error('Error fetching ranked users:', err); - } finally { - setLoading(false); - } - }; - - fetchRankedUsers(); - }, [selectedSkills]); - - if (loading) return

Loading ranked users...

; + if (!users.length) { + return

{emptyMessage}

; + } return ( -
- {rankedUsers.map(user => ( - +
+ {users.map(user => ( + ))}
); } +RankedUserList.propTypes = { + users: PropTypes.arrayOf(PropTypes.object).isRequired, + loading: PropTypes.bool.isRequired, + error: PropTypes.string, + emptyMessage: PropTypes.string.isRequired, +}; + +RankedUserList.defaultProps = { + error: null, +}; + export default RankedUserList; diff --git a/src/components/HGNHelpSkillsDashboard/UserCard.jsx b/src/components/HGNHelpSkillsDashboard/UserCard.jsx index 74e151a495..ed83b38e0c 100644 --- a/src/components/HGNHelpSkillsDashboard/UserCard.jsx +++ b/src/components/HGNHelpSkillsDashboard/UserCard.jsx @@ -4,7 +4,7 @@ import emailIcon from './style/email_icon.png'; import slackIcon from './style/slack_icon.png'; function UserCard({ user }) { - const { name, email, slack, score, topSkills } = user; + const { name, email, slack, score, topSkills = [] } = user; const getScoreColor = userScore => { if (userScore >= 5) return '#00754A'; diff --git a/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css new file mode 100644 index 0000000000..9fb9dc30b5 --- /dev/null +++ b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css @@ -0,0 +1,88 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 32px 24px 48px; +} + +.heading { + font-size: 28px; + font-weight: 600; + margin-bottom: 16px; + color: #222222; +} + +.controlsRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.searchWrapper { + flex: 1 1 280px; + min-width: 240px; +} + +.searchInput { + width: 100%; + border: 1px solid #cccccc; + border-radius: 6px; + padding: 10px 12px; + font-size: 16px; +} + +.filterButton, +.sortButton, +.clearButton { + border: 1px solid #1f6feb; + background-color: transparent; + color: #1f6feb; + border-radius: 6px; + font-size: 15px; + padding: 9px 16px; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.filterButton:hover, +.sortButton:hover, +.clearButton:hover { + background-color: #1f6feb; + color: #ffffff; +} + +.clearButton { + border-color: #d93d3d; + color: #d93d3d; +} + +.clearButton:hover { + background-color: #d93d3d; + color: #ffffff; +} + +.filtersPanel { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 16px; + padding: 12px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #fafafa; +} + +.filterOption { + display: flex; + align-items: center; + gap: 6px; + font-size: 15px; + color: #333333; +} + +.helperText { + font-size: 14px; + color: #555555; + margin-bottom: 20px; +} diff --git a/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css b/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css index 0d6da0a1d2..1b1ef2b75a 100644 --- a/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css +++ b/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css @@ -1,7 +1,8 @@ .userCard { - width: 360px; + width: 100%; + max-width: 360px; height: auto; - padding: 30px 24px; + padding: 28px 24px; background: #ffffff; border: 2px solid #eeeeee; box-shadow: 1px 4px 4px rgba(0, 0, 0, 0.25); @@ -11,6 +12,7 @@ flex-direction: column; align-items: center; gap: 16px; + margin: 0 auto; } .avatar { @@ -23,31 +25,34 @@ .info { display: flex; flex-direction: column; - align-items: flex-start; /* keeps name/email/slack left-aligned */ + align-items: stretch; text-align: left; - margin-top: 16px; - padding: 0; - width: fit-content; - margin-left: auto; - margin-right: auto; + gap: 8px; + margin-top: 12px; + width: 100%; + padding: 0 16px; } .userName { - font-size: 30px; - font-weight: 400; + width: 100%; + font-size: 24px; + font-weight: 500; color: black; - margin-bottom: 8px; - white-space: nowrap; - /*font-size: Large;*/ + margin-bottom: 6px; + word-break: break-word; + line-height: 1.25; + text-align: center; } .contactLine { + width: 100%; display: flex; align-items: center; gap: 8px; - font-size: 16px; + font-size: 13px; color: #616161; margin-bottom: 6px; + flex-wrap: wrap; } .contactIcon { @@ -56,6 +61,10 @@ margin-top: 1px; } +.contactLine span { + word-break: break-all; +} + .scoreSkillsWrapper { width: 100%; display: flex; @@ -67,7 +76,7 @@ .scoreLine { font-size: 22px; text-align: left; - margin-left: 4px; + padding: 0 16px; } .scoreLabel { @@ -87,20 +96,28 @@ .skillsSection { width: 100%; text-align: left; - padding: 0 4px; + padding: 0 16px 0 16px; } .skillsLabel { - font-size: 20px; + font-size: 18px; font-weight: 400; text-decoration: underline; margin-bottom: 4px; } .skillsText { - font-size: 16px; + font-size: 15px; font-weight: 400; color: #616161; - line-height: 22px; + line-height: 21px; word-wrap: break-word; } + +.containerGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 32px; + padding: 24px 0 32px; + justify-items: center; +}