diff --git a/.circleci/config.yml b/.circleci/config.yml
index 82a9a2c2f3..2b82baa568 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -349,7 +349,7 @@ workflows:
filters:
branches:
only:
- - free
+ - tca-certifications-on-profile-page
# This is alternate dev env for parallel testing
- "build-test":
context : org-global
diff --git a/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap b/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap
index bcc4226d58..a0cd40abdc 100644
--- a/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap
+++ b/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap
@@ -713,6 +713,7 @@ exports[`renders a full Profile correctly 1`] = `
"unsubscribe": null,
}
}
+ tcAcademyCertifications={Array []}
/>
`;
@@ -862,5 +863,6 @@ exports[`renders an empty Profile correctly 1`] = `
"unsubscribe": null,
}
}
+ tcAcademyCertifications={Array []}
/>
`;
diff --git a/config/default.js b/config/default.js
index e5325e57eb..8f3d90b2d8 100644
--- a/config/default.js
+++ b/config/default.js
@@ -444,6 +444,7 @@ module.exports = {
GIGS_PAGES_PATH: '/gigs',
GIGS_LISTING_CACHE_TIME: 300, // in seconds
START_PAGE_PATH: '/start',
+ TC_ACADEMY_BASE_PATH: '/learn',
GUIKIT: {
DEBOUNCE_ON_CHANGE_TIME: 150,
},
@@ -452,4 +453,5 @@ module.exports = {
SDK_KEY: '7V4CJhurXT3Y3bnzv1hv1',
},
PLATFORM_SITE_URL: 'https://platform.topcoder-dev.com',
+ PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder-dev.com',
};
diff --git a/config/development.js b/config/development.js
index 7991e1a4c9..5088a55f80 100644
--- a/config/development.js
+++ b/config/development.js
@@ -5,4 +5,5 @@ module.exports = {
USER_SETTINGS: '', /* No dev server is available for saved searches */
},
PLATFORM_SITE_URL: 'https://platform.topcoder-dev.com',
+ PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder-dev.com',
};
diff --git a/config/production.js b/config/production.js
index b71f7c9800..a97b9b3cb3 100644
--- a/config/production.js
+++ b/config/production.js
@@ -226,4 +226,5 @@ module.exports = {
TC_EDU_SEARCH_BAR_MAX_RESULTS_EACH_GROUP: 3,
ENABLE_RECOMMENDER: true,
PLATFORM_SITE_URL: 'https://platform.topcoder.com',
+ PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder.com',
};
diff --git a/package.json b/package.json
index ac73c3642a..7ad7f93100 100644
--- a/package.json
+++ b/package.json
@@ -153,7 +153,7 @@
"supertest": "^3.1.0",
"tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3",
"tc-ui": "^1.0.12",
- "topcoder-react-lib": "1.2.7",
+ "topcoder-react-lib": "1000.29.7",
"topcoder-react-ui-kit": "2.0.1",
"topcoder-react-utils": "0.7.8",
"turndown": "^4.0.2",
diff --git a/src/assets/images/profile/tca-certificates/datascience-badge.png b/src/assets/images/profile/tca-certificates/datascience-badge.png
new file mode 100644
index 0000000000..06c3242152
Binary files /dev/null and b/src/assets/images/profile/tca-certificates/datascience-badge.png differ
diff --git a/src/assets/images/profile/tca-certificates/design-badge.png b/src/assets/images/profile/tca-certificates/design-badge.png
new file mode 100644
index 0000000000..5bdb8bfb25
Binary files /dev/null and b/src/assets/images/profile/tca-certificates/design-badge.png differ
diff --git a/src/assets/images/profile/tca-certificates/develop-badge.png b/src/assets/images/profile/tca-certificates/develop-badge.png
new file mode 100644
index 0000000000..e24e581c73
Binary files /dev/null and b/src/assets/images/profile/tca-certificates/develop-badge.png differ
diff --git a/src/assets/images/profile/tca-certificates/qa-badge.png b/src/assets/images/profile/tca-certificates/qa-badge.png
new file mode 100644
index 0000000000..a5e73770b8
Binary files /dev/null and b/src/assets/images/profile/tca-certificates/qa-badge.png differ
diff --git a/src/assets/images/profile/tca-certificates/tca-logo.svg b/src/assets/images/profile/tca-certificates/tca-logo.svg
new file mode 100644
index 0000000000..95fc94d254
--- /dev/null
+++ b/src/assets/images/profile/tca-certificates/tca-logo.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/shared/components/ProfilePage/ProfileModal/index.jsx b/src/shared/components/ProfilePage/ProfileModal/index.jsx
new file mode 100644
index 0000000000..41a4f1bbfa
--- /dev/null
+++ b/src/shared/components/ProfilePage/ProfileModal/index.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import PT from 'prop-types';
+import { noop } from 'lodash/noop';
+import { Modal } from 'topcoder-react-ui-kit';
+import cn from 'classnames';
+
+import IconClose from 'assets/images/icon-close-green.svg';
+import styles from './styles.scss';
+
+const ProfileModal = ({
+ children,
+ title,
+ onCancel,
+ containerClassName,
+}) => (
+
+
+
+ {children}
+
+
+);
+
+ProfileModal.defaultProps = {
+ title: null,
+ onCancel: noop,
+ containerClassName: '',
+};
+
+ProfileModal.propTypes = {
+ children: PT.node.isRequired,
+ title: PT.node,
+ onCancel: PT.func,
+ containerClassName: PT.string,
+};
+
+export default ProfileModal;
diff --git a/src/shared/components/ProfilePage/ProfileModal/styles.scss b/src/shared/components/ProfilePage/ProfileModal/styles.scss
new file mode 100644
index 0000000000..a93949450d
--- /dev/null
+++ b/src/shared/components/ProfilePage/ProfileModal/styles.scss
@@ -0,0 +1,47 @@
+@import "~styles/mixins";
+
+.modal-overlay {
+ background: #000;
+}
+
+.modal-container-copilot,
+.modal-container {
+ width: 1232px;
+ min-height: 700px;
+ max-width: 1232px;
+ border-radius: 8px;
+ padding: 32px 32px 0 32px;
+ gap: 24px;
+
+ &.modal-container-copilot {
+ height: auto;
+ }
+
+ @include xs-to-sm {
+ width: 100%;
+ max-width: 100%;
+ height: 100% !important;
+ max-height: 100% !important;
+ padding: 24px 16px 48px 16px;
+ }
+
+ .header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 24px;
+
+ .title {
+ @include barlow-medium;
+
+ font-weight: 600;
+ font-size: 22px;
+ line-height: 26px;
+ text-transform: uppercase;
+ }
+
+ .icon {
+ cursor: pointer;
+ }
+ }
+}
diff --git a/src/shared/components/ProfilePage/TcaCertificates/CourseBadge/index.jsx b/src/shared/components/ProfilePage/TcaCertificates/CourseBadge/index.jsx
new file mode 100644
index 0000000000..1ab0766e66
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/CourseBadge/index.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PT from 'prop-types';
+
+import DataScienceBadgeImg from 'assets/images/profile/tca-certificates/datascience-badge.png';
+import DesignBadgeImg from 'assets/images/profile/tca-certificates/design-badge.png';
+import DevelopBadgeImg from 'assets/images/profile/tca-certificates/develop-badge.png';
+import QaBadgeImg from 'assets/images/profile/tca-certificates/qa-badge.png';
+
+import './styles.scss';
+
+const badgesMap = {
+ DATASCIENCE: DataScienceBadgeImg,
+ DESIGN: DesignBadgeImg,
+ DEV: DevelopBadgeImg,
+ QA: QaBadgeImg,
+};
+
+const CourseBadge = ({ type: badgeType, size }) => {
+ const badgeImg = badgesMap[badgeType];
+
+ return (
+
+

+
+ );
+};
+
+CourseBadge.defaultProps = {
+ size: 'md',
+};
+
+CourseBadge.propTypes = {
+ size: PT.oneOf(['md']),
+ type: PT.oneOf(['DATASCIENCE', 'DESIGN', 'DEV', 'QA']).isRequired,
+};
+
+
+export default CourseBadge;
diff --git a/src/shared/components/ProfilePage/TcaCertificates/CourseBadge/styles.scss b/src/shared/components/ProfilePage/TcaCertificates/CourseBadge/styles.scss
new file mode 100644
index 0000000000..d27584562e
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/CourseBadge/styles.scss
@@ -0,0 +1,13 @@
+.tca-badge-wrap {
+ &.size-md {
+ width: 48px;
+ height: 48px;
+ }
+
+ img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+}
diff --git a/src/shared/components/ProfilePage/TcaCertificates/List/index.jsx b/src/shared/components/ProfilePage/TcaCertificates/List/index.jsx
new file mode 100644
index 0000000000..ae77b2203d
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/List/index.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import PT from 'prop-types';
+
+import './styles.scss';
+import CourseBadge from '../CourseBadge';
+
+const preventDefault = ev => ev.stopPropagation();
+
+const List = ({
+ certificates,
+ onClick,
+}) => (
+
+ {certificates.map(certificate => (
+
onClick(certificate)}
+ onKeyPress={() => onClick(certificate)}
+ role="button"
+ tabIndex={-1}
+ >
+
+
+
+
+
+ {certificate.certificationTitle}
+
+
+
+
+ ))}
+
+);
+
+List.propTypes = {
+ certificates: PT.arrayOf(PT.shape()).isRequired,
+ onClick: PT.func.isRequired,
+};
+
+export default List;
diff --git a/src/shared/components/ProfilePage/TcaCertificates/List/styles.scss b/src/shared/components/ProfilePage/TcaCertificates/List/styles.scss
new file mode 100644
index 0000000000..24fecab25f
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/List/styles.scss
@@ -0,0 +1,63 @@
+@import "~styles/mixins";
+
+.list {
+ display: flex;
+ gap: 16px;
+ margin-top: 24px;
+ flex-wrap: wrap;
+
+ @include xs-to-sm {
+ flex-direction: column;
+ flex-wrap: nowrap;
+ margin-top: 16px;
+ }
+}
+
+.list-item {
+ background: $listing-white;
+ border-radius: 8px;
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ width: 316px;
+ transition: 0.25s ease-in-out;
+ box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+ cursor: pointer;
+
+ &:hover {
+ box-shadow: 0 0 16px rgba(22, 103, 154, 0.5);
+ }
+
+ @include xs-to-sm {
+ width: 100%;
+ }
+}
+
+.list-item_badge {
+ width: 48px;
+ height: 48px;
+
+ svg {
+ display: block;
+ width: 48px;
+ height: 48px;
+ }
+}
+
+.list-item_title {
+ @include roboto-sans-regular;
+
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: bold;
+}
+
+.list-item_sub {
+ @include roboto-sans-regular;
+
+ font-style: italic;
+ font-size: 14px;
+ line-height: 22px;
+ color: $listing-placeholder-gray;
+}
diff --git a/src/shared/components/ProfilePage/TcaCertificates/TcaCertificateModal/index.jsx b/src/shared/components/ProfilePage/TcaCertificates/TcaCertificateModal/index.jsx
new file mode 100644
index 0000000000..4d9cfb8dd1
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/TcaCertificateModal/index.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import PT from 'prop-types';
+import { noop } from 'lodash/noop';
+import { config } from 'topcoder-react-utils';
+
+import ProfileModal from '../../ProfileModal';
+import styles from './styles.scss';
+
+const tcAcademyPath = `${config.PLATFORMUI_SITE_URL}${config.TC_ACADEMY_BASE_PATH}`;
+
+const TcaCertificateModal = ({
+ certificate,
+ onCancel,
+ memberHandle,
+}) => (
+
+
+
+);
+
+TcaCertificateModal.defaultProps = {
+ onCancel: noop,
+};
+
+TcaCertificateModal.propTypes = {
+ certificate: PT.shape().isRequired,
+ onCancel: PT.func,
+ memberHandle: PT.string.isRequired,
+};
+
+export default TcaCertificateModal;
diff --git a/src/shared/components/ProfilePage/TcaCertificates/TcaCertificateModal/styles.scss b/src/shared/components/ProfilePage/TcaCertificates/TcaCertificateModal/styles.scss
new file mode 100644
index 0000000000..f553f424a4
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/TcaCertificateModal/styles.scss
@@ -0,0 +1,28 @@
+@import "~styles/mixins";
+
+.tca-certificate-modal {
+ display: flex;
+ flex-direction: column;
+ min-height: auto;
+ padding: 32px;
+ width: 980px;
+ max-width: 96vw;
+
+ @include xs-to-sm {
+ display: flex;
+ flex-direction: column;
+ max-width: none;
+ width: 100%;
+ }
+}
+
+.iframe {
+ display: block;
+ height: 600px;
+ width: 100%;
+
+ @include xs-to-sm {
+ height: auto;
+ flex: 1;
+ }
+}
diff --git a/src/shared/components/ProfilePage/TcaCertificates/index.jsx b/src/shared/components/ProfilePage/TcaCertificates/index.jsx
new file mode 100644
index 0000000000..4f357177aa
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/index.jsx
@@ -0,0 +1,44 @@
+import React, { useState } from 'react';
+import PT from 'prop-types';
+
+import './styles.scss';
+import TcaLogo from 'assets/images/profile/tca-certificates/tca-logo.svg';
+import List from './List';
+import TcaCertificateModal from './TcaCertificateModal';
+
+const TcaCertificates = ({ certificates, memberHandle }) => {
+ const [showCertificate, setShowCertificate] = useState(false);
+ return (
+
+
+
+ Topcoder Academy
+
+
+ setShowCertificate(certificate)}
+ />
+
+ { showCertificate && (
+ setShowCertificate(false)}
+ certificate={showCertificate}
+ memberHandle={memberHandle}
+ />
+ )}
+
+ );
+};
+
+TcaCertificates.defaultProps = {
+ certificates: [],
+};
+
+TcaCertificates.propTypes = {
+ certificates: PT.arrayOf(PT.shape),
+ memberHandle: PT.string.isRequired,
+};
+
+export default TcaCertificates;
diff --git a/src/shared/components/ProfilePage/TcaCertificates/styles.scss b/src/shared/components/ProfilePage/TcaCertificates/styles.scss
new file mode 100644
index 0000000000..25f502260d
--- /dev/null
+++ b/src/shared/components/ProfilePage/TcaCertificates/styles.scss
@@ -0,0 +1,40 @@
+@import "~styles/mixins";
+
+.tca-certificates {
+ margin-top: 32px;
+ background: $listing-filter-bg;
+ padding: 32px;
+ border-radius: 8px;
+
+ @include xs-to-md {
+ padding: 16px;
+ }
+
+ .title {
+ @include barlow-semi-bold;
+
+ font-size: 22px;
+ line-height: 26px;
+ color: $tco-black;
+ text-transform: uppercase;
+ padding: 5px;
+ display: flex;
+ align-items: center;
+ gap: 13px;
+
+ svg {
+ width: 37px;
+ height: 38px;
+ }
+
+ @include xs-to-md {
+ font-size: 18px;
+ line-height: 20px;
+
+ svg {
+ width: 31px;
+ height: 33px;
+ }
+ }
+ }
+}
diff --git a/src/shared/components/ProfilePage/index.jsx b/src/shared/components/ProfilePage/index.jsx
index 04d648650c..81e1432252 100644
--- a/src/shared/components/ProfilePage/index.jsx
+++ b/src/shared/components/ProfilePage/index.jsx
@@ -8,8 +8,6 @@ import _ from 'lodash';
import React from 'react';
import PT from 'prop-types';
import { isomorphy } from 'topcoder-react-utils';
-import { Modal } from 'topcoder-react-ui-kit';
-import IconClose from 'assets/images/icon-close-green.svg';
import shortId from 'shortid';
import { actions } from 'topcoder-react-lib';
import { connect } from 'react-redux';
@@ -19,10 +17,12 @@ import { dataMap } from './ExternalLink';
import Header from './Header';
import MemberTracks from './MemberTracks';
-import styles from './styles.scss';
+import './styles.scss';
import Skills from './Skills';
import MemberInfo from './MemberInfo';
import Activity from './Activity';
+import TcaCertificates from './TcaCertificates';
+import ProfileModal from './ProfileModal';
// import Awards from './Awards';
/**
@@ -149,6 +149,7 @@ class ProfilePage extends React.Component {
lookupData,
handleParam,
meta,
+ tcAcademyCertifications,
// rewards,
} = this.props;
@@ -226,6 +227,12 @@ class ProfilePage extends React.Component {
+ {tcAcademyCertifications.length > 0 && (
+
+ )}
{/* { */}
{/* (rewards || []).length ? ( */}
{/* */}
@@ -242,39 +249,26 @@ class ProfilePage extends React.Component {
}}
/>
{ showDetails && (
-
-
-
-
- {
- subTrack === 'SRM' ? 'Single round match'
- : subTrack.replace('FIRST_2_FINISH', 'FIRST2FINISH').replace(/_/g, ' ')
- }
-
-
-
-
-
- {
- this.setState({ tab });
- }}
- isAlreadyLoadChallenge={this.isAlreadyLoadChallenge}
- />
-
-
+ {
+ this.setState({ tab });
+ }}
+ isAlreadyLoadChallenge={this.isAlreadyLoadChallenge}
+ />
+
)}
);
@@ -288,6 +282,7 @@ ProfilePage.defaultProps = {
skills: null,
stats: null,
// rewards: [],
+ tcAcademyCertifications: [],
};
ProfilePage.propTypes = {
@@ -303,6 +298,7 @@ ProfilePage.propTypes = {
meta: PT.shape().isRequired,
// rewards: PT.arrayOf(PT.shape()),
clearSubtrackChallenges: PT.func.isRequired,
+ tcAcademyCertifications: PT.arrayOf(PT.shape()),
};
function mapDispatchToProps(dispatch) {
diff --git a/src/shared/components/ProfilePage/styles.scss b/src/shared/components/ProfilePage/styles.scss
index 27226ce130..1418126f3d 100644
--- a/src/shared/components/ProfilePage/styles.scss
+++ b/src/shared/components/ProfilePage/styles.scss
@@ -56,49 +56,3 @@
padding-top: 32px;
}
}
-
-.modal-overlay {
- background: #000;
-}
-
-.modal-container-copilot,
-.modal-container {
- width: 1232px;
- min-height: 700px;
- max-width: 1232px;
- border-radius: 8px;
- padding: 32px 32px 0 32px;
- gap: 24px;
-
- &.modal-container-copilot {
- height: auto;
- }
-
- @include xs-to-sm {
- width: 100%;
- max-width: 100%;
- height: 100% !important;
- max-height: 100% !important;
- padding: 24px 16px 48px 16px;
- }
-
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: 24px;
-
- .title {
- @include barlow-medium;
-
- font-weight: 600;
- font-size: 22px;
- line-height: 26px;
- text-transform: uppercase;
- }
-
- .icon {
- cursor: pointer;
- }
- }
-}
diff --git a/src/shared/containers/Profile.jsx b/src/shared/containers/Profile.jsx
index ebd0f92f76..ee7e7d5df7 100644
--- a/src/shared/containers/Profile.jsx
+++ b/src/shared/containers/Profile.jsx
@@ -23,6 +23,7 @@ class ProfileContainer extends React.Component {
handleParam,
loadProfile,
loadMarathon,
+ loadCertificates,
meta,
auth,
info,
@@ -31,7 +32,9 @@ class ProfileContainer extends React.Component {
if (info) {
loadMarathon(handleParam, auth.tokenV3, info.userId);
+ loadCertificates(info.userId);
}
+
// Redirect to the communities own profile page if
// - the member whose profile is being viewed is part of one of the configured communities
// - the user is not a topcoder user (has an email with @topcoder.com)
@@ -64,6 +67,7 @@ class ProfileContainer extends React.Component {
loadProfile,
loadMarathon,
loadMemberGroups,
+ loadCertificates,
meta,
auth,
info,
@@ -81,6 +85,7 @@ class ProfileContainer extends React.Component {
if (info && info.userId && info !== prevInfo) {
loadMarathon(handleParam, auth.tokenV3, info.userId);
+ loadCertificates(info.userId);
}
if (auth.tokenV3 && auth.user && auth.user.handle !== handleParam
&& info != null && info.userId != null
@@ -174,6 +179,7 @@ ProfileContainer.propTypes = {
loadMarathon: PT.func.isRequired,
loadProfile: PT.func.isRequired,
loadMemberGroups: PT.func.isRequired,
+ loadCertificates: PT.func.isRequired,
profileForHandle: PT.string,
skills: PT.shape(),
stats: PT.arrayOf(PT.shape()),
@@ -200,6 +206,7 @@ const mapStateToProps = (state, ownProps) => ({
stats: state.profile.stats,
memberGroups: state.groups.memberGroups,
lookupData: state.lookup,
+ tcAcademyCertifications: state.tcAcademy.certifications,
auth: {
...state.auth,
},
@@ -209,6 +216,8 @@ function mapDispatchToProps(dispatch) {
const a = actions.profile;
const lookupActions = actions.lookup;
const memberActions = actions.members;
+ const tcaActions = actions.tcAcademy;
+
return {
loadMemberGroups: (userId, tokenV3) => {
dispatch(actions.groups.getMemberGroups(userId, tokenV3));
@@ -244,6 +253,10 @@ function mapDispatchToProps(dispatch) {
true,
));
},
+ loadCertificates: (userId) => {
+ dispatch(tcaActions.getTcaCertificationsInit(userId));
+ dispatch(tcaActions.getTcaCertificationsDone(userId));
+ },
};
}