diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f5b6aa3cc..d434dc6125 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -356,21 +356,21 @@ workflows: filters: branches: only: - - tco23 + - 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: - - social-share-updates + - free # This is stage env for production QA releases - "build-prod-staging": context : org-global diff --git a/Dockerfile b/Dockerfile index f52ff2b984..6b7a48a00a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,6 +78,9 @@ ARG GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY # Optimizely ARG OPTIMIZELY_SDK_KEY +# Gamification +ARG GAMIFICATION_ORG_ID + ################################################################################ # Setting of environment variables in the Docker image. @@ -143,6 +146,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/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap index be1a732415..9f2998a102 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap @@ -70,7 +70,6 @@ exports[`Matches shallow shapshot 1`] = ` className="src-shared-components-Leaderboard-LeaderboardTable-themes-___styles__winnings-info___PBqQk" > - 0 points @@ -84,7 +83,7 @@ exports[`Matches shallow shapshot 1`] = ` - 0.00 + NaN @@ -125,7 +124,6 @@ exports[`Matches shallow shapshot 1`] = ` className="src-shared-components-Leaderboard-LeaderboardTable-themes-___styles__winnings-info___PBqQk" > - 0 points @@ -139,7 +137,7 @@ exports[`Matches shallow shapshot 1`] = ` - 0.00 + NaN @@ -180,7 +178,6 @@ exports[`Matches shallow shapshot 1`] = ` className="src-shared-components-Leaderboard-LeaderboardTable-themes-___styles__winnings-info___PBqQk" > - 0 points @@ -194,7 +191,7 @@ exports[`Matches shallow shapshot 1`] = ` - 0.00 + NaN @@ -235,7 +232,6 @@ exports[`Matches shallow shapshot 1`] = ` className="src-shared-components-Leaderboard-LeaderboardTable-themes-___styles__winnings-info___PBqQk" > - 0 points @@ -249,7 +245,7 @@ exports[`Matches shallow shapshot 1`] = ` - 0.00 + NaN diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap index c5a8a3f5e5..30cbf6a86e 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap @@ -5,7 +5,7 @@ exports[`Matches shallow shapshot 1`] = ` className="src-shared-components-Leaderboard-Podium-themes-___default__Podium___1qIZm" >
@@ -49,6 +50,7 @@ exports[`Matches shallow shapshot 1`] = ` isCopilot={false} isTopGear={false} onUsernameClick={null} + podiumPlaces={4} themeName="Default" /> @@ -69,6 +71,7 @@ exports[`Matches shallow shapshot 1`] = ` isCopilot={false} isTopGear={false} onUsernameClick={null} + podiumPlaces={4} themeName="Default" /> @@ -89,6 +92,7 @@ exports[`Matches shallow shapshot 1`] = ` isCopilot={false} isTopGear={false} onUsernameClick={null} + podiumPlaces={4} themeName="Default" /> diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap index 99ce90f051..38d804a762 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap @@ -47,7 +47,7 @@ exports[`Matches shallow shapshot 1`] = ` - 0.00 + NaN - 0.00 + NaN + + + + + + + + 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/Leaderboard/ChallengeHistoryModal/index.jsx b/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx index bd4023d261..a1ad152b8c 100644 --- a/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx +++ b/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx @@ -39,8 +39,10 @@ class ChallengeHistoryModal extends Component { } = this.props; const { sortParam } = this.state; const challengesOrdered = _.orderBy(challenges, [sortParam.field], [sortParam.order]); - const placeLabel = competitor['member_profile_basic.handle'] ? 'tco_leaderboard.placement' : 'place'; - const pointsLabel = competitor['member_profile_basic.handle'] ? 'tco_leaderboard.tco_points' : 'points'; + // eslint-disable-next-line + const placeLabel = competitor['tco23_leaderboard.challenge_count'] ? 'tco23_leaderboard.placement' : 'tco_leaderboard.placement'; + // eslint-disable-next-line + const pointsLabel = competitor['tco23_leaderboard.tco_points'] ? 'tco23_leaderboard.tco_points' : 'tco_leaderboard.tco_points'; const styles = THEMES[themeName] || THEMES.Default; /* eslint-disable no-confusing-arrow */ const sortInner = () => themeName === 'TCO23' ? ( @@ -95,7 +97,7 @@ class ChallengeHistoryModal extends Component { }} type="button" > - { sortInner() } + {sortInner()} @@ -117,7 +119,7 @@ class ChallengeHistoryModal extends Component { }} type="button" > - { sortInner() } + {sortInner()} @@ -125,23 +127,28 @@ class ChallengeHistoryModal extends Component { { - challengesOrdered.map(challenge => ( - - - - {challenge.challenge_name || challenge['challenge.challenge_name'] || challenge['tco_leaderboard.challenge_id'] || challenge.challenge_id} - - - { + challengesOrdered.map((challenge) => { + const challengeId = challenge['tco23_leaderboard.challenge_id'] || challenge['tco_leaderboard.challenge_id'] || challenge['challenge.challenge_GUID'] || challenge['challenge.challenge_id'] || challenge.challenge_id; + return ( + + + + {challenge.challenge_name || challenge['challenge.challenge_name'] || challengeId} + + + { !isCopilot ? ( - {challenge['tco_leaderboard.placement'] || challenge.place}placement + + {challenge[placeLabel]}placement + ) : null } - - {challenge['tco_leaderboard.tco_points'] || challenge.points}points - - - )) + + {challenge[pointsLabel] || challenge.points}points + + + ); + }) } diff --git a/src/shared/components/Leaderboard/LeaderboardTable/index.jsx b/src/shared/components/Leaderboard/LeaderboardTable/index.jsx index 1f7eb08bc5..89476a8dee 100644 --- a/src/shared/components/Leaderboard/LeaderboardTable/index.jsx +++ b/src/shared/components/Leaderboard/LeaderboardTable/index.jsx @@ -67,6 +67,12 @@ export default function LeaderboardTable(props) { const addSufix = val => isAlgo ? (val !== 1 ? `${val} matches` : `${val} match`) : (val !== 1 ? `${val} challenges` : `${val} challenge`); const renderTableRows = comps => ( comps.map((competitor) => { + const tcoPoints = competitor['tco23_leaderboard.tco_points'] + || competitor['tco_leaderboard.tco_points'] + || competitor.points + || competitor['tco_leaderboard.total_score'] + || competitor['srm_tco19.score']; + const tcoChallengeCnt = competitor['tco23_leaderboard.challenge_count'] || competitor['tco_leaderboard.challenge_count'] || competitor.challengecount; let photoUrl = competitor['member_profile_basic.photo_url'] || competitor.avatar; if (photoUrl) { photoUrl = `${config.CDN.PUBLIC}/avatar/${encodeURIComponent(photoUrl)}?size=40`; @@ -147,13 +153,13 @@ export default function LeaderboardTable(props) { }
{fulfillment && ({fulfillment} fulfillment)} - {competitor['tco_leaderboard.tco_points'] || competitor.points} points + {tcoPoints} points { themeName === 'TCO23' ? (
onUsernameClick(competitor)} styleName={`${stylesName}.mobile-link`}> - {addSufix(competitor['tco_leaderboard.challenge_count'] || competitor.challengecount)} + {addSufix(tcoChallengeCnt)}
- ) : {addSufix(competitor['tco_leaderboard.challenge_count'] || competitor.challengecount)} + ) : {addSufix(tcoChallengeCnt)} }
@@ -172,15 +178,15 @@ export default function LeaderboardTable(props) { style={{ cursor: 'pointer', display: 'inline-block', color: '#0d61bf' }} onClick={() => onUsernameClick(competitor)} > - { `${addSufix(competitor['tco_leaderboard.challenge_count'] || competitor.challengecount)}` } + {`${addSufix(tcoChallengeCnt)}`} - ) : `${addSufix(competitor['tco_leaderboard.challenge_count'] || competitor.challengecount)}` + ) : `${addSufix(tcoChallengeCnt)}` ) : ( - competitor['tco_leaderboard.challenge_count'] || competitor.challengecount + tcoChallengeCnt ) } - {formatPoints(competitor['tco_leaderboard.tco_points'] || competitor.points)} + {formatPoints(tcoPoints)} { isTopGear ? ( {competitor.wins} @@ -193,7 +199,7 @@ export default function LeaderboardTable(props) { } { isAlgo ? ( - {competitor['tco_leaderboard.total_score'] || competitor['srm_tco19.score']} + {tcoPoints} ) : null } diff --git a/src/shared/components/Leaderboard/Podium/index.jsx b/src/shared/components/Leaderboard/Podium/index.jsx index c3d176c973..5a6cf54310 100644 --- a/src/shared/components/Leaderboard/Podium/index.jsx +++ b/src/shared/components/Leaderboard/Podium/index.jsx @@ -64,12 +64,13 @@ export default function Podium(props) { isTopGear={isTopGear} isAlgo={isAlgo} themeName={themeName} + podiumPlaces={comps.length} /> )); return ( -
+
3 ? 'PodiumWrapCondense' : 'PodiumWrap'}`} style={comps.length === 4 ? { 'justify-content': 'space-between' } : {}}> {podiumSpots}
); diff --git a/src/shared/components/Leaderboard/Podium/themes/default.scss b/src/shared/components/Leaderboard/Podium/themes/default.scss index d41f31f5e6..e912007c97 100644 --- a/src/shared/components/Leaderboard/Podium/themes/default.scss +++ b/src/shared/components/Leaderboard/Podium/themes/default.scss @@ -18,7 +18,8 @@ } } -.PodiumWrap { +.PodiumWrap, +.PodiumWrapCondense { display: flex; justify-content: center !important; diff --git a/src/shared/components/Leaderboard/Podium/themes/tco23.scss b/src/shared/components/Leaderboard/Podium/themes/tco23.scss index cbde4c5ef1..d76552b6ff 100644 --- a/src/shared/components/Leaderboard/Podium/themes/tco23.scss +++ b/src/shared/components/Leaderboard/Podium/themes/tco23.scss @@ -23,7 +23,8 @@ } } -.PodiumWrap { +.PodiumWrap, +.PodiumWrapCondense { display: flex; justify-content: center !important; @@ -31,7 +32,9 @@ flex-direction: column; align-items: center; } +} +.PodiumWrap { .podium-column:nth-child(2) { @include md-to-xl { margin-top: -16px; @@ -58,3 +61,19 @@ margin-right: 0; } } + +.PodiumWrapCondense { + @media screen and (min-width: 375px) and (max-width: 768px) { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 17px; + } + + .podium-column { + margin-right: 16px; + + @include xs-to-sm { + margin: 0; + } + } +} diff --git a/src/shared/components/Leaderboard/PodiumSpot/index.jsx b/src/shared/components/Leaderboard/PodiumSpot/index.jsx index ad49696a8a..99d73216ab 100644 --- a/src/shared/components/Leaderboard/PodiumSpot/index.jsx +++ b/src/shared/components/Leaderboard/PodiumSpot/index.jsx @@ -25,7 +25,7 @@ */ import React from 'react'; -import PT from 'prop-types'; +import PT, { number } from 'prop-types'; import { Avatar } from 'topcoder-react-ui-kit'; import { config } from 'topcoder-react-utils'; import _ from 'lodash'; @@ -48,6 +48,8 @@ const PODIUM_ITEM_MODIFIER = { 2: 'second', 3: 'third', 4: 'fourth', + 5: 'fifth', + 6: 'sixt', }; /** @@ -78,6 +80,8 @@ const CUSTOM_STYLES = { 2: avatarStyles['tco23-2'], 3: avatarStyles['tco23-3'], 4: avatarStyles['tco23-4'], + 5: avatarStyles['tco23-5'], + 6: avatarStyles['tco23-6'], }, }; @@ -103,6 +107,8 @@ const DISPLAY_RANKING_NUM = { 2: '2ND', 3: '3RD', 4: '4TH', + 5: '5TH', + 6: '6TH', }; /** @@ -121,14 +127,21 @@ export default function PodiumSpot(props) { isTopGear, isAlgo, themeName, + podiumPlaces, } = props; const stylesName = THEME[themeName]; + const tcoPoints = competitor['tco23_leaderboard.tco_points'] + || competitor['tco_leaderboard.tco_points'] + || competitor.points + || competitor['tco_leaderboard.total_score'] + || competitor['srm_tco19.score']; + const tcoChallengeCnt = competitor['tco23_leaderboard.challenge_count'] || competitor['tco_leaderboard.challenge_count'] || competitor.challengecount; let photoUrl = competitor['member_profile_basic.photo_url'] || competitor.avatar; if (photoUrl) { photoUrl = `${config.CDN.PUBLIC}/avatar/${encodeURIComponent(photoUrl)}?size=160`; } - let rootStyle = `${stylesName}.PodiumSpot`; + let rootStyle = `${stylesName}.${podiumPlaces > 3 ? 'PodiumSpotCondense' : 'PodiumSpot'}`; if (PODIUM_ITEM_MODIFIER[competitor.rank]) rootStyle += ` ${stylesName}.PodiumSpot--${PODIUM_ITEM_MODIFIER[competitor.rank]}`; const fulfillment = competitor['tco_leaderboard.fulfillment'] ? (parseFloat(competitor['tco_leaderboard.fulfillment']) * 100).toFixed(2).replace(/[.,]00$/, '') @@ -137,7 +150,7 @@ export default function PodiumSpot(props) { return themeName === 'TCO23' ? (
{ - competitor.rank <= 4 &&

{`${DISPLAY_RANKING_NUM[competitor.rank]} PLACE`}

+ competitor.rank <= 6 &&

{`${DISPLAY_RANKING_NUM[competitor.rank]} PLACE`}

} { onUsernameClick ? ( @@ -162,15 +175,24 @@ export default function PodiumSpot(props) {
4 ? 4 : competitor.rank]}`}> 3 ? 200 : 392} + height={podiumPlaces > 3 ? 16 : 25} fill="none" - viewBox="0 0 392 25" + viewBox={`0 0 ${podiumPlaces > 3 ? 200 : 392} ${podiumPlaces > 3 ? 16 : 25}`} > - + { + podiumPlaces > 3 ? ( + + ) : ( + + ) + } { @@ -194,28 +216,28 @@ export default function PodiumSpot(props) {
) : null } + { + isAlgo ? ( +
+ {tcoPoints} + total score +
+ ) : null + } +
+ {formatPoints(tcoPoints)} + points +
- {competitor['tco_leaderboard.challenge_count'] || competitor.challengecount} + {tcoChallengeCnt} { isAlgo ? ( - # of matches + matches ) : ( challenges ) }
-
- {formatPoints(competitor['tco_leaderboard.tco_points'] || competitor.points)} - points -
- { - isAlgo ? ( -
- {competitor['tco_leaderboard.total_score'] || competitor['srm_tco19.score']} - total score -
- ) : null - }
@@ -294,7 +316,7 @@ export default function PodiumSpot(props) { ) : null }
- {competitor['tco_leaderboard.challenge_count'] || competitor.challengecount} + {tcoChallengeCnt} { isAlgo ? ( # of matches @@ -304,7 +326,7 @@ export default function PodiumSpot(props) { }
- {formatPoints(competitor['tco_leaderboard.tco_points'] || competitor.points)} + {formatPoints(tcoPoints)} points
{ @@ -326,7 +348,7 @@ export default function PodiumSpot(props) { { isAlgo ? (
- {competitor['tco_leaderboard.total_score'] || competitor['srm_tco19.score']} + {tcoPoints} total score
) : null @@ -354,6 +376,7 @@ PodiumSpot.propTypes = { isTopGear: PT.bool, isAlgo: PT.bool, themeName: PT.string, + podiumPlaces: number, }; PodiumSpot.defaultProps = { @@ -362,4 +385,5 @@ PodiumSpot.defaultProps = { isTopGear: false, isAlgo: false, themeName: 'Default', + podiumPlaces: 1, }; diff --git a/src/shared/components/Leaderboard/PodiumSpot/themes/styles.scss b/src/shared/components/Leaderboard/PodiumSpot/themes/styles.scss index 9f45f5f9be..cceab354ad 100644 --- a/src/shared/components/Leaderboard/PodiumSpot/themes/styles.scss +++ b/src/shared/components/Leaderboard/PodiumSpot/themes/styles.scss @@ -1,7 +1,8 @@ @import '~styles/mixins'; $podium-border-color: #ededf2; -.PodiumSpot { +.PodiumSpot, +.PodiumSpotCondense { border: 1px solid $podium-border-color; border-radius: 6px; padding: 20px 0; diff --git a/src/shared/components/Leaderboard/PodiumSpot/themes/tco20.scss b/src/shared/components/Leaderboard/PodiumSpot/themes/tco20.scss index fffcb4919b..a09b62ca94 100644 --- a/src/shared/components/Leaderboard/PodiumSpot/themes/tco20.scss +++ b/src/shared/components/Leaderboard/PodiumSpot/themes/tco20.scss @@ -1,7 +1,8 @@ @import '~styles/mixins'; $podium-border-color: #ededf2; -.PodiumSpot { +.PodiumSpot, +.PodiumSpotCondense { border-radius: 6px; padding: 23px 15px; display: flex; diff --git a/src/shared/components/Leaderboard/PodiumSpot/themes/tco22.scss b/src/shared/components/Leaderboard/PodiumSpot/themes/tco22.scss index 319e1bd11e..8f03f24033 100644 --- a/src/shared/components/Leaderboard/PodiumSpot/themes/tco22.scss +++ b/src/shared/components/Leaderboard/PodiumSpot/themes/tco22.scss @@ -1,7 +1,8 @@ @import '~styles/mixins'; $podium-border-color: #ededf2; -.PodiumSpot { +.PodiumSpot, +.PodiumSpotCondense { border-radius: 12px; padding: 22px 17px 12px; display: flex; diff --git a/src/shared/components/Leaderboard/PodiumSpot/themes/tco23.scss b/src/shared/components/Leaderboard/PodiumSpot/themes/tco23.scss index 663879a3f2..facff2d8c8 100644 --- a/src/shared/components/Leaderboard/PodiumSpot/themes/tco23.scss +++ b/src/shared/components/Leaderboard/PodiumSpot/themes/tco23.scss @@ -2,7 +2,8 @@ @import '~styles/mixins'; $podium-border-color: #ededf2; -.PodiumSpot { +.PodiumSpot, +.PodiumSpotCondense { border-radius: 8px; display: flex; flex-direction: column; @@ -120,7 +121,7 @@ $podium-border-color: #ededf2; line-height: 44px; text-transform: uppercase; margin-bottom: 8px; - color: #2a2a2a; + color: #000; } .wave-wrap--first, @@ -133,11 +134,11 @@ $podium-border-color: #ededf2; flex-direction: column; align-items: center; justify-content: center; - margin-top: 120px; - padding-bottom: 36px; + margin-top: 45px; + padding-bottom: 24px; .leaderboard-avatar { - margin-top: -120px; + margin-top: -70px; border: 3px solid white; border-radius: 50%; height: 152px; @@ -160,6 +161,7 @@ $podium-border-color: #ededf2; svg { margin-top: -1px; + transform: scaleX(1.03); } } @@ -168,16 +170,102 @@ $podium-border-color: #ededf2; } .wave-wrap--second { - background-color: #219174; + background-color: #16679a; } .wave-wrap--third { - background-color: #16679a; + background-color: #227681; } .wave-wrap--fourth { + background-color: #219174; + + .stats { + color: #fff; + } + } + + .wave-wrap--fifth { + background-color: #219174; + } + + .wave-wrap--sixt { + background-color: #219174; + } +} + +.PodiumSpotCondense { + min-width: auto; + max-width: 200px; + padding-top: 24px; + + @media screen and (min-width: 375px) and (max-width: 768px) { + max-width: 162px; + margin: auto; + } + + @media screen and (max-width: 3320px) { + margin-bottom: 15px; + } + + .place { + @include barlow-condensed-semi-bold; + + font-size: 26px; + font-weight: 500; + line-height: 28px; + text-transform: uppercase; + margin-bottom: 8px; + color: #000; + } + + .handle-link { + font-size: 16px; + line-height: 24px; + } + + .leaderboard-avatar { + height: 88px !important; + width: 88px !important; + } + + .winnings-info { + margin-top: 16px; + + @include xs-to-sm { + flex-direction: column; + align-items: flex-start; + padding: 0 13px; + } + .stats { - color: #2a2a2a; + margin-right: 16px !important; + + @include xs-to-sm { + flex-direction: row; + margin-bottom: 4px; + } + + &:last-child { + margin-right: 0 !important; + } + + .value { + font-size: 14px !important; + font-weight: 700; + line-height: 16px !important; + text-align: left; + + @include xs-to-sm { + margin-right: 4px !important; + } + } + + .value-title { + font-size: 12px !important; + font-weight: 400; + line-height: 18px !important; + } } } } @@ -210,10 +298,26 @@ $podium-border-color: #ededf2; } } +.PodiumSpot--fifth { + .ranking { + background-color: #1e94a3; + color: #fff; + } +} + +.PodiumSpot--sixt { + .ranking { + background-color: #1e94a3; + color: #fff; + } +} + .PodiumSpot--first, .PodiumSpot--second, .PodiumSpot--third, -.PodiumSpot--fourth { +.PodiumSpot--fourth, +.PodiumSpot--fifth, +.PodiumSpot--sixt { margin-top: 0; .leaderboard-avatar { diff --git a/src/shared/components/Leaderboard/avatarStyles.scss b/src/shared/components/Leaderboard/avatarStyles.scss index c84446b00d..413dc5e999 100644 --- a/src/shared/components/Leaderboard/avatarStyles.scss +++ b/src/shared/components/Leaderboard/avatarStyles.scss @@ -63,7 +63,9 @@ .tco23-1, .tco23-2, .tco23-3, -.tco23-4 { +.tco23-4, +.tco23-5, +.tco23-6 { border-radius: 50%; height: 100%; width: 100%; 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 && (
{ diff --git a/src/shared/containers/Profile.jsx b/src/shared/containers/Profile.jsx index ee7e7d5df7..68755b7936 100644 --- a/src/shared/containers/Profile.jsx +++ b/src/shared/containers/Profile.jsx @@ -7,6 +7,7 @@ import PT from 'prop-types'; import { connect } from 'react-redux'; import { config } from 'topcoder-react-utils'; import { actions } from 'topcoder-react-lib'; +import profileActions from 'actions/page/profile'; import shortId from 'shortid'; import MetaTags from 'components/MetaTags'; import Error404 from 'components/Error404'; @@ -206,6 +207,8 @@ const mapStateToProps = (state, ownProps) => ({ 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();