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 ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+ } 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 = () => {
-
+