Skip to content

Commit e1f8947

Browse files
Merge pull request #4281 from OneCommunityGlobal/deep-implement-community-members-list
Deep implement community members list
2 parents 261c7e9 + 904f9e2 commit e1f8947

5 files changed

Lines changed: 287 additions & 65 deletions

File tree

src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,154 @@
1-
import { useState } from 'react';
2-
import RankedUserList from './RankedUserList'; // wherever your RankedUserList is
1+
import { useEffect, useMemo, useState } from 'react';
2+
import axios from 'axios';
3+
import RankedUserList from './RankedUserList';
4+
import styles from './style/CommunityMembersPage.module.css';
35

46
const availableSkills = ['React', 'Redux', 'HTML', 'CSS', 'MongoDB', 'Database', 'Agile'];
7+
const RANKED_USERS_ENDPOINT = 'http://localhost:4500/api/hgnform/ranked';
58

69
function CommunityMembersPage() {
710
const [selectedSkills, setSelectedSkills] = useState([]);
11+
const [searchTerm, setSearchTerm] = useState('');
12+
const [sortOrder, setSortOrder] = useState('asc');
13+
const [showFilters, setShowFilters] = useState(false);
14+
const [rankedUsers, setRankedUsers] = useState([]);
15+
const [loading, setLoading] = useState(true);
16+
const [error, setError] = useState(null);
17+
18+
useEffect(() => {
19+
const fetchRankedUsers = async () => {
20+
setLoading(true);
21+
try {
22+
const params = {
23+
skills: (selectedSkills.length ? selectedSkills : availableSkills).join(','),
24+
};
25+
const response = await axios.get(RANKED_USERS_ENDPOINT, { params });
26+
setRankedUsers(response.data);
27+
setError(null);
28+
} catch (err) {
29+
setError('Unable to load community members right now. Please try again later.');
30+
} finally {
31+
setLoading(false);
32+
}
33+
};
34+
35+
fetchRankedUsers();
36+
}, [selectedSkills]);
837

938
const handleCheckboxChange = skill => {
1039
setSelectedSkills(prev =>
1140
prev.includes(skill) ? prev.filter(s => s !== skill) : [...prev, skill],
1241
);
1342
};
1443

44+
const toggleSortOrder = () => {
45+
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
46+
};
47+
48+
const clearFilters = () => {
49+
setSelectedSkills([]);
50+
};
51+
52+
const filteredUsers = useMemo(() => {
53+
const normalizedSearch = searchTerm.trim().toLowerCase();
54+
const normalizedSelectedSkills = selectedSkills.map(skill => skill.toLowerCase());
55+
let result = rankedUsers;
56+
57+
if (normalizedSearch) {
58+
result = rankedUsers.filter(user => {
59+
const nameMatches = user.name?.toLowerCase().includes(normalizedSearch);
60+
const skillMatches = Array.isArray(user.topSkills)
61+
? user.topSkills.some(skill => skill.toLowerCase().includes(normalizedSearch))
62+
: false;
63+
return nameMatches || skillMatches;
64+
});
65+
}
66+
67+
if (normalizedSelectedSkills.length) {
68+
result = result.filter(user => {
69+
if (!Array.isArray(user.topSkills) || user.topSkills.length === 0) return false;
70+
return user.topSkills.some(skill =>
71+
normalizedSelectedSkills.includes((skill || '').toLowerCase()),
72+
);
73+
});
74+
}
75+
76+
return [...result].sort((a, b) => {
77+
const first = a.name || '';
78+
const second = b.name || '';
79+
return sortOrder === 'asc' ? first.localeCompare(second) : second.localeCompare(first);
80+
});
81+
}, [rankedUsers, searchTerm, sortOrder, selectedSkills]);
82+
83+
const emptyMessage =
84+
searchTerm || selectedSkills.length
85+
? 'No community members match your current filters.'
86+
: 'No community members available yet.';
87+
1588
return (
16-
<div>
17-
<h2>Select Skills to Filter Community Members</h2>
18-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
19-
{availableSkills.map(skill => (
20-
<label key={skill}>
21-
<input
22-
type="checkbox"
23-
checked={selectedSkills.includes(skill)}
24-
onChange={() => handleCheckboxChange(skill)}
25-
/>
26-
{skill}
27-
</label>
28-
))}
89+
<div className={styles.container}>
90+
<h1 className={styles.heading}>One Community Members</h1>
91+
<div className={styles.controlsRow}>
92+
<div className={styles.searchWrapper}>
93+
<input
94+
type="search"
95+
value={searchTerm}
96+
onChange={event => setSearchTerm(event.target.value)}
97+
placeholder="Search by team member name or skills"
98+
className={styles.searchInput}
99+
aria-label="Search community members"
100+
/>
101+
</div>
102+
<button
103+
type="button"
104+
className={styles.filterButton}
105+
onClick={() => setShowFilters(prev => !prev)}
106+
>
107+
{showFilters ? 'Hide Filters' : 'Filter'}
108+
</button>
109+
<button type="button" className={styles.sortButton} onClick={toggleSortOrder}>
110+
{sortOrder === 'asc' ? 'A→Z Sort' : 'Z→A Sort'}
111+
</button>
112+
{(selectedSkills.length > 0 || searchTerm) && (
113+
<button
114+
type="button"
115+
className={styles.clearButton}
116+
onClick={() => {
117+
clearFilters();
118+
setSearchTerm('');
119+
}}
120+
>
121+
Clear All
122+
</button>
123+
)}
29124
</div>
30125

31-
{selectedSkills.length > 0 && <RankedUserList selectedSkills={selectedSkills} />}
126+
{showFilters && (
127+
<div className={styles.filtersPanel}>
128+
{availableSkills.map(skill => (
129+
<label key={skill} className={styles.filterOption}>
130+
<input
131+
type="checkbox"
132+
checked={selectedSkills.includes(skill)}
133+
onChange={() => handleCheckboxChange(skill)}
134+
/>
135+
<span>{skill}</span>
136+
</label>
137+
))}
138+
</div>
139+
)}
140+
141+
<p className={styles.helperText}>
142+
When multiple filters are selected, the score represents the average value, and the options
143+
are ranked based on their scoring. Click each profile to learn more details.
144+
</p>
145+
146+
<RankedUserList
147+
users={filteredUsers}
148+
loading={loading}
149+
error={error}
150+
emptyMessage={emptyMessage}
151+
/>
32152
</div>
33153
);
34154
}
Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,38 @@
1-
import { useEffect, useState } from 'react';
2-
import axios from 'axios';
1+
import PropTypes from 'prop-types';
32
import UserCard from './UserCard';
4-
import './style/UserCard.module.css';
3+
import styles from './style/UserCard.module.css';
54

6-
function RankedUserList({ selectedSkills }) {
7-
const [rankedUsers, setRankedUsers] = useState([]);
8-
const [loading, setLoading] = useState(true);
5+
function RankedUserList({ users, loading, error, emptyMessage }) {
6+
if (loading) {
7+
return <p>Loading community members...</p>;
8+
}
99

10-
useEffect(() => {
11-
if (!selectedSkills || selectedSkills.length === 0) return;
10+
if (error) {
11+
return <p className="text-danger">{error}</p>;
12+
}
1213

13-
const fetchRankedUsers = async () => {
14-
setLoading(true);
15-
try {
16-
const response = await axios.get('http://localhost:4500/api/hgnform/ranked', {
17-
params: { skills: selectedSkills.join(',') },
18-
});
19-
setRankedUsers(response.data);
20-
} catch (err) {
21-
// console.error('Error fetching ranked users:', err);
22-
} finally {
23-
setLoading(false);
24-
}
25-
};
26-
27-
fetchRankedUsers();
28-
}, [selectedSkills]);
29-
30-
if (loading) return <p>Loading ranked users...</p>;
14+
if (!users.length) {
15+
return <p>{emptyMessage}</p>;
16+
}
3117

3218
return (
33-
<div className="user-card-container">
34-
{rankedUsers.map(user => (
35-
<UserCard key={user._id} user={user} />
19+
<div className={styles.containerGrid}>
20+
{users.map(user => (
21+
<UserCard key={user._id || user.email || user.name} user={user} />
3622
))}
3723
</div>
3824
);
3925
}
4026

27+
RankedUserList.propTypes = {
28+
users: PropTypes.arrayOf(PropTypes.object).isRequired,
29+
loading: PropTypes.bool.isRequired,
30+
error: PropTypes.string,
31+
emptyMessage: PropTypes.string.isRequired,
32+
};
33+
34+
RankedUserList.defaultProps = {
35+
error: null,
36+
};
37+
4138
export default RankedUserList;

src/components/HGNHelpSkillsDashboard/UserCard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import emailIcon from './style/email_icon.png';
44
import slackIcon from './style/slack_icon.png';
55

66
function UserCard({ user }) {
7-
const { name, email, slack, score, topSkills } = user;
7+
const { name, email, slack, score, topSkills = [] } = user;
88

99
const getScoreColor = userScore => {
1010
if (userScore >= 5) return '#00754A';
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
.container {
2+
max-width: 1200px;
3+
margin: 0 auto;
4+
padding: 32px 24px 48px;
5+
}
6+
7+
.heading {
8+
font-size: 28px;
9+
font-weight: 600;
10+
margin-bottom: 16px;
11+
color: #222222;
12+
}
13+
14+
.controlsRow {
15+
display: flex;
16+
flex-wrap: wrap;
17+
align-items: center;
18+
gap: 12px;
19+
margin-bottom: 16px;
20+
}
21+
22+
.searchWrapper {
23+
flex: 1 1 280px;
24+
min-width: 240px;
25+
}
26+
27+
.searchInput {
28+
width: 100%;
29+
border: 1px solid #cccccc;
30+
border-radius: 6px;
31+
padding: 10px 12px;
32+
font-size: 16px;
33+
}
34+
35+
.filterButton,
36+
.sortButton,
37+
.clearButton {
38+
border: 1px solid #1f6feb;
39+
background-color: transparent;
40+
color: #1f6feb;
41+
border-radius: 6px;
42+
font-size: 15px;
43+
padding: 9px 16px;
44+
cursor: pointer;
45+
transition: background-color 0.2s ease, color 0.2s ease;
46+
}
47+
48+
.filterButton:hover,
49+
.sortButton:hover,
50+
.clearButton:hover {
51+
background-color: #1f6feb;
52+
color: #ffffff;
53+
}
54+
55+
.clearButton {
56+
border-color: #d93d3d;
57+
color: #d93d3d;
58+
}
59+
60+
.clearButton:hover {
61+
background-color: #d93d3d;
62+
color: #ffffff;
63+
}
64+
65+
.filtersPanel {
66+
display: flex;
67+
flex-wrap: wrap;
68+
gap: 12px;
69+
margin-bottom: 16px;
70+
padding: 12px;
71+
border: 1px solid #e0e0e0;
72+
border-radius: 8px;
73+
background-color: #fafafa;
74+
}
75+
76+
.filterOption {
77+
display: flex;
78+
align-items: center;
79+
gap: 6px;
80+
font-size: 15px;
81+
color: #333333;
82+
}
83+
84+
.helperText {
85+
font-size: 14px;
86+
color: #555555;
87+
margin-bottom: 20px;
88+
}

0 commit comments

Comments
 (0)