-
-
Notifications
You must be signed in to change notification settings - Fork 77
Deep implement community members list #4281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4931a1a
0ffaeec
cb1d1a3
bd95851
6792ffe
904f9e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,154 @@ | ||
| 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 () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fetch always sends either the selected skills or the fixed availableSkills list. That means the “load all members up front” requirement is never met; users outside that seven-skill whitelist can never appear, breaking search/filter parity. |
||
| 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 { | ||
|
Check warning on line 30 in src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx
|
||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| fetchRankedUsers(); | ||
| }, [selectedSkills]); | ||
|
|
||
| const handleCheckboxChange = skill => { | ||
| setSelectedSkills(prev => | ||
| prev.includes(skill) ? prev.filter(s => s !== skill) : [...prev, skill], | ||
| ); | ||
| }; | ||
|
|
||
| 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 ( | ||
| <div> | ||
| <h2>Select Skills to Filter Community Members</h2> | ||
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}> | ||
| {availableSkills.map(skill => ( | ||
| <label key={skill}> | ||
| <input | ||
| type="checkbox" | ||
| checked={selectedSkills.includes(skill)} | ||
| onChange={() => handleCheckboxChange(skill)} | ||
| /> | ||
| {skill} | ||
| </label> | ||
| ))} | ||
| <div className={styles.container}> | ||
| <h1 className={styles.heading}>One Community Members</h1> | ||
| <div className={styles.controlsRow}> | ||
| <div className={styles.searchWrapper}> | ||
| <input | ||
| type="search" | ||
| value={searchTerm} | ||
| onChange={event => setSearchTerm(event.target.value)} | ||
| placeholder="Search by team member name or skills" | ||
| className={styles.searchInput} | ||
| aria-label="Search community members" | ||
| /> | ||
| </div> | ||
| <button | ||
| type="button" | ||
| className={styles.filterButton} | ||
| onClick={() => setShowFilters(prev => !prev)} | ||
| > | ||
| {showFilters ? 'Hide Filters' : 'Filter'} | ||
| </button> | ||
| <button type="button" className={styles.sortButton} onClick={toggleSortOrder}> | ||
| {sortOrder === 'asc' ? 'A→Z Sort' : 'Z→A Sort'} | ||
| </button> | ||
| {(selectedSkills.length > 0 || searchTerm) && ( | ||
| <button | ||
| type="button" | ||
| className={styles.clearButton} | ||
| onClick={() => { | ||
| clearFilters(); | ||
| setSearchTerm(''); | ||
| }} | ||
| > | ||
| Clear All | ||
| </button> | ||
| )} | ||
| </div> | ||
|
|
||
| {selectedSkills.length > 0 && <RankedUserList selectedSkills={selectedSkills} />} | ||
| {showFilters && ( | ||
| <div className={styles.filtersPanel}> | ||
| {availableSkills.map(skill => ( | ||
| <label key={skill} className={styles.filterOption}> | ||
| <input | ||
| type="checkbox" | ||
| checked={selectedSkills.includes(skill)} | ||
| onChange={() => handleCheckboxChange(skill)} | ||
| /> | ||
| <span>{skill}</span> | ||
| </label> | ||
| ))} | ||
| </div> | ||
| )} | ||
|
|
||
| <p className={styles.helperText}> | ||
| 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. | ||
| </p> | ||
|
|
||
| <RankedUserList | ||
| users={filteredUsers} | ||
| loading={loading} | ||
| error={error} | ||
| emptyMessage={emptyMessage} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <p>Loading community members...</p>; | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| if (!selectedSkills || selectedSkills.length === 0) return; | ||
| if (error) { | ||
| return <p className="text-danger">{error}</p>; | ||
| } | ||
|
|
||
| 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 <p>Loading ranked users...</p>; | ||
| if (!users.length) { | ||
| return <p>{emptyMessage}</p>; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="user-card-container"> | ||
| {rankedUsers.map(user => ( | ||
| <UserCard key={user._id} user={user} /> | ||
| <div className={styles.containerGrid}> | ||
| {users.map(user => ( | ||
| <UserCard key={user._id || user.email || user.name} user={user} /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Check warning on line 62 in src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css
|
||
| } | ||
|
|
||
| .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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RANKED_USERS_ENDPOINT is hard-coded to http://localhost:4500, so the page will 404 in any deployed environment unless a proxy rewrites it. Current frontend standards recommend sourcing API roots from environment config to keep builds environment-agnostic and testable.