diff --git a/.circleci/config.yml b/.circleci/config.yml index aa0445c1ed..30e6fbba04 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -363,7 +363,7 @@ workflows: filters: branches: only: - - feature/dice-setup + - feat/badges-box # This is beta env for production soft releases - "build-prod-beta": context : org-global diff --git a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap index 3e08a0f553..71281dcec4 100644 --- a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap +++ b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap @@ -66,7 +66,10 @@ exports[`Matches shallow shapshot 2`] = ` disabled={false} expanding={false} isAuth={false} + isReviewer={false} + loading={true} past={false} + reviewCount={0} /> diff --git a/__tests__/shared/components/challenge-listing/Sidebar/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/Sidebar/__snapshots__/index.jsx.snap index 1baf5a8b91..aa2d45e0eb 100644 --- a/__tests__/shared/components/challenge-listing/Sidebar/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/challenge-listing/Sidebar/__snapshots__/index.jsx.snap @@ -13,7 +13,9 @@ exports[`Matches shallow shapshot 1`] = ` disabled={false} expanding={false} isAuth={false} + loading={true} past={false} + reviewCount={0} selectBucket={[MockFunction]} /> @@ -36,7 +38,9 @@ exports[`Matches shallow shapshot 2`] = ` disabled={false} expanding={false} isAuth={false} + loading={true} past={false} + reviewCount={0} selectBucket={[MockFunction]} /> diff --git a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap index 455a808b80..88fee86659 100644 --- a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap @@ -29,6 +29,7 @@ exports[`Matches shallow shapshot 1 shapshot 1 1`] = ` > CommonHelper.findElementByText( diff --git a/src/shared/actions/page/profile.js b/src/shared/actions/page/profile.js index cd8d10f6fe..60e15d117b 100644 --- a/src/shared/actions/page/profile.js +++ b/src/shared/actions/page/profile.js @@ -21,11 +21,11 @@ async function getGamificationBadgesInit(handle) { * @param {String} handle Topcoder member handle. * @return {Action} */ -async function getGamificationBadgesDone(handle) { +async function getGamificationBadgesDone(handle, limit) { 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}`) + const badges = await fetch(`${config.API.V5}/gamification/badges/assigned/${memberInfo.userId}?organization_id=${config.GAMIFICATION.ORG_ID}&limit=${limit || 4}`) .then(response => response.json()); return { diff --git a/src/shared/components/ProfileBadgesPage/index.jsx b/src/shared/components/ProfileBadgesPage/index.jsx new file mode 100644 index 0000000000..fd69254b3a --- /dev/null +++ b/src/shared/components/ProfileBadgesPage/index.jsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import PT from 'prop-types'; +import { Link } from 'react-router-dom'; +import { get } from 'lodash'; +import { Modal } from 'topcoder-react-ui-kit'; +import IconClose from 'assets/images/tc-edu/icon-close-big.svg'; +import FallBackAwardIcon from 'assets/images/default-award.svg'; +import md from 'utils/markdown'; +import { format } from 'date-fns'; +import AwardModal from '../ProfilePage/Awards/AwardModal'; + +import style from './styles.scss'; + +const ProfileBadges = ({ badges, handleParam }) => { + const [showModal, setShowModal] = useState(false); + const [modalData, setModalData] = useState({}); + + return ( +
+ + + + + Return to Profile + +
+
COMMUNITY AWARDS & HONORS
+
+ { + badges.rows.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); + } + let awardedAt = get(reward, 'awarded_at'); + if (awardedAt) { + awardedAt = format(new Date(awardedAt), 'PPP'); + } + + return ( +
{ + setShowModal(true); + setModalData({ + title, + description, + imageUrl, + awardedAt, + }); + }} + > + { + imageUrl ? ( + award-badge + ) : ( + + ) + } +
+ +
+ +
+
+ ); + }) + } +
+
+ { + showModal && ( + setShowModal(false)} theme={style}> +
+
+

Community Awards & Honors

+
setShowModal(false)}> + +
+
+
+ + +
+
+ ) + } +
+ ); +}; + +ProfileBadges.defaultProps = { + badges: {}, +}; + +ProfileBadges.propTypes = { + badges: PT.shape(), + handleParam: PT.string.isRequired, +}; + +export default ProfileBadges; diff --git a/src/shared/components/ProfileBadgesPage/styles.scss b/src/shared/components/ProfileBadgesPage/styles.scss new file mode 100644 index 0000000000..df6c570438 --- /dev/null +++ b/src/shared/components/ProfileBadgesPage/styles.scss @@ -0,0 +1,144 @@ +/* stylelint-disable no-descending-specificity */ +@import "~styles/mixins"; + +.outer-container { + width: 100%; + max-width: $screen-max; + margin: 0 auto; + display: flex; + flex-direction: column; + + @include xs-to-md { + margin: 0 32px; + } + + .memberPageBackLink { + text-transform: uppercase; + color: $listing-checkbox-green; + font-weight: 700; + font-family: Roboto, sans-serif; + margin: 32px 0; + display: flex; + align-items: center; + + svg { + margin-right: 6px; + } + } + + .badgesWrap { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + border-radius: 8px; + display: flex; + flex-direction: column; + padding: 32px; + margin-bottom: 32px; + + .seactionTitle { + @include barlow-medium; + + font-weight: 600; + color: #2a2a2a; + font-size: 22px; + line-height: 26px; + text-transform: uppercase; + padding-bottom: 24px; + border-bottom: 2px solid #e9e9e9; + } + + .badgesGrid { + display: grid; + grid-template-columns: repeat(6, 1fr); + + @include xs-to-sm { + grid-template-columns: repeat(2, 1fr); + } + + @include md { + grid-template-columns: repeat(4, 1fr); + } + + .awardBadge { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + padding-top: 32px; + + .image { + width: 100px; + height: 100px; + } + + .title { + @include roboto-bold; + $color: $tco-black; + + font-size: 12px; + font-weight: 700; + line-height: 16px; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + align-items: center; + max-width: 130px; + margin-top: 17px; + text-transform: uppercase; + + @include xs-to-sm { + max-width: unset; + } + } + } + } + } +} + +.award-modal { + padding-bottom: 10px; + border-radius: 8px; + margin: 25px 32px 32px; + + .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; + } + } +} + +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/Awards/AwardBadge/index.jsx b/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx index 39a2a126eb..1903b27c2f 100644 --- a/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx +++ b/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx @@ -17,6 +17,7 @@ const AwardBadge = ({ }
+ {/* eslint-disable-next-line react/no-danger */}
diff --git a/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss b/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss index a8dd313416..1f9c43019a 100644 --- a/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss +++ b/src/shared/components/ProfilePage/Awards/AwardBadge/styles.scss @@ -6,6 +6,7 @@ border-radius: 8px; display: flex; cursor: pointer; + min-width: 316px; .image { width: 48px; diff --git a/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx b/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx index 4eb1257d25..5d52322494 100644 --- a/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx +++ b/src/shared/components/ProfilePage/Awards/AwardModal/index.jsx @@ -8,7 +8,7 @@ const AwatarModal = ({ modalData, }) => { const { - title, description, imageUrl, + title, description, imageUrl, awardedAt, } = modalData; return ( @@ -25,6 +25,9 @@ const AwatarModal = ({
{title}
+ { + awardedAt &&
{`Awarded on ${awardedAt}`}
+ }
{description}
@@ -41,6 +44,7 @@ AwatarModal.propTypes = { modalData: PT.shape( { title: PT.string, + awardedAt: PT.string, description: PT.string, imageUrl: PT.string, }, diff --git a/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss b/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss index 0a230c30e9..953e239a88 100644 --- a/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss +++ b/src/shared/components/ProfilePage/Awards/AwardModal/styles.scss @@ -27,6 +27,10 @@ justify-content: center; flex-direction: column; + @include xs-to-md { + align-items: center; + } + .title { @include roboto-bold; @@ -40,6 +44,16 @@ } } + .awardedAt { + @include roboto-bold; + + color: $listing-placeholder-gray; + font-size: 12px; + line-height: 16px; + text-transform: uppercase; + margin-top: 8px; + } + .description { @include brackets-content; @@ -47,7 +61,7 @@ color: $tco-black; font-size: 16px; line-height: 24px; - margin-top: 10px; + margin-top: 16px; } @include xs-to-md { diff --git a/src/shared/components/ProfilePage/Awards/index.jsx b/src/shared/components/ProfilePage/Awards/index.jsx index 7551d74071..b5b9b0109a 100644 --- a/src/shared/components/ProfilePage/Awards/index.jsx +++ b/src/shared/components/ProfilePage/Awards/index.jsx @@ -4,12 +4,15 @@ 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 { Link } from 'react-router-dom'; +import { format } from 'date-fns'; import style from './styles.scss'; import AwardBadge from './AwardBadge'; import AwatarModal from './AwardModal'; -const Awards = ({ badges }) => { + +const Awards = ({ badges, info }) => { const [showModal, setShowModal] = useState(false); const [modalData, setModalData] = useState({}); @@ -18,6 +21,12 @@ const Awards = ({ badges }) => {
Community Awards & Honors + + View All Badges +
@@ -29,6 +38,10 @@ const Awards = ({ badges }) => { if (description) { description = md(description); } + let awardedAt = _.get(reward, 'awarded_at'); + if (awardedAt) { + awardedAt = format(new Date(awardedAt), 'PPP'); + } return ( { title, description, imageUrl, + awardedAt, }); }} /> @@ -77,6 +91,7 @@ Awards.defaultProps = { Awards.propTypes = { badges: PT.arrayOf(PT.shape()), + info: PT.shape().isRequired, }; export default Awards; diff --git a/src/shared/components/ProfilePage/Awards/styles.scss b/src/shared/components/ProfilePage/Awards/styles.scss index 2fc895902b..8b272ce659 100644 --- a/src/shared/components/ProfilePage/Awards/styles.scss +++ b/src/shared/components/ProfilePage/Awards/styles.scss @@ -19,6 +19,13 @@ cursor: pointer; margin-top: 5px; } + + .badgesPageLink { + text-transform: uppercase; + color: $listing-checkbox-green; + font-weight: 700; + font-family: Roboto, sans-serif; + } } .awards { @@ -28,7 +35,7 @@ padding-bottom: 32px; .header { - padding: 32px 0 20px 32px; + padding: 32px 32px 20px 32px; @include xs-to-sm { padding: 16px 0 16px 16px; diff --git a/src/shared/components/ProfilePage/index.jsx b/src/shared/components/ProfilePage/index.jsx index c981a9fd20..60953d0d5d 100644 --- a/src/shared/components/ProfilePage/index.jsx +++ b/src/shared/components/ProfilePage/index.jsx @@ -229,7 +229,7 @@ class ProfilePage extends React.Component {
{ (config.GAMIFICATION.ENABLE_BADGE_UI && badges && (badges.rows || [])).length ? ( - + ) : null } {tcAcademyCertifications.length > 0 && ( diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx index a92b2e90a6..3cd0d3328f 100644 --- a/src/shared/components/challenge-detail/Header/index.jsx +++ b/src/shared/components/challenge-detail/Header/index.jsx @@ -397,7 +397,7 @@ export default function ChallengeHeader(props) { onClick={unregisterFromChallenge} theme={{ button: unregisterButtonDisabled - ? style.submitButtonDisabled + ? style.unregisterButtonDisabled : style.unregisterButton, }} > @@ -423,16 +423,16 @@ export default function ChallengeHeader(props) { Submit { - track === COMPETITION_TRACKS.DES && hasRegistered && !unregistering + track === COMPETITION_TRACKS.DES && hasRegistered && !unregistering && hasSubmissions && ( - - View Submissions - - ) - } + + View Submissions + + ) + }
diff --git a/src/shared/components/challenge-detail/Header/style.scss b/src/shared/components/challenge-detail/Header/style.scss index f97c457a6d..a3daf5e533 100644 --- a/src/shared/components/challenge-detail/Header/style.scss +++ b/src/shared/components/challenge-detail/Header/style.scss @@ -10,66 +10,150 @@ padding: 0 25px !important; } -.submitButton { - margin: $base-unit 0; +.challenge-ops-container .submitButton { + margin: 5px; min-width: 0; - width: 100%; border-radius: 50px !important; height: 48px; - padding: 0 25px !important; background: #137d60 !important; color: #fff !important; + white-space: nowrap; + padding: 12px 24px !important; + font-size: 16px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.008em; @include xs-to-sm { width: fit-content; } } -.submitButtonDisabled { - margin: $base-unit 0; +.challenge-ops-container .submitButtonDisabled { + margin: 5px; min-width: 0; - width: 100%; border-radius: 50px !important; height: 48px; - padding: 0 25px !important; + padding: 12px 24px !important; color: #767676 !important; background: #f4f4f4 !important; + font-size: 16px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.008em; @include xs-to-sm { width: fit-content; } } -.unregisterButton { - margin: $base-unit 0; +.challenge-ops-container .unregisterButton { + margin: 5px; min-width: 0; - width: 100%; border-radius: 50px !important; height: 48px; - padding: 0 25px !important; + padding: 12px 24px !important; color: #137d60 !important; - border-color: #137d60 !important; + border: 2px solid #137d60 !important; background: #fff !important; + font-size: 16px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.008em; @include xs-to-sm { width: fit-content; } } -.registerBtn { - margin: $base-unit 0; +.challenge-ops-container .unregisterButtonDisabled { + margin: 5px; min-width: 0; - width: 100%; border-radius: 50px !important; height: 48px; - padding: 0 25px !important; + padding: 12px 24px !important; + color: #767676 !important; + border: 2px solid #f4f4f4; + background: #fff !important; + font-size: 16px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.008em; + + @include xs-to-sm { + width: fit-content; + } +} + +.challenge-ops-container .registerBtn { + margin: 5px; + min-width: 0; + border-radius: 50px !important; + height: 48px; + padding: 12px 24px !important; background-color: #137d60 !important; + font-size: 16px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.008em; @include xs-to-sm { width: fit-content; } } +.challenge-ops-container .submitButton:hover, +.challenge-ops-container .registerBtn:hover { + color: #fff !important; + background: #219174 !important; + border-color: #219174 !important; +} + +.challenge-ops-container .unregisterButton:hover { + color: #219174 !important; + border-color: #219174 !important; + background: #fff !important; +} + +.challenge-ops-container .submitButton:active, +.challenge-ops-container .registerBtn:active { + color: #fff !important; + background: #0d664e !important; + border-color: #0d664e !important; +} + +.challenge-ops-container .unregisterButton:active { + outline: none !important; + box-shadow: none !important; + color: #0d664e !important; + border-color: #0d664e !important; + background: #fff !important; +} + +.challenge-ops-container .submitButton:focus, +.challenge-ops-container .submitButton:focus-within, +.challenge-ops-container .submitButton:focus-visible { + outline: none !important; + box-shadow: none !important; + border-color: #0d664e; +} + +.challenge-ops-container .registerBtn:focus, +.challenge-ops-container .registerBtn:focus-within, +.challenge-ops-container .registerBtn:focus-visible { + outline: none !important; + box-shadow: none !important; + border-color: #0d664e; +} + +.challenge-ops-container .unregisterButton:focus, +.challenge-ops-container .unregisterButton:focus-within, +.challenge-ops-container .unregisterButton:focus-visible { + outline: none !important; + box-shadow: none !important; + border-color: #0d664e; +} + .challenge-ops-container { display: flex; margin-top: 32px; diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx index 9506f868d6..bb51b1f3fd 100644 --- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx +++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx @@ -31,6 +31,7 @@ import Tooltip from 'components/Tooltip'; import { config, Link } from 'topcoder-react-utils'; import { COMPOSE, PRIORITY } from 'react-css-super-themr'; import { REVIEW_OPPORTUNITY_TYPES } from 'utils/tc'; +import { isReviewerOrAdmin } from 'utils/challenge-listing/helper'; import { isFilterEmpty, isPastBucket, BUCKETS } from 'utils/challenge-listing/buckets'; import SwitchWithLabel from 'components/SwitchWithLabel'; import ChallengeSearchBar from 'containers/challenge-listing/ChallengeSearchBar'; @@ -71,6 +72,7 @@ export default function FiltersPanel({ setExpanded, setSort, selectBucket, + reviewCount, }) { if (hidden && !expanded) { return ( @@ -382,8 +384,10 @@ export default function FiltersPanel({ disabled={disabled} expanding={expanding} isAuth={isAuth} + isReviewer={isReviewerOrAdmin(auth)} selectBucket={selectBucket} past={past} + reviewCount={reviewCount} /> @@ -757,6 +761,7 @@ FiltersPanel.defaultProps = { onClose: _.noop, expanding: false, disabled: false, + reviewCount: 0, }; FiltersPanel.propTypes = { @@ -787,4 +792,5 @@ FiltersPanel.propTypes = { selectBucket: PT.func.isRequired, expanding: PT.bool, disabled: PT.bool, + reviewCount: PT.number, }; diff --git a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx index 07cc41627d..9408e22f31 100644 --- a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx @@ -84,7 +84,7 @@ export default function ReviewOpportunityBucket({ filteredOpportunities ? filteredOpportunities.length > 0 && ( ({ label: Sort[item].name, @@ -100,7 +100,7 @@ export default function ReviewOpportunityBucket({ ) : ( ({ diff --git a/src/shared/components/challenge-listing/Listing/index.jsx b/src/shared/components/challenge-listing/Listing/index.jsx index dc4b67d2d3..52b91f5681 100644 --- a/src/shared/components/challenge-listing/Listing/index.jsx +++ b/src/shared/components/challenge-listing/Listing/index.jsx @@ -15,6 +15,7 @@ import Bucket from './Bucket'; import ReviewOpportunityBucket from './ReviewOpportunityBucket'; import CardPlaceholder from '../placeholders/ChallengeCard'; import './style.scss'; +import { isReviewerOrAdmin } from '../../../utils/challenge-listing/helper'; // const Filter = challengeUtils.filter; const LOADING_MESSAGE = 'Loading Challenges'; @@ -146,25 +147,33 @@ function Listing({ * and are only shown when explicitly chosen from the sidebar */ isReviewOpportunitiesBucket(bucket) ? ( - setSort(bucket, sort)} - sort={sorts[bucket]} - challengeTypes={challengeTypes} - isLoggedIn={isLoggedIn} - setSearchText={setSearchText} - /> + + { + isReviewerOrAdmin(auth) ? ( + setSort(bucket, sort)} + sort={sorts[bucket]} + challengeTypes={challengeTypes} + isLoggedIn={isLoggedIn} + setSearchText={setSearchText} + /> + ) : ( +
You have no access to review page.
+ ) + } +
) : ( {BUCKET_DATA[bucket].name} - {(bucket !== BUCKETS.ALL && count > 0) ? {count} : null} + {(bucket !== BUCKETS.ALL && bucket !== BUCKETS.REVIEW_OPPORTUNITIES && count > 0 && !loading) ? {count} : null} + {(bucket === BUCKETS.REVIEW_OPPORTUNITIES && count > 0) ? {count} : null} ); } @@ -100,6 +106,8 @@ Bucket.defaultProps = { disabled: false, onClick: _.noop, meta: {}, + reviewCount: 0, + loading: true, }; Bucket.propTypes = { @@ -116,6 +124,8 @@ Bucket.propTypes = { onClick: PT.func, meta: PT.shape(), // allActiveChallengesLoaded: PT.bool.isRequired, + loading: PT.bool, + reviewCount: PT.number, }; const mapStateToProps = (state) => { diff --git a/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/style.scss b/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/style.scss index 08fff51366..9f660fa94e 100644 --- a/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/style.scss +++ b/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/style.scss @@ -52,7 +52,7 @@ font-weight: 400; font-size: 16px; - line-height: 24px; + line-height: 26px; color: $tco-black; margin-left: 5px; } @@ -74,7 +74,7 @@ text-align: center; padding: 0 8px; margin-left: 8px; - margin-bottom: 5px; + margin-bottom: 8px; } } diff --git a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx index db92ca637c..00d50cbdba 100644 --- a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx +++ b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx @@ -7,6 +7,7 @@ import PT from 'prop-types'; import React from 'react'; import { BUCKETS } from 'utils/challenge-listing/buckets'; +import { isReviewerOrAdmin } from 'utils/challenge-listing/helper'; // import { challenge as challengeUtils } from 'topcoder-react-lib'; import Bucket from './Bucket'; @@ -29,11 +30,15 @@ export default function BucketSelector({ // extraBucket, // filterState, isAuth, + // isReviewer, // savedFilters, selectBucket, // selectSavedFilter, // setEditSavedFiltersMode, past, + auth, + reviewCount, + loading, }) { // let filteredChallenges = challenges.filter(Filter.getFilterFunction(filterState)); @@ -49,6 +54,7 @@ export default function BucketSelector({ { @@ -57,6 +63,7 @@ export default function BucketSelector({ document.body.scrollTop = 0; document.documentElement.scrollTop = 0; }} + loading={loading} /> ); }; @@ -89,7 +96,7 @@ export default function BucketSelector({ {getBucket(BUCKETS.OPEN_FOR_REGISTRATION)} {/* DISABLED: Until api receive fix community-app#5073 */} {/* {getBucket(BUCKETS.ONGOING)} */} - {getBucket(BUCKETS.REVIEW_OPPORTUNITIES)} + {isReviewerOrAdmin(auth) ? getBucket(BUCKETS.REVIEW_OPPORTUNITIES) : null} {/* {getBucket(BUCKETS.PAST)} */} {/* NOTE: We do not show upcoming challenges for now, for various reasons, * more political than technical ;) @@ -140,11 +147,19 @@ BucketSelector.defaultProps = { disabled: false, // extraBucket: null, isAuth: false, + // isReviewer: false, expanding: false, past: false, + reviewCount: 0, + loading: true, }; BucketSelector.propTypes = { + auth: PT.shape({ + profile: PT.shape(), + tokenV3: PT.string, + user: PT.shape(), + }).isRequired, activeBucket: PT.string.isRequired, expanding: PT.bool, // activeSavedFilter: PT.number.isRequired, @@ -156,9 +171,12 @@ BucketSelector.propTypes = { // extraBucket: PT.string, // filterState: PT.shape().isRequired, isAuth: PT.bool, + // isReviewer: PT.bool, // savedFilters: PT.arrayOf(PT.shape()).isRequired, selectBucket: PT.func.isRequired, + reviewCount: PT.number, // selectSavedFilter: PT.func.isRequired, // setEditSavedFiltersMode: PT.func.isRequired, past: PT.bool, + loading: PT.bool, }; diff --git a/src/shared/components/challenge-listing/Sidebar/index.jsx b/src/shared/components/challenge-listing/Sidebar/index.jsx index b0ad3de237..5ecd71537b 100644 --- a/src/shared/components/challenge-listing/Sidebar/index.jsx +++ b/src/shared/components/challenge-listing/Sidebar/index.jsx @@ -17,7 +17,7 @@ import React from 'react'; import PT from 'prop-types'; -// import _ from 'lodash'; +// import { isReviewerOrAdmin } from 'utils/challenge-listing/helper'; import { isPastBucket } from 'utils/challenge-listing/buckets'; import ChallengeSearchBar from 'containers/challenge-listing/ChallengeSearchBar'; import BucketSelector from './BucketSelector'; @@ -42,6 +42,7 @@ export default function SideBarFilters({ // extraBucket, // filterState, // hideTcLinksInFooter, + auth, isAuth, // resetFilterName, // savedFilters, @@ -52,6 +53,8 @@ export default function SideBarFilters({ // updateSavedFilter, // setFilter, setFilterState, + reviewCount, + loading, }) { const past = isPastBucket(activeBucket); @@ -113,11 +116,14 @@ export default function SideBarFilters({ // extraBucket={extraBucket} // filterState={filterState} isAuth={isAuth} + auth={auth} + reviewCount={reviewCount} // savedFilters={savedFilters} selectBucket={selectBucket} // selectSavedFilter={selectSavedFilter} // setEditSavedFiltersMode={setEditSavedFiltersMode} past={past} + loading={loading} /> {/* )} */} @@ -134,6 +140,7 @@ SideBarFilters.defaultProps = { // extraBucket: null, // hideTcLinksInFooter: false, isAuth: false, + reviewCount: 0, expanding: false, }; @@ -155,7 +162,9 @@ SideBarFilters.propTypes = { // extraBucket: PT.string, // filterState: PT.shape().isRequired, // hideTcLinksInFooter: PT.bool, + auth: PT.shape().isRequired, isAuth: PT.bool, + reviewCount: PT.number, // resetFilterName: PT.func.isRequired, // savedFilters: PT.arrayOf(PT.shape()).isRequired, selectBucket: PT.func.isRequired, @@ -165,4 +174,5 @@ SideBarFilters.propTypes = { // updateSavedFilter: PT.func.isRequired, // setFilter: PT.func.isRequired, setFilterState: PT.func.isRequired, + loading: PT.bool.isRequired, }; diff --git a/src/shared/components/challenge-listing/index.jsx b/src/shared/components/challenge-listing/index.jsx index 00b5c564e7..c8bec33705 100644 --- a/src/shared/components/challenge-listing/index.jsx +++ b/src/shared/components/challenge-listing/index.jsx @@ -7,6 +7,10 @@ import FilterPanel from 'containers/challenge-listing/FilterPanel'; // import moment from 'moment'; import React from 'react'; +import { + BUCKET_DATA, +} from 'utils/challenge-listing/buckets'; +import { challenge as challengeUtils } from 'topcoder-react-lib'; import PT from 'prop-types'; // import { challenge as challengeUtils } from 'topcoder-react-lib'; import Sidebar from 'containers/challenge-listing/Sidebar'; @@ -20,7 +24,7 @@ import ChallengeTab from './ChallengeTab'; import './style.scss'; -// const Filter = challengeUtils.filter; +const Filter = challengeUtils.filter; // Number of challenge placeholder card to display // const CHALLENGE_PLACEHOLDER_COUNT = 8; @@ -54,10 +58,30 @@ export default function ChallengeListing(props) { setPreviousBucketOfPastChallengesTab, previousBucketOfPastChallengesTab, previousBucketOfActiveTab, + reviewOpportunities, + filterState, + challengeTypes, } = props; // const { challenges } = props; + // const activeSort = sort || BUCKET_DATA[bucket].sorts[0]; + + // const sortedOpportunities = _.clone(opportunities); + // sortedOpportunities.sort(Sort[activeSort].func); + + /* Filtering for Review Opportunities will be done entirely in the front-end + * which means it can be done at render, rather than in the reducer, + * which avoids reloading the review opportunities from server every time + * a filter is changed. */ + const filteredOpportunities = reviewOpportunities.filter( + Filter.getReviewOpportunitiesFilterFunction({ + ...BUCKET_DATA.reviewOpportunities.filter, // Default bucket filters from utils/buckets.js + ...filterState, // User selected filters + }, challengeTypes), + // }), + ); + // if (communityFilter) { // challenges = challenges.filter(Filter.getFilterFunction(props.communityFilter)); // } @@ -175,6 +199,10 @@ export default function ChallengeListing(props) { { + loadBadges(handleParam); + }, []); + + return ( + + + { + !isEmpty(badges) ? ( + + ) : + } + + ); +} + +ProfileBadgesContainer.defaultProps = { + profile: null, +}; + +ProfileBadgesContainer.propTypes = { + profile: PT.shape(), + loadBadges: PT.func.isRequired, + handleParam: PT.string.isRequired, + badges: PT.shape().isRequired, +}; + +function mapStateToProps(state, ownProps) { + const profile = state.auth && state.auth.profile ? { ...state.auth.profile } : {}; + return { + handleParam: ownProps.match.params.handle, + badges: state.page.profile[ownProps.match.params.handle] + ? state.page.profile[ownProps.match.params.handle].badges : {}, + profile, + }; +} + +function mapDispatchToActions(dispatch) { + return { + loadBadges: (handle) => { + dispatch(profileActions.page.profile.getGamificationBadgesInit(handle)); + dispatch(profileActions.page.profile.getGamificationBadgesDone(handle, 100)); + }, + }; +} + +export default connect( + mapStateToProps, + mapDispatchToActions, +)(ProfileBadgesContainer); diff --git a/src/shared/containers/challenge-listing/Listing/index.jsx b/src/shared/containers/challenge-listing/Listing/index.jsx index d92ec6cd27..920fd523a5 100644 --- a/src/shared/containers/challenge-listing/Listing/index.jsx +++ b/src/shared/containers/challenge-listing/Listing/index.jsx @@ -23,6 +23,7 @@ import Banner from 'components/tc-communities/Banner'; import sidebarActions from 'actions/challenge-listing/sidebar'; import filterPanelActions from 'actions/challenge-listing/filter-panel'; import communityActions from 'actions/tc-communities'; + // import SORT from 'utils/challenge-listing/sort'; import { BUCKETS, filterChanged, sortChangedBucket, @@ -64,6 +65,7 @@ export class ListingContainer extends React.Component { selectCommunity, queryBucket, filter, + getReviewOpportunities, } = this.props; markHeaderMenu(); @@ -105,6 +107,8 @@ export class ListingContainer extends React.Component { }); } // } + + getReviewOpportunities(0, auth.tokenV3); } componentDidUpdate(prevProps) { @@ -138,6 +142,7 @@ export class ListingContainer extends React.Component { loading, setFilter, } = this.props; + const oldUserId = _.get(prevProps, 'auth.user.userId'); const userId = _.get(this.props, 'auth.user.userId'); const handle = _.get(auth, 'user.handle'); @@ -512,6 +517,7 @@ export class ListingContainer extends React.Component { setFilter, setSort, sorts, + setReviewCount, // hideTcLinksInSidebarFooter, // isBucketSwitching, // userChallenges, @@ -698,6 +704,7 @@ export class ListingContainer extends React.Component { // userChallenges={[]} isLoggedIn={isLoggedIn} meta={meta} + setReviewCount={setReviewCount} setSearchText={setSearchText} previousBucketOfActiveTab={previousBucketOfActiveTab} previousBucketOfPastChallengesTab={previousBucketOfPastChallengesTab} @@ -833,6 +840,7 @@ ListingContainer.propTypes = { // userChallenges: PT.arrayOf(PT.string), // getUserChallenges: PT.func.isRequired, setSearchText: PT.func.isRequired, + setReviewCount: PT.func.isRequired, filterState: PT.shape().isRequired, loading: PT.bool, }; diff --git a/src/shared/containers/challenge-listing/Sidebar.jsx b/src/shared/containers/challenge-listing/Sidebar.jsx index cd53b07d1d..1bff3af62d 100644 --- a/src/shared/containers/challenge-listing/Sidebar.jsx +++ b/src/shared/containers/challenge-listing/Sidebar.jsx @@ -71,6 +71,12 @@ export class SidebarContainer extends React.Component { // userChallenges, // } = this.props; + const { + loadingMyChallenges, + loadingOpenForRegistrationChallenges, + loadingReviewOpportunities, + } = this.props; + const { previousBucketOfActiveTab, previousBucketOfPastChallengesTab, @@ -98,6 +104,10 @@ export class SidebarContainer extends React.Component { // const savedFilters = checkFilterErrors(origSavedFilters, updatedCommunityFilters); + const loading = loadingMyChallenges + || loadingOpenForRegistrationChallenges + || loadingReviewOpportunities; + return ( { this.setState({ previousBucketOfPastChallengesTab: bucket }); }} + loading={loading} /> ); } @@ -160,6 +171,9 @@ SidebarContainer.propTypes = { // user: PT.shape(), // userChallenges: PT.arrayOf(PT.string), expanding: PT.bool, + loadingMyChallenges: PT.bool.isRequired, + loadingOpenForRegistrationChallenges: PT.bool.isRequired, + loadingReviewOpportunities: PT.bool.isRequired, }; function mapDispatchToProps(dispatch) { @@ -189,6 +203,7 @@ function mapStateToProps(state) { // hideTcLinksInFooter: ownProps.hideTcLinksInFooter, filterState: state.challengeListing.filter, isAuth: Boolean(state.auth.user), + auth: state.auth, // communityFilters: state.tcCommunities.list.data, // selectedCommunityId: state.challengeListing.selectedCommunityId, // tokenV2: state.auth.tokenV2, diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index 319c245e0b..c831296092 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -507,6 +507,10 @@ function onGetReviewOpportunitiesDone(state, { payload, error }) { reviewOpportunities, loadingReviewOpportunitiesUUID: '', allReviewOpportunitiesLoaded: loaded.length === 0, + meta: { + ...state.meta, + openReviewCount: reviewOpportunities.length, + }, }; } diff --git a/src/shared/routes/ProfileBadges.jsx b/src/shared/routes/ProfileBadges.jsx new file mode 100644 index 0000000000..d53ea77376 --- /dev/null +++ b/src/shared/routes/ProfileBadges.jsx @@ -0,0 +1,27 @@ +/** + * The loader of Profile webpack chunks. + */ +import path from 'path'; +import React from 'react'; + +import LoadingPagePlaceholder from 'components/LoadingPagePlaceholder'; +import { AppChunk, webpack } from 'topcoder-react-utils'; + +export default function ProfileLoader(props) { + return ( + import(/* webpackChunkName: "profile-badges/chunk" */ 'containers/ProfileBadges') + .then(({ default: ProfileBadgesContainer }) => ( + + )) + } + renderPlaceholder={() => } + renderServer={() => { + const p = webpack.resolveWeak('containers/Profile'); + const ProfileBadgesContainer = webpack.requireWeak(path.resolve(__dirname, p)); + return ; + }} + /> + ); +} diff --git a/src/shared/routes/Topcoder/Routes.jsx b/src/shared/routes/Topcoder/Routes.jsx index c45868ad5c..769904af87 100644 --- a/src/shared/routes/Topcoder/Routes.jsx +++ b/src/shared/routes/Topcoder/Routes.jsx @@ -31,6 +31,7 @@ import Notifications from './Notifications'; import Settings from '../Settings'; import HallOfFame from '../HallOfFame'; import Profile from '../Profile'; +import ProfileBadges from '../ProfileBadges'; import Scoreboard from '../tco/scoreboard'; import MemberSearch from '../../containers/MemberSearch'; @@ -89,6 +90,15 @@ export default function Topcoder() { exact path="/members/:handle([\w\-\[\].{} ]{2,15})" /> + { + config.GAMIFICATION.ENABLE_BADGE_UI && ( + + ) + } } path="/settings" diff --git a/src/shared/utils/challenge-listing/buckets.js b/src/shared/utils/challenge-listing/buckets.js index ca85e0572a..368aa533a4 100644 --- a/src/shared/utils/challenge-listing/buckets.js +++ b/src/shared/utils/challenge-listing/buckets.js @@ -114,7 +114,7 @@ export const BUCKET_DATA = { [BUCKETS.REVIEW_OPPORTUNITIES]: { filter: {}, // hideCount: true, - name: 'Open for Review', + name: 'Review Opportunities', sorts: [ SORTS.REVIEW_OPPORTUNITIES_START_DATE, SORTS.REVIEW_OPPORTUNITIES_PAYMENT, diff --git a/src/shared/utils/challenge-listing/helper.js b/src/shared/utils/challenge-listing/helper.js index 919bba5f86..11998eb42f 100644 --- a/src/shared/utils/challenge-listing/helper.js +++ b/src/shared/utils/challenge-listing/helper.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import moment from 'moment'; /** @@ -62,3 +63,19 @@ export const formatOrdinals = (n) => { return ord; }; + +/** + * Check if user's role is reviewer or admin + * @param {Object || null} auth + * + * @returns {Boolean} + */ +export const isReviewerOrAdmin = (auth) => { + const roles = _.get(auth, 'user.roles'); + + if (!roles || !_.isArray(roles)) { + return false; + } + + return _.intersection(roles, ['administrator', 'Reviewer', 'Gamification Admin', 'Connect Admin', 'admin']).length; +};