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];
}