diff --git a/web-ui/src/api/certification.js b/web-ui/src/api/certification.js new file mode 100644 index 000000000..a8c9dafb5 --- /dev/null +++ b/web-ui/src/api/certification.js @@ -0,0 +1,17 @@ +import { resolve } from './api.js'; + +const certificationUrl = '/services/certification'; + +export const getCertifications = async cookie => { + return resolve({ + url: certificationUrl, + headers: { 'X-CSRF-Header': cookie, Accept: 'application/json' } + }); +}; + +export const getCertification = async (id, cookie) => { + return resolve({ + url: `${certificationUrl}/${id}`, + headers: { 'X-CSRF-Header': cookie, Accept: 'application/json' } + }); +}; diff --git a/web-ui/src/components/earned-certifications/EarnedCertificationBadges.css b/web-ui/src/components/earned-certifications/EarnedCertificationBadges.css new file mode 100644 index 000000000..4e7b06cad --- /dev/null +++ b/web-ui/src/components/earned-certifications/EarnedCertificationBadges.css @@ -0,0 +1,10 @@ +.earned-certification-badges { + img { + max-height: 5rem; + } + + .MuiCardContent-root { + display: flex; + gap: 1rem; + } +} diff --git a/web-ui/src/components/earned-certifications/EarnedCertificationBadges.jsx b/web-ui/src/components/earned-certifications/EarnedCertificationBadges.jsx new file mode 100644 index 000000000..6c012ef9c --- /dev/null +++ b/web-ui/src/components/earned-certifications/EarnedCertificationBadges.jsx @@ -0,0 +1,142 @@ +import PropTypes from 'prop-types'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; + +import {Card, CardContent, CardHeader, Chip, Tooltip} from '@mui/material'; + +import { resolve } from '../../api/api.js'; +import { AppContext } from '../../context/AppContext'; +import { selectCsrfToken } from '../../context/selectors'; + +import './EarnedCertificationBadges.css'; +import certifications from "../certifications/Certifications.jsx"; + +const earnedCertificationBaseUrl = '/services/earned-certification'; + +const propTypes = { + memberId: PropTypes.string, certifications: PropTypes.array, +}; +const EarnedCertificationBadges = ({ memberId, certifications }) => { + const [earnedCertifications, setEarnedCertifications] = useState([]); + + const { state } = useContext(AppContext); + const csrf = selectCsrfToken(state); + + const loadCertifications = useCallback(async () => { + const res = await resolve({ + method: 'GET', + url: earnedCertificationBaseUrl + '?memberId=' + memberId, + headers: { + 'X-CSRF-Header': csrf, + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + } + }); + if (res.error) return; + + const certifications = res.payload.data; + setEarnedCertifications(certifications); + }, [csrf]); + + useEffect(() => { + if (csrf) loadCertifications(); + }, [csrf]); + + if (earnedCertifications.length === 0 || !certifications) return null; + return ( + + + + {earnedCertifications.map(earnedCert => { + // Find the corresponding certification using earnedCert.certificationId + const cert = certifications.find(c => c.id === earnedCert.certificationId); + // If no matching cert is found, skip rendering for that earnedCert + if (!cert) return null; + if (cert.badgeUrl && cert.badgeUrl.trim().length > 0) { + return ( + + {cert.name}
+ Issued on: {earnedCert.earnedDate}
+ Expires on: {earnedCert.expirationDate} + + } + > + {earnedCert.validationUrl ? ( + + {`${cert.name}, + + ) : ( + {`${cert.name}, + )} +
+ ); + } else { + return ( + <> + {earnedCert.validationUrl ? ( + + + {cert.name}
+ Issued on: {earnedCert.earnedDate}
+ Expires on: {earnedCert.expirationDate} + + } + /> +
+ ) : ( + + {cert.name}
+ Issued on: {earnedCert.earnedDate}
+ Expires on: {earnedCert.expirationDate} + + } + /> + )} + + ); + } + })} +
+
+ ); + +}; + +EarnedCertificationBadges.propTypes = propTypes; + +export default EarnedCertificationBadges; diff --git a/web-ui/src/context/AppContext.jsx b/web-ui/src/context/AppContext.jsx index a725b0c85..f14fe6ef3 100644 --- a/web-ui/src/context/AppContext.jsx +++ b/web-ui/src/context/AppContext.jsx @@ -11,6 +11,7 @@ import { UPDATE_MEMBER_PROFILES, UPDATE_TERMINATED_MEMBERS, UPDATE_SKILLS, + UPDATE_CERTIFICATIONS, UPDATE_TEAMS, UPDATE_PEOPLE_LOADING, UPDATE_TEAMS_LOADING @@ -26,6 +27,7 @@ import { BASE_API_URL } from '../api/api'; import { getAllGuilds } from '../api/guild'; import { getSkills } from '../api/skill'; import { getAllTeams } from '../api/team'; +import {getCertifications} from "../api/certification.js"; const AppContext = React.createContext(); @@ -51,6 +53,7 @@ const AppContextProvider = props => { memberProfiles, checkins, skills, + certifications, roles, userRoles } = state; @@ -214,6 +217,26 @@ const AppContextProvider = props => { } }, [csrf, skills]); + useEffect(() => { + const getAllCertifications = async () => { + const res = await getCertifications(csrf); + const data = + res && + res.payload && + res.payload.data && + res.payload.status === 200 && + !res.error + ? res.payload.data + : null; + if (data && data.length > 0) { + dispatch({ type: UPDATE_CERTIFICATIONS, payload: data }); + } + }; + if (csrf && !certifications) { + getAllCertifications(); + } + }, [csrf, certifications]); + useEffect(() => { const getRoles = async () => { const res = await getAllRoles(csrf); diff --git a/web-ui/src/context/actions.js b/web-ui/src/context/actions.js index 80d90b160..c51b7e707 100644 --- a/web-ui/src/context/actions.js +++ b/web-ui/src/context/actions.js @@ -22,6 +22,7 @@ export const UPDATE_MEMBER_SKILLS = '@@check-ins/update_member_skills'; export const ADD_ROLE = '@@check-ins/add_role'; export const UPDATE_SKILL = '@@check-ins/update_skill'; export const UPDATE_SKILLS = '@@check-ins/update_skills'; +export const UPDATE_CERTIFICATIONS = '@@check-ins/update_certifications'; export const UPDATE_TEAM_MEMBERS = '@@check-ins/update_team_members'; export const UPDATE_TEAMS = '@@check-ins/update_teams'; export const UPDATE_TERMINATED_MEMBERS = diff --git a/web-ui/src/context/reducer.js b/web-ui/src/context/reducer.js index 081d0f144..4e9d952e6 100644 --- a/web-ui/src/context/reducer.js +++ b/web-ui/src/context/reducer.js @@ -20,6 +20,7 @@ import { UPDATE_MEMBER_SKILLS, UPDATE_SKILL, UPDATE_SKILLS, + UPDATE_CERTIFICATIONS, UPDATE_GUILD, UPDATE_GUILDS, ADD_ROLE, @@ -113,6 +114,9 @@ export const reducer = (state, action) => { case UPDATE_SKILLS: state.skills = action.payload; break; + case UPDATE_CERTIFICATIONS: + state.certifications = action.payload; + break; case SET_CSRF: state.csrf = action.payload; break; diff --git a/web-ui/src/pages/MemberProfilePage.jsx b/web-ui/src/pages/MemberProfilePage.jsx index 963b4b3d1..c0feffa13 100644 --- a/web-ui/src/pages/MemberProfilePage.jsx +++ b/web-ui/src/pages/MemberProfilePage.jsx @@ -16,11 +16,11 @@ import { getTeamByMember } from '../api/team'; import { getGuildsForMember } from '../api/guild'; import { getAvatarURL } from '../api/api.js'; import ProfilePage from './ProfilePage'; -import CertificationBadges from '../components/certifications/CertificationBadges'; import VolunteerBadges from '../components/volunteer/VolunteerBadges'; import { levelList } from '../context/util'; import StarIcon from '@mui/icons-material/Star'; import KudosDialog from '../components/kudos_dialog/KudosDialog'; +import EarnedCertificationBadges from "../components/earned-certifications/EarnedCertificationBadges.jsx"; import { Avatar, @@ -40,7 +40,7 @@ import './MemberProfilePage.css'; const MemberProfilePage = () => { const { state } = useContext(AppContext); const history = useHistory(); - const { csrf, skills, userProfile } = state; + const { csrf, skills, certifications, userProfile } = state; const { memberId } = useParams(); const [selectedMember, setSelectedMember] = useState(null); const [kudosDialogOpen, setKudosDialogOpen] = useState(false); @@ -330,7 +330,7 @@ const MemberProfilePage = () => { - +