diff --git a/src/containers/Challenges/Listing/ChallengeItem/styles.scss b/src/containers/Challenges/Listing/ChallengeItem/styles.scss
index 693f532..bc53fbc 100644
--- a/src/containers/Challenges/Listing/ChallengeItem/styles.scss
+++ b/src/containers/Challenges/Listing/ChallengeItem/styles.scss
@@ -1,4 +1,5 @@
@import "styles/variables";
+@import "styles/mixins";
.challenge-item {
display: flex;
@@ -35,8 +36,14 @@
}
.tags {
- max-width: calc(50% - 32px);
+ max-width: calc(50% - 84px);
+ min-width: calc(50% - 84px);
flex: 1 1 auto;
+
+ @media (min-width: $screen-xxl + 1px) {
+ min-width: 294px;
+ max-width: 25%;
+ }
}
.nums {
@@ -44,7 +51,15 @@
white-space: nowrap;
> * {
- margin: 0 16px;
+ margin: 0 20px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
}
}
}
diff --git a/src/containers/Challenges/Listing/ChallengeRecommendedError/index.jsx b/src/containers/Challenges/Listing/ChallengeRecommendedError/index.jsx
new file mode 100644
index 0000000..bc7fa3e
--- /dev/null
+++ b/src/containers/Challenges/Listing/ChallengeRecommendedError/index.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import IconNotFound from "assets/icons/not-found-recommended.png";
+import "./styles.scss";
+
+const ChallengeRecommendedError = () => (
+
+);
+
+export default ChallengeRecommendedError;
diff --git a/src/containers/Challenges/Listing/ChallengeRecommendedError/styles.scss b/src/containers/Challenges/Listing/ChallengeRecommendedError/styles.scss
new file mode 100644
index 0000000..a1efc68
--- /dev/null
+++ b/src/containers/Challenges/Listing/ChallengeRecommendedError/styles.scss
@@ -0,0 +1,24 @@
+@import "styles/variables";
+
+.challenge-recommended-error {
+ padding: 16px 24px;
+ min-height: 136px;
+ margin-bottom: 35px;
+ font-size: $font-size-sm;
+ line-height: 22px;
+ text-align: center;
+ background: $white;
+ border-radius: $border-radius-lg;
+
+ h1 {
+ padding: 15px 0 10px;
+ }
+
+ p {
+ margin-bottom: 20px;
+ }
+
+ strong {
+ font-weight: bold;
+ }
+}
diff --git a/src/containers/Challenges/Listing/index.jsx b/src/containers/Challenges/Listing/index.jsx
index 7c18893..eb4ec76 100644
--- a/src/containers/Challenges/Listing/index.jsx
+++ b/src/containers/Challenges/Listing/index.jsx
@@ -16,6 +16,7 @@ import "./styles.scss";
const Listing = ({
challenges,
+ search,
page,
perPage,
sortBy,
@@ -25,9 +26,10 @@ const Listing = ({
updateFilter,
bucket,
getChallenges,
+ challengeSortBys,
}) => {
const sortByOptions = utils.createDropdownOptions(
- Object.keys(constants.CHALLENGE_SORT_BY),
+ challengeSortBys,
utils.getSortByLabel(constants.CHALLENGE_SORT_BY, sortBy)
);
@@ -42,6 +44,7 @@ const Listing = ({
{
@@ -64,11 +67,11 @@ const Listing = ({
options={sortByOptions}
size="xs"
onChange={(newSortByOptions) => {
- const selectOption = utils.getSelectedDropdownOption(
+ const selectedOption = utils.getSelectedDropdownOption(
newSortByOptions
);
const filterChange = {
- sortBy: constants.CHALLENGE_SORT_BY[selectOption.label],
+ sortBy: constants.CHALLENGE_SORT_BY[selectedOption.label],
};
updateFilter(filterChange);
getChallenges(filterChange);
@@ -140,6 +143,7 @@ const Listing = ({
Listing.propTypes = {
challenges: PT.arrayOf(PT.shape()),
+ search: PT.string,
page: PT.number,
perPage: PT.number,
sortBy: PT.string,
@@ -149,6 +153,7 @@ Listing.propTypes = {
getChallenges: PT.func,
updateFilter: PT.func,
bucket: PT.string,
+ challengeSortBys: PT.arrayOf(PT.string),
};
export default Listing;
diff --git a/src/containers/Challenges/index.jsx b/src/containers/Challenges/index.jsx
index 9a890c0..9ff80e3 100644
--- a/src/containers/Challenges/index.jsx
+++ b/src/containers/Challenges/index.jsx
@@ -4,10 +4,17 @@ import { connect } from "react-redux";
import Listing from "./Listing";
import actions from "../../actions";
import ChallengeError from "./Listing/ChallengeError";
+import ChallengeRecommendedError from "./Listing/ChallengeRecommendedError";
+import IconListView from "../../assets/icons/list-view.svg";
+import IconCardView from "../../assets/icons/card-view.svg";
+import { ButtonIcon } from "../../components/Button";
+import * as constants from "../../constants";
+
import "./styles.scss";
const Challenges = ({
challenges,
+ search,
page,
perPage,
sortBy,
@@ -17,6 +24,8 @@ const Challenges = ({
getChallenges,
updateFilter,
bucket,
+ recommended,
+ loadingRecommendedChallengesError,
}) => {
const [initialized, setInitialized] = useState(false);
@@ -24,23 +33,56 @@ const Challenges = ({
getChallenges().finally(() => setInitialized(true));
}, []);
+ const BUCKET_OPEN_FOR_REGISTRATION = constants.FILTER_BUCKETS[1];
+ const isRecommended = recommended && bucket === BUCKET_OPEN_FOR_REGISTRATION;
+ const sortByValue = isRecommended
+ ? sortBy
+ : sortBy === constants.CHALLENGE_SORT_BY_RECOMMENDED
+ ? constants.CHALLENGE_SORT_BY_DEFAULT
+ : sortBy;
+ const sortByLabels = isRecommended
+ ? Object.keys(constants.CHALLENGE_SORT_BY)
+ : Object.keys(constants.CHALLENGE_SORT_BY).filter(
+ (label) => label !== constants.CHALLENGE_SORT_BY_RECOMMENDED_LABEL
+ );
+
+ const isNoRecommendedChallenges =
+ bucket === BUCKET_OPEN_FOR_REGISTRATION &&
+ recommended &&
+ loadingRecommendedChallengesError;
+
return (
-
CHALLENGES
- {challenges.length === 0 ? (
- initialized &&
- ) : (
+
+ CHALLENGES
+
+
+
+
+
+
+
+
+
+
+ {isNoRecommendedChallenges && initialized && (
+
+ )}
+ {challenges.length === 0 && initialized && }
+ {challenges.length > 0 && (
)}
@@ -49,6 +91,7 @@ const Challenges = ({
Challenges.propTypes = {
challenges: PT.arrayOf(PT.shape()),
+ search: PT.string,
page: PT.number,
perPage: PT.number,
sortBy: PT.string,
@@ -58,18 +101,24 @@ Challenges.propTypes = {
getChallenges: PT.func,
updateFilter: PT.func,
bucket: PT.string,
+ recommended: PT.bool,
+ loadingRecommendedChallengesError: PT.bool,
};
const mapStateToProps = (state) => ({
state: state,
+ search: state.filter.challenge.search,
page: state.filter.challenge.page,
perPage: state.filter.challenge.perPage,
sortBy: state.filter.challenge.sortBy,
total: state.challenges.total,
endDateStart: state.filter.challenge.endDateStart,
startDateEnd: state.filter.challenge.startDateEnd,
- challenges: state.challenges.challengesFiltered,
+ challenges: state.challenges.challenges,
bucket: state.filter.challenge.bucket,
+ recommended: state.filter.challenge.recommended,
+ loadingRecommendedChallengesError:
+ state.challenges.loadingRecommendedChallengesError,
});
const mapDispatchToProps = {
diff --git a/src/containers/Challenges/styles.scss b/src/containers/Challenges/styles.scss
index b43a724..046abe5 100644
--- a/src/containers/Challenges/styles.scss
+++ b/src/containers/Challenges/styles.scss
@@ -9,3 +9,11 @@
@include barlow-condensed-medium;
margin-bottom: 22px;
}
+
+.view-mode {
+ float: right;
+
+ > * {
+ margin: 0 2px;
+ }
+}
diff --git a/src/containers/Filter/ChallengeFilter/index.jsx b/src/containers/Filter/ChallengeFilter/index.jsx
index 0575912..7764915 100644
--- a/src/containers/Filter/ChallengeFilter/index.jsx
+++ b/src/containers/Filter/ChallengeFilter/index.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect, useRef } from "react";
import PT from "prop-types";
import _ from "lodash";
import RadioButton from "../../../components/RadioButton";
@@ -8,6 +8,7 @@ import Toggle from "../../../components/Toggle";
import Button from "../../../components/Button";
import TextInput from "../../../components/TextInput";
import * as utils from "../../../utils";
+import * as constants from "../../../constants";
import "./styles.scss";
@@ -18,6 +19,7 @@ const ChallengeFilter = ({
tags,
prizeFrom,
prizeTo,
+ recommended,
subCommunities,
challengeBuckets,
challengeTypes,
@@ -26,7 +28,10 @@ const ChallengeFilter = ({
challengeSubCommunities,
saveFilter,
clearFilter,
+ switchBucket,
+ openForRegistrationCount,
}) => {
+ const BUCKET_OPEN_FOR_REGISTRATION = constants.FILTER_BUCKETS[1];
const tagOptions = utils.createDropdownTermOptions(challengeTags);
const bucketOptions = utils.createRadioOptions(challengeBuckets, bucket);
@@ -39,26 +44,66 @@ const ChallengeFilter = ({
prizeFrom,
prizeTo,
subCommunities,
+ recommended,
})
);
utils.setSelectedDropdownTermOptions(tagOptions, filter.tags);
+ useEffect(() => {
+ const newFilter = _.cloneDeep({
+ bucket,
+ types,
+ tracks,
+ tags,
+ prizeFrom,
+ prizeTo,
+ subCommunities,
+ recommended,
+ });
+ setFilter(newFilter);
+ }, [
+ bucket,
+ types,
+ tracks,
+ tags,
+ prizeFrom,
+ prizeTo,
+ subCommunities,
+ recommended,
+ ]);
+
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+
+ const openForRegistrationElement = ref.current.children[0].children[1];
+ const badgeElement = utils.icon.createBadgeElement(
+ openForRegistrationElement,
+ `${openForRegistrationCount}`
+ );
+
+ return () => {
+ badgeElement.parentElement.removeChild(badgeElement);
+ };
+ }, [ref.current, openForRegistrationCount]);
+
return (
-
+
{
- const filterChange = {
- ...filter,
- bucket: utils.getSelectedRadioOption(newBucketOptions).label,
- };
- setFilter(filterChange);
- saveFilter(filterChange);
+ const selectedBucket = utils.getSelectedRadioOption(
+ newBucketOptions
+ ).label;
+ setFilter({ ...filter, bucket: selectedBucket });
+ switchBucket(selectedBucket);
}}
/>
-
@@ -115,14 +160,43 @@ const ChallengeFilter = ({
tags: selectedTagOptions.map((tagOption) => tagOption.label),
});
}}
+ size="xs"
/>
Prize Amount
-
+
+ {
+ setFilter({
+ ...filter,
+ prizeFrom: utils.parsePrizeAmountText(value),
+ });
+ }}
+ />
+ USD
+
-
+
+ {
+ setFilter({
+ ...filter,
+ prizeTo: utils.parsePrizeAmountText(value),
+ });
+ }}
+ />
+ USD
+
{challengeSubCommunities.length > 0 && (
@@ -145,12 +219,26 @@ const ChallengeFilter = ({
)}
-
-
-
-
- Recommended Challenges
-
+
+ {bucket === BUCKET_OPEN_FOR_REGISTRATION && (
+
+
+ {
+ setFilter({
+ ...filter,
+ recommended: checked,
+ sortBy: checked
+ ? constants.CHALLENGE_SORT_BY_RECOMMENDED
+ : constants.CHALLENGE_SORT_BY_DEFAULT,
+ });
+ }}
+ />
+
+ Recommended Challenges
+
+ )}
@@ -175,6 +263,8 @@ ChallengeFilter.propTypes = {
challengeSubCommunities: PT.arrayOf(PT.string),
saveFilter: PT.func,
clearFilter: PT.func,
+ switchBucket: PT.func,
+ openForRegistrationCount: PT.number,
};
export default ChallengeFilter;
diff --git a/src/containers/Filter/ChallengeFilter/styles.scss b/src/containers/Filter/ChallengeFilter/styles.scss
index 1af3174..7d1c7fc 100644
--- a/src/containers/Filter/ChallengeFilter/styles.scss
+++ b/src/containers/Filter/ChallengeFilter/styles.scss
@@ -17,7 +17,7 @@ $filter-padding-y: 3 * $base-unit;
margin-bottom: 18px;
> h3 {
- margin-bottom: 12px;
+ margin-bottom: 15px;
font-size: inherit;
line-height: 19px;
}
@@ -29,6 +29,18 @@ $filter-padding-y: 3 * $base-unit;
margin: $base-unit 0;
}
}
+
+ :global(.badge) {
+ display: inline-block;
+ margin-left: $base-unit;
+ padding: 0 5px;
+ font-weight: bold;
+ font-size: 11px;
+ line-height: 16px;
+ color: white;
+ background: red;
+ border-radius: 13px;
+ }
}
.challenge-types,
@@ -51,6 +63,7 @@ $filter-padding-y: 3 * $base-unit;
.skills {
margin-bottom: 34px;
+ max-width: 230px;
}
.prize {
@@ -74,6 +87,27 @@ $filter-padding-y: 3 * $base-unit;
margin-top: -12px;
}
+.input-group {
+ position: relative;
+ margin-top: -12px;
+
+ input {
+ padding-right: 36px !important;
+ }
+
+ .suffix {
+ @include roboto-medium;
+
+ position: absolute;
+ top: 22px;
+ right: 10px;
+ font-size: 13px;
+ line-height: 22px;
+ color: $tc-gray-30;
+ pointer-events: none;
+ }
+}
+
.recommended-challenges {
margin: 20px 0 25px;
font-size: $font-size-sm;
@@ -98,3 +132,4 @@ $filter-padding-y: 3 * $base-unit;
margin-left: 9px;
}
}
+
diff --git a/src/containers/Filter/index.jsx b/src/containers/Filter/index.jsx
index e4717f2..13fdf4d 100644
--- a/src/containers/Filter/index.jsx
+++ b/src/containers/Filter/index.jsx
@@ -11,6 +11,7 @@ const Filter = ({
tags,
prizeFrom,
prizeTo,
+ recommended,
subCommunities,
challengeBuckets,
challengeTypes,
@@ -21,6 +22,7 @@ const Filter = ({
getChallenges,
getTags,
getSubCommunities,
+ openForRegistrationCount,
}) => {
useEffect(() => {
getTags();
@@ -32,6 +34,10 @@ const Filter = ({
getChallenges(filter);
};
+ const onSwitchBucket = (bucket) => {
+ updateFilter({ bucket });
+ };
+
return (
{}}
+ switchBucket={onSwitchBucket}
+ openForRegistrationCount={openForRegistrationCount}
/>
);
};
@@ -59,6 +68,7 @@ Filter.propTypes = {
tags: PT.arrayOf(PT.string),
prizeFrom: PT.number,
prizeTo: PT.number,
+ recommended: PT.bool,
subCommunities: PT.arrayOf(PT.string),
challengeBuckets: PT.arrayOf(PT.string),
challengeTypes: PT.arrayOf(PT.string),
@@ -69,6 +79,7 @@ Filter.propTypes = {
getChallenges: PT.func,
getTags: PT.func,
getSubCommunities: PT.func,
+ openForRegistrationCount: PT.number,
};
const mapStateToProps = (state) => ({
@@ -79,12 +90,14 @@ const mapStateToProps = (state) => ({
tags: state.filter.challenge.tags,
prizeFrom: state.filter.challenge.prizeFrom,
prizeTo: state.filter.challenge.prizeTo,
+ recommended: state.filter.challenge.recommended,
subCommunities: state.filter.challenge.subCommunities,
challengeBuckets: state.lookup.buckets,
challengeTypes: state.lookup.types,
challengeTracks: state.lookup.tracks,
challengeTags: state.lookup.tags,
challengeSubCommunities: state.lookup.subCommunities,
+ openForRegistrationCount: state.challenges.openForRegistrationCount,
});
const mapDispatchToProps = {
diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js
index 827b06f..055d9aa 100644
--- a/src/reducers/challenges.js
+++ b/src/reducers/challenges.js
@@ -1,15 +1,26 @@
import { handleActions } from "redux-actions";
+import * as util from "../utils/challenge";
+import * as constants from "../constants";
const defaultState = {
loadingChallenges: false,
loadingChallengesError: null,
+ loadingRecommendedChallengesError: false,
challenges: [],
- challengesFiltered: [],
+ allActiveChallenges: [],
+ openForRegistrationChallenges: [],
+ closedChallenges: [],
+ openForRegistrationCount: 0,
total: 0,
};
function onGetChallengesInit(state) {
- return { ...state, loadingChallenges: true, loadingChallengesError: null };
+ return {
+ ...state,
+ loadingChallenges: true,
+ loadingChallengesError: null,
+ loadingRecommendedChallengesError: false,
+ };
}
function onGetChallengesDone(state, { payload }) {
@@ -17,8 +28,13 @@ function onGetChallengesDone(state, { payload }) {
...state,
loadingChallenges: false,
loadingChallengesError: null,
+ loadingRecommendedChallengesError:
+ payload.loadingRecommendedChallengesError,
challenges: payload.challenges,
- challengesFiltered: payload.challengesFiltered,
+ allActiveChallenges: payload.allActiveChallenges,
+ openForRegistrationChallenges: payload.openForRegistrationChallenges,
+ closedChallenges: payload.closedChallenges,
+ openForRegistrationCount: payload.openForRegistrationCount,
total: payload.total,
};
}
@@ -29,16 +45,55 @@ function onGetChallengesFailure(state, { payload }) {
loadingChallenges: false,
loadingChallengesError: payload,
challenges: [],
- challengesFiltered: [],
+ allActiveChallenges: [],
+ openForRegistrationChallenges: [],
+ closedChallenges: [],
+ openForRegistrationCount: 0,
total: 0,
};
}
+function onUpdateFilter(state, { payload }) {
+ const FILTER_BUCKETS = constants.FILTER_BUCKETS;
+ const BUCKET_ALL_ACTIVE_CHALLENGES = FILTER_BUCKETS[0];
+ const BUCKET_OPEN_FOR_REGISTRATION = FILTER_BUCKETS[1];
+ const BUCKET_CLOSED_CHALLENGES = FILTER_BUCKETS[2];
+ const filterChange = payload;
+ const {
+ allActiveChallenges,
+ openForRegistrationChallenges,
+ closedChallenges,
+ } = state;
+
+ let challenges;
+ let total;
+
+ if (util.isSwitchingBucket(filterChange)) {
+ switch (filterChange.bucket) {
+ case BUCKET_ALL_ACTIVE_CHALLENGES:
+ challenges = allActiveChallenges;
+ break;
+ case BUCKET_OPEN_FOR_REGISTRATION:
+ challenges = openForRegistrationChallenges;
+ break;
+ case BUCKET_CLOSED_CHALLENGES:
+ challenges = closedChallenges;
+ break;
+ }
+ total = challenges.meta.total;
+
+ return { ...state, challenges, total };
+ }
+
+ return { ...state };
+}
+
export default handleActions(
{
GET_CHALLENGE_INIT: onGetChallengesInit,
GET_CHALLENGES_DONE: onGetChallengesDone,
GET_CHALLENGES_FAILURE: onGetChallengesFailure,
+ UPDATE_FILTER: onUpdateFilter,
},
defaultState
);
diff --git a/src/reducers/filter.js b/src/reducers/filter.js
index 6346dce..073fef5 100644
--- a/src/reducers/filter.js
+++ b/src/reducers/filter.js
@@ -1,19 +1,20 @@
import { handleActions } from "redux-actions";
import * as constants from "../constants";
+import moment from "moment";
const defaultState = {
challenge: {
types: constants.FILTER_CHALLENGE_TYPES,
- tracks: constants.FILTER_CHALLENGE_TRACKS,
+ tracks: constants.FILTER_CHALLENGE_TRACKS.filter((track) => track !== "QA"),
search: "",
tags: [],
groups: [],
- startDateEnd: null,
- endDateStart: null,
+ endDateStart: moment().subtract(99, "year").toDate().toISOString(),
+ startDateEnd: moment().add(1, "year").toDate().toISOString(),
page: 1,
perPage: constants.PAGINATION_PER_PAGE[0],
sortBy: constants.CHALLENGE_SORT_BY["Most recent"],
- sortOrder: null,
+ sortOrder: constants.SORT_ORDER.ASC,
// ---
@@ -21,15 +22,21 @@ const defaultState = {
prizeFrom: 0,
prizeTo: 10000,
subCommunities: [],
+ recommended: false,
},
};
+function onRestoreFilter(state, { payload }) {
+ return { ...state, ...payload };
+}
+
function onUpdateFilter(state, { payload }) {
return { ...state, challenge: { ...state.challenge, ...payload } };
}
export default handleActions(
{
+ RESTORE_FILTER: onRestoreFilter,
UPDATE_FILTER: onUpdateFilter,
},
defaultState
diff --git a/src/root.component.js b/src/root.component.js
index a49494c..4eb8c1c 100644
--- a/src/root.component.js
+++ b/src/root.component.js
@@ -5,6 +5,8 @@ import { createHistory, LocationProvider } from "@reach/router";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
+import * as util from "./utils/session";
+import actions from "./actions";
// History for location provider
const history = createHistory(window);
@@ -13,6 +15,13 @@ export default function Root() {
useEffect(() => {
// when app starts it should set its side menu structure
setAppMenu("/earn", appMenu);
+
+ const unsubscribe = store.subscribe(() =>
+ util.persistFilter(util.selectFilter(store.getState()))
+ );
+ return () => {
+ unsubscribe();
+ };
}, []);
return (
diff --git a/src/services/challenges.js b/src/services/challenges.js
index 1889d9c..b93a5dd 100644
--- a/src/services/challenges.js
+++ b/src/services/challenges.js
@@ -11,6 +11,12 @@ async function getChallenges(filter) {
return api.get(`/challenges/${challengeQuery}`);
}
+async function getRecommendedChallenges(filter, handle) {
+ const challengeQuery = util.buildQueryString(filter);
+ return api.get(`/recommender-api/${handle}/${challengeQuery}`);
+}
+
export default {
getChallenges,
+ getRecommendedChallenges,
};
diff --git a/src/store.js b/src/store.js
index 0852395..4e2867f 100644
--- a/src/store.js
+++ b/src/store.js
@@ -4,6 +4,8 @@
import { createStore, compose, applyMiddleware } from "redux";
import { createPromise } from "redux-promise-middleware";
import root from "./reducers";
+import actions from "./actions";
+import * as util from "./utils/session";
const middlewares = [
createPromise({ promiseTypeSuffixes: ["INIT", "DONE", "FAILURE"] }),
@@ -15,4 +17,8 @@ if (process.env.APPMODE === "development") {
middlewares.push(logger);
}
-export default createStore(root, compose(applyMiddleware(...middlewares)));
+const store = createStore(root, compose(applyMiddleware(...middlewares)));
+
+store.dispatch(actions.filter.restoreFilter(util.restoreFilter()));
+
+export default store;
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index 433ec8b..fde2cc4 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -127,6 +127,9 @@ $font-size-sm: 14px;
$font-size-xs: 12px;
/// APP
+
+$screen-xxl: 1366px;
+
$base-unit: 5px;
$body-color: $tc-gray-90;
diff --git a/src/utils/challenge.js b/src/utils/challenge.js
index 158f9ea..dceacf3 100644
--- a/src/utils/challenge.js
+++ b/src/utils/challenge.js
@@ -34,7 +34,9 @@ export function createChallengeCriteria(filter) {
startDateEnd: filter.startDateEnd,
endDateStart: filter.endDateStart,
endDateEnd: filter.endDateEnd,
- sortBy: filter.sortBy,
+ sortBy: isValidCriteriaSortBy(filter.sortBy)
+ ? filter.sortBy
+ : constants.CHALLENGE_SORT_BY_DEFAULT,
sortOrder: filter.sortOrder,
groups: filter.groups,
};
@@ -44,18 +46,22 @@ export function createOpenForRegistrationChallengeCriteria() {
return {
status: "Active",
currentPhaseName: "Registration",
+ endDateStart: null,
+ startDateEnd: null,
};
}
-export function createActiveChallengeCriteria() {
+export function createAllActiveChallengeCriteria() {
return {
status: "Active",
currentPhaseName: "Submission",
registrationEndDateEnd: new Date().toISOString(),
+ endDateStart: null,
+ startDateEnd: null,
};
}
-export function createPastChallengeCriteria() {
+export function createClosedChallengeCriteria() {
return {
status: "Completed",
};
@@ -102,6 +108,19 @@ export function checkRequiredFilterAttributes(filter) {
return valid;
}
+export function isSwitchingBucket(filterChange) {
+ const keys = Object.keys(filterChange);
+ return keys.length === 1 && keys[0] === "bucket";
+}
+
+export function isDisplayingBucket(filter, bucket) {
+ return filter.bucket === bucket;
+}
+
+export function isValidCriteriaSortBy(sortBy) {
+ return ["updated", "overview.totalPrizes", "name"].includes(sortBy);
+}
+
/**
* Returns phase's end date.
* @param {Object} phase
diff --git a/src/utils/icon.js b/src/utils/icon.js
index 5cc61ff..759360e 100644
--- a/src/utils/icon.js
+++ b/src/utils/icon.js
@@ -119,7 +119,7 @@ function createTCOEventIcon (color) {
-
+
TCO
@@ -136,3 +136,13 @@ function createTCOEventIcon (color) {
);
}
+
+export function createBadgeElement(htmlElement, content) {
+ const badgeElement = document.createElement('span');
+
+ badgeElement.classList.add('badge');
+ badgeElement.textContent = content;
+ htmlElement.appendChild(badgeElement);
+
+ return badgeElement;
+}
diff --git a/src/utils/index.js b/src/utils/index.js
index 62d01b8..5df8191 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -122,14 +122,51 @@ export function formatMoneyValue(value) {
}
if (val.startsWith("-")) {
- val = `-$${val.slice(1)}`;
+ val = `-\uFF04${val.slice(1)}`;
} else {
- val = `$${val}`;
+ val = `\uFF04${val}`;
}
return val;
}
+/**
+ * Format a number value into the integer text of amount.
+ * Ex: 0 -> 0, greater than 10000 -> 10,000+
+ */
+export function formatPrizeAmount(value) {
+ let val = value || 0;
+ let greaterThan10000 = val >= 10000;
+
+ val = val.toLocaleString("en-US");
+
+ const i = val.indexOf(".");
+ if (i !== -1) {
+ val = val.slice(0, i);
+ }
+
+ val = greaterThan10000 ? "10,000+" : val;
+
+ return val;
+}
+
+export function parsePrizeAmountText(s) {
+ let val = s;
+ if (val.endsWith("+")) {
+ val = val.slice(0, val.length - 1);
+ }
+
+ const i = val.indexOf(".");
+ if (i !== -1) {
+ val = val.slice(0, i);
+ }
+
+ val = val.replace("/,/g", "");
+ val = parseInt(val);
+
+ return isNaN(val) ? 0 : val;
+}
+
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
diff --git a/src/utils/menu.js b/src/utils/menu.js
index b060439..b388ee6 100644
--- a/src/utils/menu.js
+++ b/src/utils/menu.js
@@ -9,7 +9,7 @@ export class MenuSelection {
}
travel(root) {
- Object.keys(root).forEach((key) => {
+ this.getMenuItems(root).forEach((key) => {
if (_.isObject(root[key])) {
root[key].expanded = false;
root[key].branch = true;
@@ -24,35 +24,37 @@ export class MenuSelection {
});
}
+ getMenuItems(menu) {
+ return Object.keys(_.omit(menu, "expanded", "active", "branch", "leaf"));
+ }
+
select(name) {
let found = false;
const selectInternal = (root) => {
- Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
- (key) => {
- if (found) {
- return;
- }
+ this.getMenuItems(root).forEach((key) => {
+ if (found) {
+ return;
+ }
- if (key !== name) {
- if (root[key].branch) {
- selectInternal(root[key]);
- } else {
- root[key].active = false;
- }
+ if (key !== name) {
+ if (root[key].branch) {
+ selectInternal(root[key]);
} else {
- if (root[key].leaf) {
- root[key].active = true;
- this.selectedMenuItem = name;
- } else {
- root[key].expanded = !root[key].expanded;
- }
-
- found = true;
- this.emitSelectionEvent();
+ root[key].active = false;
}
+ } else {
+ if (root[key].leaf) {
+ root[key].active = true;
+ this.selectedMenuItem = name;
+ } else {
+ root[key].expanded = !root[key].expanded;
+ }
+
+ found = true;
+ this.emitSelectionEvent();
}
- );
+ });
};
selectInternal(this.menu);
@@ -62,17 +64,15 @@ export class MenuSelection {
let leaf = false;
const isLeafInternal = (root) => {
- Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
- (key) => {
- if (key !== name) {
- if (root[key].branch) {
- isLeafInternal(root[key]);
- }
- } else if (root[key].leaf) {
- leaf = true;
+ this.getMenuItems(root).forEach((key) => {
+ if (key !== name) {
+ if (root[key].branch) {
+ isLeafInternal(root[key]);
}
+ } else if (root[key].leaf) {
+ leaf = true;
}
- );
+ });
};
isLeafInternal(this.menu);
@@ -85,20 +85,18 @@ export class MenuSelection {
}
isExpanded(name) {
- let expanded = false;
+ let expanded;
const isExpandedInternal = (root) => {
- Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
- (key) => {
- if (key !== name) {
- if (root[key].branch) {
- isExpandedInternal(root[key]);
- }
- } else if (root[key].branch) {
- expanded = root[key].expanded;
+ this.getMenuItems(root).forEach((key) => {
+ if (key !== name) {
+ if (root[key].branch) {
+ isExpandedInternal(root[key]);
}
+ } else if (root[key].branch) {
+ expanded = root[key].expanded;
}
- );
+ });
};
isExpandedInternal(this.menu);
@@ -123,20 +121,18 @@ export class MenuSelection {
}
const isActiveInternal = (root) => {
- Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
- (key) => {
- if (key !== this.selectedMenuItem) {
- if (root[key].branch) {
- stack.push(key);
- isActiveInternal(root[key]);
- stack.pop(key);
- }
- } else {
+ this.getMenuItems(root).forEach((key) => {
+ if (key !== this.selectedMenuItem) {
+ if (root[key].branch) {
stack.push(key);
- path = [...stack.arr];
+ isActiveInternal(root[key]);
+ stack.pop(key);
}
+ } else {
+ stack.push(key);
+ path = [...stack.arr];
}
- );
+ });
};
isActiveInternal(this.menu);
diff --git a/src/utils/pagination.js b/src/utils/pagination.js
index 8f0adf6..292d7fe 100644
--- a/src/utils/pagination.js
+++ b/src/utils/pagination.js
@@ -10,11 +10,11 @@ export function pageIndexToPage(pageIndex) {
* @param {any} response Web APIs Response
* @return {Object} pagination data
*/
-export function getResponseHeaders(reponse) {
+export function getResponseHeaders(response) {
return {
- page: +(reponse.headers.get("X-Page") || 0),
- perPage: +(reponse.headers.get("X-Per-Page") || 0),
- total: +(reponse.headers.get("X-Total") || 0),
- totalPages: +(reponse.headers.get("X-Total-Pages") || 0),
+ page: +(response.headers.get("X-Page") || 0),
+ perPage: +(response.headers.get("X-Per-Page") || 0),
+ total: +(response.headers.get("X-Total") || 0),
+ totalPages: +(response.headers.get("X-Total-Pages") || 0),
};
}
diff --git a/src/utils/session.js b/src/utils/session.js
new file mode 100644
index 0000000..ef396f6
--- /dev/null
+++ b/src/utils/session.js
@@ -0,0 +1,29 @@
+function selectFilter(state) {
+ return state.filter;
+}
+
+let currentFilterValue;
+function persistFilter(filter) {
+ let previousFilterValue = currentFilterValue;
+ currentFilterValue = filter;
+
+ if (previousFilterValue !== currentFilterValue) {
+ try {
+ sessionStorage.setItem("filter", JSON.stringify(filter));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+function restoreFilter() {
+ let filter;
+ try {
+ filter = JSON.parse(sessionStorage.getItem("filter"));
+ } catch (e) {
+ filter = {};
+ }
+ return filter;
+}
+
+export { selectFilter, persistFilter, restoreFilter };
diff --git a/src/utils/tag.js b/src/utils/tag.js
index 39bea25..2b4418a 100644
--- a/src/utils/tag.js
+++ b/src/utils/tag.js
@@ -6,7 +6,7 @@ export function calculateNumberOfVisibleTags(tags) {
let n = tags.length;
if (tagsString.length > MAX_LEN) {
let ss = "";
- for (n = 0; n < tags.length && ss.length < 20; n += 1) {
+ for (n = 0; n < tags.length && ss.length < MAX_LEN; n += 1) {
ss = ss.concat(tags[n]);
}
}
diff --git a/src/utils/url.js b/src/utils/url.js
index bffc6d1..80fb7d3 100644
--- a/src/utils/url.js
+++ b/src/utils/url.js
@@ -8,7 +8,7 @@ import qs from "qs";
* `{ p: undefined }` => ""
* `{ p: value }` => "p=value"
* `{ p: [] }` => ""
- * `{ p: ['Challenge', 'First2Finish', 'Task'] } => "p[]=Challenge&p[]=First2Finish&p[]=Taks`
+ * `{ p: ['Challenge', 'First2Finish', 'Task'] } => "p[]=Challenge&p[]=First2Finish&p[]=Task`
* `{ p: ['Design', 'Development', 'Data Science', 'Quality Assurance'] }` => "p[]=Design&p[]=Development&p=Data%20Science&p[]=Quality%20Assurance"
* `{ p: { Des: true, Dev: true, DS: false, QA: false } }` => "p[Des]=true&p[Dev]=true&p[DS]=false&p[QA]=false"
*