diff --git a/src/actions/auth.js b/src/actions/auth.js index 4f19902..1f7241b 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -4,10 +4,11 @@ */ import { createActions } from "redux-actions"; -import { decodeToken } from "../utils/token"; +import { decodeToken, readCookie } from "../utils/token"; import { getApiV3, getApiV5 } from "../services/challenge-api"; import { setErrorIcon, ERROR_ICON_TYPES } from "../utils/errors"; import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app"; +import { TOKEN_COOKIE_KEYS } from "../constants/index"; /** * Helper method that checks for HTTP error response v5 and throws Error in this case. @@ -83,11 +84,27 @@ async function setAuthDone() { return user; } +/** + * @static + * @desc Check token cookies to find if a user is logged out: + * This is because all the token cookies are cleared if a user is logged out. + * @return {Action} + */ +function checkIsLoggedOut() { + const tokenKeys = Object.keys(TOKEN_COOKIE_KEYS); + const isLoggedOut = _.every( + tokenKeys, + (k) => readCookie(TOKEN_COOKIE_KEYS[k]) === undefined + ); + return { isLoggedOut }; +} + export default createActions({ AUTH: { LOAD_PROFILE: loadProfileDone, SET_TC_TOKEN_V2: setTcTokenV2, SET_TC_TOKEN_V3: setTcTokenV3, SET_AUTH_DONE: setAuthDone, + CHECK_IS_LOGGED_OUT: checkIsLoggedOut, }, }); diff --git a/src/actions/challenge.js b/src/actions/challenge.js index 5256ad9..84272bd 100644 --- a/src/actions/challenge.js +++ b/src/actions/challenge.js @@ -454,6 +454,20 @@ function getChallengeDone(challengeId) { return challengeService.getChallenge(challengeId); } +/** + * @static + * @desc Check if a user has registered a challenge + * @param {String} challengeId Challenge ID. + * @param {String} userId User Id. + * @return {Action} + */ +async function getIsRegistered(challengeId, userId) { + const registrants = await challengeService.getChallengeRegistrants(challengeId); + const isRegistered = _.some(registrants, (r) => `${r.memberId}` === `${userId}`); + return { isRegistered }; +} + + export default createActions({ CHALLENGE: { DROP_CHECKPOINTS: dropCheckpoints, @@ -483,5 +497,6 @@ export default createActions({ GET_SUBMISSION_INFORMATION_DONE: getSubmissionInformationDone, GET_CHALLENGE_INIT: _.noop, GET_CHALLENGE_DONE: getChallengeDone, + GET_IS_REGISTERED: getIsRegistered, }, }); diff --git a/src/components/DateRangePicker/index.jsx b/src/components/DateRangePicker/index.jsx index 5c82772..96b0d27 100644 --- a/src/components/DateRangePicker/index.jsx +++ b/src/components/DateRangePicker/index.jsx @@ -298,7 +298,7 @@ function DateRangePicker(props) { }); setIsComponentVisible(false); - } + }; /** * Event handler on date selection changes diff --git a/src/components/challenge-detail/Header/ChallengeTags.jsx b/src/components/challenge-detail/Header/ChallengeTags.jsx index f4f8ef2..a96a7fb 100644 --- a/src/components/challenge-detail/Header/ChallengeTags.jsx +++ b/src/components/challenge-detail/Header/ChallengeTags.jsx @@ -127,7 +127,9 @@ export default function ChallengeTags(props) { onClick={() => setImmediate(() => setChallengeListingFilter({ tags: [tag] })) } - to={`${challengesUrl}${filterByTag}&tags[]=${encodeURIComponent(tag)}`} + to={`${challengesUrl}${filterByTag}&tags[]=${encodeURIComponent( + tag + )}`} > {tag} diff --git a/src/components/challenge-listing/ChallengeLoading/index.jsx b/src/components/challenge-listing/ChallengeLoading/index.jsx new file mode 100644 index 0000000..42ac1e2 --- /dev/null +++ b/src/components/challenge-listing/ChallengeLoading/index.jsx @@ -0,0 +1,19 @@ +import React from "react"; +import "./styles.module.scss"; + +export default function ChallengeLoading() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/src/components/challenge-listing/ChallengeLoading/styles.module.scss b/src/components/challenge-listing/ChallengeLoading/styles.module.scss new file mode 100644 index 0000000..f0b4ed9 --- /dev/null +++ b/src/components/challenge-listing/ChallengeLoading/styles.module.scss @@ -0,0 +1,84 @@ +@import "~styles/mixins"; + +@keyframes placeholderAnim { + 0% { + background-position: -$base-unit * 94 0; + } + + 100% { + background-position: $base-unit * 94 0; + } +} + +.animated-placeholder-template { + animation-duration: 1.25s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: placeholderAnim; + animation-timing-function: linear; + background: $tc-gray-neutral-dark; + background: linear-gradient(to right, $tc-gray-neutral-dark 8%, $tc-gray-10 18%, $tc-gray-neutral-dark 33%); + background-size: $base-unit * 256 $base-unit * 20; + position: relative; +} + +.placeholder-template { + border-radius: $corner-radius; + + @extend .animated-placeholder-template; +} + +.challenge-loading { + height: 126px; + padding: 16px; + margin: 0 24px; + display: flex; + border-bottom: 1px solid #E9E9E9; + border-top: 1px solid #E9E9E9; + + > div { + margin-right: 16px; + } + + &:nth-child(even) { + background: #FBFBFB; + } + .track { + flex: 1 0 46px; + width: 46px; + height: 44px; + } + + .main { + flex: 1 1 70%; + } + + .title { + height: 22px; + width: 100px; + margin-bottom: 8px; + } + + .info { + height: 18px; + width: 200px; + margin-bottom: 16px; + } + + .footer { + height: 18px; + width: 70%; + } + + .prize { + height: 18px; + width: 60px; + margin-bottom: 8px; + margin-right: 40px; + } + + .prize-nominal { + height: 42px; + width: 40px; + } +} diff --git a/src/constants/index.js b/src/constants/index.js index 77faca2..c6c8cf8 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -96,3 +96,9 @@ export const COMPETITION_TRACKS = { DEV: "Development", QA: "Quality Assurance", }; + +export const TOKEN_COOKIE_KEYS = { + V3JWT: "v3jwt", + TCJWT: "tcjwt", + TCSSO: "tcsso", +}; diff --git a/src/containers/Challenges/Listing/ChallengeItem/index.jsx b/src/containers/Challenges/Listing/ChallengeItem/index.jsx index e9605b0..1fef4cd 100644 --- a/src/containers/Challenges/Listing/ChallengeItem/index.jsx +++ b/src/containers/Challenges/Listing/ChallengeItem/index.jsx @@ -12,8 +12,8 @@ import * as utils from "../../../../utils"; import ProgressTooltip from "../tooltips/ProgressTooltip"; import PlacementsTooltip from "../tooltips/PlacementsTooltip"; import TagsMoreTooltip from "../tooltips/TagsMoreTooltip"; -import { CHALLENGES_URL } from 'constants'; -import { Link } from '@reach/router'; +import { CHALLENGES_URL } from "constants"; +import { Link } from "@reach/router"; import "./styles.scss"; @@ -45,9 +45,7 @@ const ChallengeItem = ({ challenge, onClickTag, onClickTrack, isLoggedIn }) => {
- + {challenge.name}
@@ -72,9 +70,7 @@ const ChallengeItem = ({ challenge, onClickTag, onClickTrack, isLoggedIn }) => { />
- + diff --git a/src/containers/Challenges/Listing/index.jsx b/src/containers/Challenges/Listing/index.jsx index e376d19..4934b8c 100644 --- a/src/containers/Challenges/Listing/index.jsx +++ b/src/containers/Challenges/Listing/index.jsx @@ -9,14 +9,16 @@ import ChallengeItem from "./ChallengeItem"; import TextInput from "../../../components/TextInput"; import Dropdown from "../../../components/Dropdown"; import DateRangePicker from "../../../components/DateRangePicker"; +import ChallengeLoading from "../../../components/challenge-listing/ChallengeLoading"; import * as utils from "../../../utils"; + import * as constants from "../../../constants"; import IconSearch from "../../../assets/icons/search.svg"; - import "./styles.scss"; const Listing = ({ challenges, + loadingChallenges, search, page, perPage, @@ -111,7 +113,9 @@ const Listing = ({
- {challenges.length ? ( + {loadingChallenges ? + _.times(3, () => ) : + challenges.length ? ( {challenges.map((challenge, index) => (
{ const [isLoggedIn, setIsLoggedIn] = useState(null); @@ -76,20 +79,21 @@ const Challenges = ({

CHALLENGES - + {/* - + */}

- {initialized && ( + {initialized ? ( <> {/*noRecommendedChallenges && */} + ) : ( + + + + + )}
); @@ -128,6 +138,7 @@ Challenges.propTypes = { initialized: PT.bool, updateQuery: PT.func, tags: PT.arrayOf(PT.string), + loadingChallenges: PT.bool, }; const mapStateToProps = (state) => ({ @@ -146,6 +157,7 @@ const mapStateToProps = (state) => ({ recommendedChallenges: state.challenges.recommendedChallenges, initialized: state.challenges.initialized, tags: state.filter.challenge.tags, + loadingChallenges: state.challenges.loadingChallenges }); const mapDispatchToProps = { diff --git a/src/containers/Filter/ChallengeFilter/index.jsx b/src/containers/Filter/ChallengeFilter/index.jsx index cc3e9ae..e6d407f 100644 --- a/src/containers/Filter/ChallengeFilter/index.jsx +++ b/src/containers/Filter/ChallengeFilter/index.jsx @@ -121,7 +121,7 @@ const ChallengeFilter = ({ updateFilter(filterChange); }} /> - {track.replace('Quality Assurance', 'QA')} + {track.replace("Quality Assurance", "QA")} ))} diff --git a/src/containers/Submission/index.jsx b/src/containers/Submission/index.jsx index 9c1e1ea..81375be 100644 --- a/src/containers/Submission/index.jsx +++ b/src/containers/Submission/index.jsx @@ -5,6 +5,7 @@ import { navigate } from "@reach/router"; import { PrimaryButton } from "components/Buttons"; import AccessDenied from "components/AccessDenied"; import LoadingIndicator from "components/LoadingIndicator"; +import { login } from "@topcoder/micro-frontends-navbar-app"; import { ACCESS_DENIED_REASON, CHALLENGES_URL } from "../../constants"; import Submit from "./Submit"; import actions from "../../actions"; @@ -38,6 +39,7 @@ const Submission = ({ submitDone, uploadProgress, + getIsRegistered, getChallenge, submit, resetForm, @@ -47,6 +49,7 @@ const Submission = ({ setFilePickerUploadProgress, setFilePickerDragged, setSubmissionFilestackData, + checkIsLoggedOut, setAuth, }) => { const propsRef = useRef(); @@ -92,6 +95,17 @@ const Submission = ({ ); } + const handleSubmit = async (data) => { + const isLoggedOut = checkIsLoggedOut(); + if (isLoggedOut) { + window.sessionStorage && window.sessionStorage.clear(); + login(); + } else { + const registered = await getIsRegistered(challengeId, userId); + if (registered) submit(data); + } + }; + return ( ); }; @@ -155,6 +169,7 @@ Submission.propTypes = { uploadProgress: PT.number, getChallenge: PT.func, + getIsRegistered: PT.func, submit: PT.func, resetForm: PT.func, setAgreed: PT.func, @@ -164,6 +179,7 @@ Submission.propTypes = { setFilePickerDragged: PT.func, setSubmissionFilestackData: PT.func, setAuth: PT.func, + checkIsLoggedOut: PT.func, }; const mapStateToProps = (state, ownProps) => { @@ -209,6 +225,16 @@ const mapDispatchToProps = (dispatch) => { setAuth: () => { dispatch(actions.auth.setAuthDone()); }, + checkIsLoggedOut: () => { + const action = dispatch(actions.auth.checkIsLoggedOut()); + return action?.payload?.isLoggedOut; + }, + getIsRegistered: async (challengeId, userId) => { + const action = await dispatch( + actions.challenge.getIsRegistered(challengeId, userId) + ); + return action?.payload?.isRegistered; + }, getChallenge: (challengeId) => { dispatch(actions.challenge.getChallengeInit(challengeId)); dispatch(actions.challenge.getChallengeDone(challengeId)); diff --git a/src/containers/challenge-detail/index.jsx b/src/containers/challenge-detail/index.jsx index f3f3cfc..d84f39e 100644 --- a/src/containers/challenge-detail/index.jsx +++ b/src/containers/challenge-detail/index.jsx @@ -977,9 +977,7 @@ const mapDispatchToProps = (dispatch) => { change.types = constants.FILTER_CHALLENGE_TYPES; } dispatch(updateFilter(change)); - dispatch( - updateQuery({ ...stateProps.filter.challenge, ...change }) - ); + dispatch(updateQuery({ ...stateProps.filter.challenge, ...change })); }, setSpecsTabState: (state) => dispatch(pageActions.page.challengeDetails.setSpecsTabState(state)), diff --git a/src/reducers/challenge.js b/src/reducers/challenge.js index 346db3e..5e213f8 100644 --- a/src/reducers/challenge.js +++ b/src/reducers/challenge.js @@ -472,6 +472,30 @@ function onGetChallengeDone(state, { error, payload }) { }; } +/** + * Update isRegistered to before challenge submit + * @param {Object} state Old state. + * @param {Object} actions Action error/payload. + * @param {Object} action Action. + */ +function onGetIsRegistered(state, { error, payload }) { + if (error) { + logger.error("Failed to get the user's registration status!", payload); + fireErrorMessage( + "ERROR: Failed to submit", + "Please, try again a bit later" + ); + return state; + } + return { + ...state, + challenge: { + ...state.challenge, + isRegistered: payload.isRegistered + } + }; +} + /** * Creates a new Challenge reducer with the specified initial state. * @param {Object} initialState Optional. Initial state. @@ -520,6 +544,7 @@ function create(initialState) { [a.getSubmissionInformationDone]: onGetSubmissionInformationDone, [a.getChallengeInit]: onGetChallengeInit, [a.getChallengeDone]: onGetChallengeDone, + [a.getIsRegistered]: onGetIsRegistered, }, _.defaults(initialState, { details: null, diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 31ae15a..2266300 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -1,7 +1,7 @@ import { handleActions } from "redux-actions"; const defaultState = { - loadingChallenges: false, + loadingChallenges: true, loadingChallengesError: null, challenges: [], challengesMeta: {}, @@ -57,7 +57,7 @@ function onGetChallengesFailure(state, { payload }) { export default handleActions( { - GET_CHALLENGE_INIT: onGetChallengesInit, + GET_CHALLENGES_INIT: onGetChallengesInit, GET_CHALLENGES_DONE: onGetChallengesDone, }, defaultState diff --git a/src/services/challenge.js b/src/services/challenge.js index 38e4c6b..adde73c 100644 --- a/src/services/challenge.js +++ b/src/services/challenge.js @@ -113,4 +113,5 @@ async function getChallenge(challengeId) { export default { getChallenge, + getChallengeRegistrants }; diff --git a/src/utils/challenge.js b/src/utils/challenge.js index 1775a04..e9b1f04 100644 --- a/src/utils/challenge.js +++ b/src/utils/challenge.js @@ -8,47 +8,47 @@ import { initialChallengeFilter } from "../reducers/filter"; Joi.optionalId = () => Joi.string().uuid(); Joi.page = () => - Joi.alternatives() - .try( - Joi.number() - .min(1), - Joi.any().custom(() => 1) - ); + Joi.alternatives().try( + Joi.number().min(1), + Joi.any().custom(() => 1) + ); Joi.perPage = () => - Joi.alternatives() - .try( - Joi.number() - .integer() - .min(1) - .max(100) - .valid(...constants.PAGINATION_PER_PAGES), - Joi.any().custom(() => constants.PAGINATION_PER_PAGES[0]) - ); + Joi.alternatives().try( + Joi.number() + .integer() + .min(1) + .max(100) + .valid(...constants.PAGINATION_PER_PAGES), + Joi.any().custom(() => constants.PAGINATION_PER_PAGES[0]) + ); Joi.bucket = () => - Joi.string().custom((param) => - constants.FILTER_BUCKETS.find( - (bucket) => param && param.toLowerCase() === bucket.toLowerCase() - ) || null + Joi.string().custom( + (param) => + constants.FILTER_BUCKETS.find( + (bucket) => param && param.toLowerCase() === bucket.toLowerCase() + ) || null ); Joi.track = () => - Joi.string().custom((param) => - _.findKey( - constants.FILTER_CHALLENGE_TRACK_ABBREVIATIONS, - (trackAbbreviation) => - param && param.toLowerCase() === trackAbbreviation.toLowerCase() - ) || null + Joi.string().custom( + (param) => + _.findKey( + constants.FILTER_CHALLENGE_TRACK_ABBREVIATIONS, + (trackAbbreviation) => + param && param.toLowerCase() === trackAbbreviation.toLowerCase() + ) || null ); Joi.type = () => - Joi.string().custom((param) => - _.findKey( - constants.FILTER_CHALLENGE_TYPE_ABBREVIATIONS, - (typeAbbreviation) => - param && param.toLowerCase() === typeAbbreviation.toLowerCase() - ) || null + Joi.string().custom( + (param) => + _.findKey( + constants.FILTER_CHALLENGE_TYPE_ABBREVIATIONS, + (typeAbbreviation) => + param && param.toLowerCase() === typeAbbreviation.toLowerCase() + ) || null ); export function getCurrencySymbol(prizeSets) { diff --git a/src/utils/lifeCycle.js b/src/utils/lifeCycle.js index 0f64daf..28dce2b 100644 --- a/src/utils/lifeCycle.js +++ b/src/utils/lifeCycle.js @@ -1,7 +1,7 @@ import store from "../store"; import action from "../actions/initApp"; import * as utils from "../utils"; -import { CHALLENGES_URL } from '../constants'; +import { CHALLENGES_URL } from "../constants"; export default function appInit() { let initialQuery; diff --git a/src/utils/token.js b/src/utils/token.js index 0c914cf..f6f83a1 100644 --- a/src/utils/token.js +++ b/src/utils/token.js @@ -87,6 +87,6 @@ function parseCookie(cookie) { ); } -function readCookie(name) { +export function readCookie(name) { return parseCookie(document.cookie)[name]; }