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
139 changes: 14 additions & 125 deletions src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx
Original file line number Diff line number Diff line change
@@ -1,154 +1,43 @@
import { useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import React, { useState, useMemo } from 'react';
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 =>
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.';
// EFFECTIVE SKILLS = what we pass to RankedUserList
const effectiveSkills = useMemo(() => {
return selectedSkills.length > 0 ? selectedSkills : availableSkills;
}, [selectedSkills]);

return (
<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>
<div>
<h1>Community Members</h1>

{showFilters && (
<div className={styles.filtersPanel}>
<div style={{ marginBottom: 16 }}>
<strong>Filter by skills:</strong>
<div style={{ display: 'flex', gap: 10, marginTop: 8, flexWrap: 'wrap' }}>
{availableSkills.map(skill => (
<label key={skill} className={styles.filterOption}>
<label key={skill} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="checkbox"
checked={selectedSkills.includes(skill)}
onChange={() => handleCheckboxChange(skill)}
/>
<span>{skill}</span>
{skill}
</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>
</div>

<RankedUserList
users={filteredUsers}
loading={loading}
error={error}
emptyMessage={emptyMessage}
/>
<RankedUserList selectedSkills={effectiveSkills} />
</div>
);
}
Expand Down
75 changes: 49 additions & 26 deletions src/components/HGNHelpSkillsDashboard/RankedUserList.jsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import axios from 'axios';
import UserCard from './UserCard';
import styles from './style/UserCard.module.css';
import './style/UserCard.module.css';

function RankedUserList({ users, loading, error, emptyMessage }) {
if (loading) {
return <p>Loading community members...</p>;
}
function RankedUserList({ selectedSkills = [] }) {
const [rankedUsers, setRankedUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

if (error) {
return <p className="text-danger">{error}</p>;
}
useEffect(() => {
let canceled = false;

if (!users.length) {
return <p>{emptyMessage}</p>;
}
const fetchRankedUsers = async () => {
setLoading(true);
setError(null);

try {
const params = {};
if (Array.isArray(selectedSkills) && selectedSkills.length > 0) {
params.skills = selectedSkills.join(',');
}

const response = await axios.get('http://localhost:4500/api/hgnform/ranked', { params });

if (!canceled) {
const data = response?.data || [];
setRankedUsers(Array.isArray(data) ? data : []);
}
} catch (err) {
console.error('Failed to fetch ranked users', err);
if (!canceled) {
setError(err);
setRankedUsers([]);
}
} finally {
if (!canceled) setLoading(false);
}
};

fetchRankedUsers();

return () => {
canceled = true;
};
}, [selectedSkills]);

if (loading) return <p>Loading ranked users...</p>;
if (error) return <p>Failed to load users.</p>;
if (!rankedUsers.length) return <p>No members found.</p>;

return (
<div className={styles.containerGrid}>
{users.map(user => (
<UserCard key={user._id || user.email || user.name} user={user} />
<div className="user-card-container">
{rankedUsers.map(user => (
<UserCard key={user._id || user.id || user.uuid} 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;
16 changes: 12 additions & 4 deletions src/components/HGNHelpSkillsDashboard/UserCard.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import React from 'react';
import styles from './style/UserCard.module.css';
import avatar from './style/avatar.png';
import emailIcon from './style/email_icon.png';
import slackIcon from './style/slack_icon.png';

import { useSelector } from 'react-redux';

function UserCard({ user }) {
const { name, email, slack, score, topSkills = [] } = user;
const darkMode = useSelector(state => state.theme?.darkMode);
const { name, email, slack, score, topSkills } = user;

const getScoreColor = userScore => {
if (userScore >= 5) return '#00754A';
return '#D93D3D';
};

return (
<div className={`${styles.userCard}`}>
<div className={`${styles.userCard} ${darkMode ? styles.dark : ''}`}>
<img src={avatar} alt="Avatar" className={`${styles.avatar}`} />
<div className={`${styles.info}`}>
<div className={`${styles.userName}`}>{name}</div>
<div className={`${styles.userName}`} title={name}>
{name}
</div>
{email && (
<div className={`${styles.contactLine}`}>
<img src={emailIcon} alt="Email" className={`${styles.contactIcon}`} />
Expand All @@ -41,7 +47,9 @@ function UserCard({ user }) {

<div className={`${styles.skillsSection}`}>
<div className={`${styles.skillsLabel}`}>Top Skills:</div>
<div className={`${styles.skillsText}`}>{topSkills.join(', ')}</div>
<div className={`${styles.skillsText}`}>
{Array.isArray(topSkills) ? topSkills.join(', ') : topSkills || ''}
</div>
</div>
</div>
</div>
Expand Down
Loading
Loading