Skip to content
Merged
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
152 changes: 136 additions & 16 deletions src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx
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';
Copy link
Copy Markdown
Contributor

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.


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 () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZoepOWK9IRxVOOn-bUk&open=AZoepOWK9IRxVOOn-bUk&pullRequest=4281
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>
);
}
Expand Down
55 changes: 26 additions & 29 deletions src/components/HGNHelpSkillsDashboard/RankedUserList.jsx
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;
2 changes: 1 addition & 1 deletion src/components/HGNHelpSkillsDashboard/UserCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Text does not meet the minimal contrast requirement with its background.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZp00RR0LblY_uE4iwRa&open=AZp00RR0LblY_uE4iwRa&pullRequest=4281
}

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