diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d5d82eb1d..d434dc6125 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -356,21 +356,21 @@ workflows: filters: branches: only: - - gig-share + - free # This is alternate dev env for parallel testing - "build-qa": context : org-global filters: branches: only: - - reskin-profile-settings + - free # This is beta env for production soft releases - "build-prod-beta": context : org-global filters: branches: only: - - tco23-leaderboards + - free # This is stage env for production QA releases - "build-prod-staging": context : org-global diff --git a/Dockerfile b/Dockerfile index f52ff2b984..c67c7f091a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -143,6 +143,8 @@ ENV GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=$GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY # Optimizely ENV OPTIMIZELY_SDK_KEY=$OPTIMIZELY_SDK_KEY +ENV GAMIFICATION_ORG_ID=$GAMIFICATION_ORG_ID + ################################################################################ # Testing and build of the application inside the container. diff --git a/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap b/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap index a0cd40abdc..b9e6eccc2c 100644 --- a/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/ProfilePage/__snapshots__/index.jsx.snap @@ -58,6 +58,7 @@ exports[`renders a full Profile correctly 1`] = ` }, ] } + badges={Object {}} challenges={null} clearSubtrackChallenges={[Function]} copilot={true} @@ -720,6 +721,7 @@ exports[`renders a full Profile correctly 1`] = ` exports[`renders an empty Profile correctly 1`] = ` + + + + + + + + diff --git a/src/assets/images/profile/header-overlay.png b/src/assets/images/profile/header-overlay.png new file mode 100644 index 0000000000..b88c5159c1 Binary files /dev/null and b/src/assets/images/profile/header-overlay.png differ diff --git a/src/assets/images/profile/header-overlay.svg b/src/assets/images/profile/header-overlay.svg deleted file mode 100644 index b4d2bc7d82..0000000000 --- a/src/assets/images/profile/header-overlay.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/shared/actions/page/index.js b/src/shared/actions/page/index.js index aaaf978697..fe59bdea94 100644 --- a/src/shared/actions/page/index.js +++ b/src/shared/actions/page/index.js @@ -1,4 +1,5 @@ import _ from 'lodash'; import challengeDetails from './challenge-details'; +import memberProfile from './profile'; -export default _.merge({}, challengeDetails); +export default _.merge({}, challengeDetails, memberProfile); diff --git a/src/shared/actions/page/profile.js b/src/shared/actions/page/profile.js new file mode 100644 index 0000000000..cd8d10f6fe --- /dev/null +++ b/src/shared/actions/page/profile.js @@ -0,0 +1,50 @@ +/** + * Actions for member profile page. + */ +/* global fetch */ +import { redux, config } from 'topcoder-react-utils'; + +/** + * @static + * @desc Initiates an action that fetch member's badges + * @param {String} handle Member handle. + * @return {Action} + */ +async function getGamificationBadgesInit(handle) { + return { handle }; +} + +/** + * @static + * @desc Creates an action that gets member's badges + * + * @param {String} handle Topcoder member handle. + * @return {Action} + */ +async function getGamificationBadgesDone(handle) { + try { + const memberInfo = await fetch(`${config.API.V5}/members/${handle}`) + .then(response => response.json()); + const badges = await fetch(`${config.API.V5}/gamification/badges/assigned/${memberInfo.userId}?organization_id=${config.GAMIFICATION.ORG_ID}`) + .then(response => response.json()); + + return { + handle, + badges, + }; + } catch (error) { + return { + handle, + error, + }; + } +} + +export default redux.createActions({ + PAGE: { + PROFILE: { + GET_GAMIFICATION_BADGES_INIT: getGamificationBadgesInit, + GET_GAMIFICATION_BADGES_DONE: getGamificationBadgesDone, + }, + }, +}); diff --git a/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx b/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx new file mode 100644 index 0000000000..39a2a126eb --- /dev/null +++ b/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PT from 'prop-types'; +import FallBackAwardIcon from 'assets/images/default-award.svg'; + +import './styles.scss'; + +const AwardBadge = ({ + title, imageUrl, mimeType, onSelectBadge, +}) => ( +
+ { + imageUrl ? ( + award-badge + ) : ( + + ) + } +
+ +
+ +
+
+); + +AwardBadge.defaultProps = { + title: '', + imageUrl: null, + mimeType: 'image/svg+xml', +}; + +AwardBadge.propTypes = { + title: PT.string, + imageUrl: PT.string, + mimeType: PT.string, + onSelectBadge: PT.func.isRequired, +}; + +export default AwardBadge; diff --git a/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss b/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss new file mode 100644 index 0000000000..a8dd313416 --- /dev/null +++ b/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss @@ -0,0 +1,32 @@ +@import "~styles/mixins"; + +.awardBadge { + background-color: $tc-white; + padding: 16px; + border-radius: 8px; + display: flex; + cursor: pointer; + + .image { + width: 48px; + height: 48px; + } + + .title { + @include roboto-bold; + $color: $tco-black; + + font-size: 14px; + font-weight: 700; + line-height: 16px; + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + max-width: 130px; + + @include xs-to-sm { + max-width: unset; + } + } +} diff --git a/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx b/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx new file mode 100644 index 0000000000..4eb1257d25 --- /dev/null +++ b/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PT from 'prop-types'; +import FallBackAwardIcon from 'assets/images/default-award.svg'; + +import './styles.scss'; + +const AwatarModal = ({ + modalData, +}) => { + const { + title, description, imageUrl, + } = modalData; + + return ( +
+ { + imageUrl ? ( + award-badge + ) : ( + + ) + } + +
+
+ {title} +
+ +
{description}
+ +
+
+ ); +}; + +AwatarModal.defaultProps = { + modalData: {}, +}; + +AwatarModal.propTypes = { + modalData: PT.shape( + { + title: PT.string, + description: PT.string, + imageUrl: PT.string, + }, + ), +}; + +export default AwatarModal; diff --git a/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss b/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss new file mode 100644 index 0000000000..0a230c30e9 --- /dev/null +++ b/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss @@ -0,0 +1,58 @@ +@import "~styles/mixins"; +@import "~components/Contentful/brackets"; + +.awardModal { + display: flex; + margin-top: 48px; + + @include xs-to-md { + flex-direction: column; + margin-top: 24px; + } + + .image { + width: 100px; + height: 100px; + + @include xs-to-md { + display: block; + margin: 0 auto; + } + } + + .rightContent { + margin-left: 16px; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + + .title { + @include roboto-bold; + + color: $tco-black; + font-size: 20px; + font-weight: 700; + line-height: 26px; + + @include xs-to-md { + text-align: center; + } + } + + .description { + @include brackets-content; + + font-weight: 400; + color: $tco-black; + font-size: 16px; + line-height: 24px; + margin-top: 10px; + } + + @include xs-to-md { + margin-top: 24px; + text-align: center; + } + } +} diff --git a/src/shared/components/ProfilePage/Awards/index.jsx b/src/shared/components/ProfilePage/Awards/index.jsx new file mode 100644 index 0000000000..7551d74071 --- /dev/null +++ b/src/shared/components/ProfilePage/Awards/index.jsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import PT from 'prop-types'; +import { Modal } from 'topcoder-react-ui-kit'; +import IconClose from 'assets/images/tc-edu/icon-close-big.svg'; +import _ from 'lodash'; +import md from 'utils/markdown'; + +import style from './styles.scss'; +import AwardBadge from './AwardBadge'; +import AwatarModal from './AwardModal'; + +const Awards = ({ badges }) => { + const [showModal, setShowModal] = useState(false); + const [modalData, setModalData] = useState({}); + + return ( + +
+
+ Community Awards & Honors +
+ +
+ { + badges.map((reward) => { + const title = _.get(reward, 'org_badge.badge_name'); + const imageUrl = _.get(reward, 'org_badge.badge_image_url'); + let description = _.get(reward, 'org_badge.badge_description'); + if (description) { + description = md(description); + } + + return ( + { + setShowModal(true); + setModalData({ + title, + description, + imageUrl, + }); + }} + /> + ); + }) + } +
+
+ { + showModal && ( + setShowModal(false)} theme={style}> +
+
+

Community Awards & Honors

+
setShowModal(false)}> + +
+
+
+ + +
+
+ ) + } +
+ ); +}; + +Awards.defaultProps = { + badges: [], +}; + +Awards.propTypes = { + badges: PT.arrayOf(PT.shape()), +}; + +export default Awards; diff --git a/src/shared/components/ProfilePage/Awards/styles.scss b/src/shared/components/ProfilePage/Awards/styles.scss new file mode 100644 index 0000000000..2fc895902b --- /dev/null +++ b/src/shared/components/ProfilePage/Awards/styles.scss @@ -0,0 +1,85 @@ +@import "~styles/mixins"; + +.header { + display: flex; + justify-content: space-between; + margin-bottom: 25px; + + .title { + @include barlow-medium; + + font-weight: 600; + color: #2a2a2a; + font-size: 22px; + line-height: 26px; + text-transform: uppercase; + } + + .icon { + cursor: pointer; + margin-top: 5px; + } +} + +.awards { + background-color: $listing-filter-bg; + border-radius: 8px; + margin: 68px 0 -36px 0; + padding-bottom: 32px; + + .header { + padding: 32px 0 20px 32px; + + @include xs-to-sm { + padding: 16px 0 16px 16px; + } + + span { + @include barlow-bold; + + font-weight: 600; + font-size: 20px; + line-height: 22px; + text-transform: uppercase; + color: $tco-black; + } + } +} + +.badgesContainer { + margin: 0 32px; + display: flex; + gap: 16px; + flex-wrap: wrap; + + @include xs-to-sm { + flex-direction: column; + margin: 0 16px; + } +} + +.award-modal { + padding-bottom: 10px; + border-radius: 8px; + margin: 25px 32px 32px; +} + +hr { + opacity: 0.5; +} + +.container { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + border-radius: 8px; + min-width: 600px; + + @include xs-to-sm { + width: 90%; + min-width: unset; + } +} + +.overlay { + background-color: #0c0c0c; + opacity: 0.85; +} diff --git a/src/shared/components/ProfilePage/BadgesModal/modal.scss b/src/shared/components/ProfilePage/BadgesModal/modal.scss index c0655a5605..e7ad2943fe 100644 --- a/src/shared/components/ProfilePage/BadgesModal/modal.scss +++ b/src/shared/components/ProfilePage/BadgesModal/modal.scss @@ -24,6 +24,6 @@ } @include xs-to-sm { - padding: 40px 10px; + padding: 0; } } diff --git a/src/shared/components/ProfilePage/Header/styles.scss b/src/shared/components/ProfilePage/Header/styles.scss index 7537655f7e..5b3b96de88 100644 --- a/src/shared/components/ProfilePage/Header/styles.scss +++ b/src/shared/components/ProfilePage/Header/styles.scss @@ -6,11 +6,11 @@ background-repeat: no-repeat; background-position: center center; width: 100%; - max-width: $screen-max; margin: 0 auto; height: 115px; position: absolute; top: 0; + left: 0; @include xs-to-sm { height: 173px; @@ -23,8 +23,9 @@ left: 0; width: 100%; height: 60px; - background: url(assets/images/profile/header-overlay.svg); + background: url(assets/images/profile/header-overlay.png); background-repeat: no-repeat; + background-size: 100% 100%; @include xs-to-sm { height: 36px; diff --git a/src/shared/components/ProfilePage/index.jsx b/src/shared/components/ProfilePage/index.jsx index e8e01730d2..c981a9fd20 100644 --- a/src/shared/components/ProfilePage/index.jsx +++ b/src/shared/components/ProfilePage/index.jsx @@ -7,13 +7,12 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; -import { isomorphy } from 'topcoder-react-utils'; +import { isomorphy, config } 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'; - import ProfileStats from 'containers/ProfileStats'; import { dataMap } from './ExternalLink'; import Header from './Header'; @@ -23,9 +22,8 @@ import styles from './styles.scss'; import Skills from './Skills'; import MemberInfo from './MemberInfo'; import Activity from './Activity'; +import Awards from './Awards'; import TcaCertificates from './TcaCertificates'; -// import ProfileModal from './ProfileModal'; -// import Awards from './Awards'; /** * Inspects a subtrack and determines if the member is active @@ -149,10 +147,10 @@ class ProfilePage extends React.Component { skills: propSkills, stats, lookupData, + badges, handleParam, meta, tcAcademyCertifications, - // rewards, } = this.props; const { @@ -229,6 +227,11 @@ class ProfilePage extends React.Component {
+ { + (config.GAMIFICATION.ENABLE_BADGE_UI && badges && (badges.rows || [])).length ? ( + + ) : null + } {tcAcademyCertifications.length > 0 && ( ({ stats: state.profile.stats, memberGroups: state.groups.memberGroups, lookupData: state.lookup, + badges: state.page.profile[ownProps.match.params.handle] + ? state.page.profile[ownProps.match.params.handle].badges : {}, tcAcademyCertifications: state.tcAcademy.certifications, auth: { ...state.auth, @@ -232,6 +235,7 @@ function mapDispatchToProps(dispatch) { dispatch(a.getSkillsInit()); dispatch(a.getStatsInit()); dispatch(lookupActions.getCountriesInit()); + dispatch(profileActions.page.profile.getGamificationBadgesInit(handle)); dispatch(a.getAchievementsV3Done(handle)); dispatch(a.getExternalAccountsDone(handle)); dispatch(a.getExternalLinksDone(handle)); @@ -239,6 +243,7 @@ function mapDispatchToProps(dispatch) { dispatch(a.getSkillsDone(handle)); dispatch(a.getStatsDone(handle, showPublicStats ? undefined : groupIds, tokenV3)); dispatch(lookupActions.getCountriesDone()); + dispatch(profileActions.page.profile.getGamificationBadgesDone(handle)); }, loadMarathon: (handle, tokenV3, memberId) => { const uuid = shortId(); diff --git a/src/shared/reducers/page/index.js b/src/shared/reducers/page/index.js index 5bd6895078..bf9a7671e7 100644 --- a/src/shared/reducers/page/index.js +++ b/src/shared/reducers/page/index.js @@ -22,6 +22,7 @@ import ui, { factory as uiFactory } from './ui'; import settings, { factory as settingsFactory } from './settings'; import reviewOpportunityDetails from './review-opportunity-details'; +import profile from './profile'; /** * Reducer factory. @@ -40,6 +41,7 @@ export function factory(req) { dashboard, reviewOpportunityDetails, submissionManagement, + profile, })); } @@ -52,4 +54,5 @@ export default combineReducers({ submission, ui, submissionManagement, + profile, }); diff --git a/src/shared/reducers/page/profile.js b/src/shared/reducers/page/profile.js new file mode 100644 index 0000000000..e0dc467391 --- /dev/null +++ b/src/shared/reducers/page/profile.js @@ -0,0 +1,60 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import { logger } from 'topcoder-react-lib'; + +import actions from 'actions/page/profile'; + +/** + * Inits the loading of user's badges. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function getGamificationBadgesInit(state, { payload }) { + const { handle } = payload; + return { + ...state, + [handle]: { + ...state[handle], + badges: {}, + }, + }; +} + +/** + * Finalizes the loading of user's badges. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function getGamificationBadgesDone(state, { error, payload }) { + if (error) { + logger.error('Failed to get user badges', payload); + return state; + } + + const { badges, handle } = payload; + + return { + ...state, + [handle]: { + ...state[handle], + badges, + }, + }; +} + +/** + * Creates a new reducer. + * @param {Object} state Optional. Initial state. + * @return {Function} Reducer. + */ +function create(defaultState = {}) { + const a = actions.page.profile; + return handleActions({ + [a.getGamificationBadgesInit]: getGamificationBadgesInit, + [a.getGamificationBadgesDone]: getGamificationBadgesDone, + }, _.defaults(defaultState, {})); +} + +export default create();