)
@@ -70,7 +71,16 @@ export default function Tags({
const renderTags = () => {
const nonVerified = tags.filter(tag => !verifiedTags.includes(tag));
- const allTags = _.union(verifiedTags, nonVerified);
+ const allTags = [
+ ..._.union(verifiedTags, nonVerified).map(tag => ({
+ type: 'tag',
+ value: tag,
+ })),
+ ...skills.map(skill => ({
+ type: 'skill',
+ value: skill,
+ })),
+ ];
if (allTags.length) {
let display = [...new Set(allTags)];
@@ -80,13 +90,16 @@ export default function Tags({
if (allTags.length > VISIBLE_TAGS && !isExpanded) {
const expandItem = `+${display.length - VISIBLE_TAGS}`;
display = allTags.slice(0, VISIBLE_TAGS);
- display.push(expandItem);
+ display.push({
+ type: 'tag',
+ value: expandItem,
+ });
}
return display.map((item, index) => {
- if (item) {
- if ((recommended && index < verifiedTags.length) || item[0] === '+') {
+ if (item.value) {
+ if ((recommended && index < verifiedTags.length) || item.value[0] === '+') {
return (
- item[0] === '+' ? (
+ item.value[0] === '+' ? (
@@ -118,14 +133,14 @@ export default function Tags({
}
return (
-
+
onClick(item.trim())}
- key={item}
+ onClick={() => onClick(item.value.trim())}
+ key={`${item.type}_${item.value}`}
role="button"
- to={tagRedirectLink(item)}
+ to={tagRedirectLink(item.value)}
>
- {item}
+ {item.value}
);
@@ -148,6 +163,7 @@ export default function Tags({
Tags.defaultProps = {
onTechTagClicked: _.noop,
tags: [],
+ skills: [],
isExpanded: false,
expand: null,
challengesUrl: null,
@@ -159,6 +175,7 @@ Tags.defaultProps = {
Tags.propTypes = {
onTechTagClicked: PT.func,
tags: PT.arrayOf(PT.string),
+ skills: PT.arrayOf(PT.string),
isExpanded: PT.bool,
expand: PT.func,
challengesUrl: PT.string,
diff --git a/src/shared/components/challenge-listing/style.scss b/src/shared/components/challenge-listing/style.scss
index 518bc5e05e..f7ac0cb769 100644
--- a/src/shared/components/challenge-listing/style.scss
+++ b/src/shared/components/challenge-listing/style.scss
@@ -1,3 +1,5 @@
+/* stylelint-disable no-descending-specificity */
+
@import '~styles/mixins';
$challenge-space-5: $base-unit;
$challenge-space-10: $base-unit * 2;
@@ -101,27 +103,41 @@ $challenge-radius-4: $corner-radius * 2;
}
}
+.recommended-plus-tag {
+ margin-left: 3px;
+ display: inline-block;
+ background-color: $tc-white;
+
+ button:hover {
+ background-color: #d4d4d4 !important;
+ }
+}
+
.tag {
button {
border-radius: 2px;
max-width: 400px;
font-size: 11px;
- color: $tco-black;
+ color: $color-black-80;
font-weight: 500;
+ background-color: white !important;
+ border: 1px solid $color-black-60;
&:hover {
background-color: #d4d4d4 !important;
}
}
-}
-.recommended-plus-tag {
- margin-left: 3px;
- display: inline-block;
- background-color: $tc-white;
+ &.skill {
+ button {
+ color: $color-black-100;
+ background-color: $color-black-10 !important;
+ border: none;
- button:hover {
- background-color: #d4d4d4 !important;
+ &:hover {
+ background-color: #d4d4d4 !important;
+ }
+ }
}
}
@@ -156,8 +172,25 @@ $challenge-radius-4: $corner-radius * 2;
margin-left: 3px;
display: inline-block;
- & > button:hover {
- background-color: #d4d4d4 !important;
+ & > button {
+ background-color: white !important;
+ border: 1px solid $color-black-60;
+
+ &:hover {
+ background-color: #d4d4d4 !important;
+ }
+ }
+
+ &.skill {
+ & > button {
+ color: $color-black-100;
+ background-color: $color-black-10 !important;
+ border: none;
+
+ &:hover {
+ background-color: #d4d4d4 !important;
+ }
+ }
}
}
diff --git a/src/shared/containers/Dashboard/ChallengesFeed.jsx b/src/shared/containers/Dashboard/ChallengesFeed.jsx
index 5dbbfd7e56..5e9ed2aef5 100644
--- a/src/shared/containers/Dashboard/ChallengesFeed.jsx
+++ b/src/shared/containers/Dashboard/ChallengesFeed.jsx
@@ -2,6 +2,7 @@
* ChallengesFeed component
*/
import React from 'react';
+import _ from 'lodash';
import PT from 'prop-types';
import ChallengesFeed from 'components/Dashboard/Challenges';
import { connect } from 'react-redux';
@@ -9,28 +10,57 @@ import actions from '../../actions/dashboard';
class ChallengesFeedContainer extends React.Component {
componentDidMount() {
- const { getChallenges, challenges, itemCount } = this.props;
+ const {
+ getChallenges, challenges, itemCount, tags,
+ includeAllTags, projectId, excludeTags, title, tracks,
+ } = this.props;
if (!challenges || challenges.length === 0) {
- getChallenges({
- page: 1,
- perPage: itemCount,
- types: ['CH', 'F2F', 'MM'],
- tracks: ['DES', 'DEV', 'DEV', 'DS', 'QA'],
- status: 'Active',
- sortBy: 'updated',
- sortOrder: 'desc',
- isLightweight: true,
- currentPhaseName: 'Registration',
- });
+ getChallenges(
+ title,
+ _.omitBy({
+ page: 1,
+ perPage: excludeTags && excludeTags.length ? undefined : itemCount,
+ types: ['CH', 'F2F', 'MM'],
+ tracks,
+ status: 'Active',
+ sortBy: 'updated',
+ sortOrder: 'desc',
+ isLightweight: true,
+ currentPhaseName: 'Registration',
+ tags: tags && tags.length ? tags : undefined,
+ includeAllTags: !!includeAllTags || undefined,
+ projectId: projectId || undefined,
+ }, _.isUndefined),
+ );
}
}
render() {
- const { challenges, theme, loading } = this.props;
+ const {
+ theme, loading, excludeTags, itemCount, title, challengeListingQuery,
+ } = this.props;
+ let { challenges } = this.props;
+
+ // this is a workaround for excluding challenges by tags
+ // there is no API support for this, so we have to do it manually
+ // in taht case we load more challenges, not limited to itemCount and filter out by tags
+ // default value for perPage is 20 when not specified
+ if (excludeTags && excludeTags.length) {
+ // filter out by excluded tags
+ challenges = challenges.filter(c => !c.tags.some(t => excludeTags.includes(t)));
+ // limit to itemCount
+ challenges = challenges.slice(0, itemCount);
+ }
return (
-
+
);
}
}
@@ -40,6 +70,13 @@ ChallengesFeedContainer.defaultProps = {
challenges: [],
loading: true,
theme: 'light',
+ tags: [],
+ includeAllTags: false,
+ projectId: null,
+ excludeTags: [],
+ title: 'CHALLENGES',
+ challengeListingQuery: undefined,
+ tracks: ['DES', 'DEV', 'DS', 'QA'],
};
ChallengesFeedContainer.propTypes = {
@@ -48,19 +85,35 @@ ChallengesFeedContainer.propTypes = {
getChallenges: PT.func.isRequired,
loading: PT.bool,
theme: PT.oneOf(['dark', 'light']),
+ tags: PT.arrayOf(PT.string),
+ includeAllTags: PT.bool,
+ projectId: PT.number,
+ excludeTags: PT.arrayOf(PT.string),
+ title: PT.string,
+ challengeListingQuery: PT.shape(),
+ tracks: PT.arrayOf(PT.string),
};
-const mapStateToProps = state => ({
- challenges: state.dashboard.challenges,
- loading: state.dashboard.loading,
-});
+function mapStateToProps(state, ownProps) {
+ const { dashboard } = state;
+ const id = ownProps.title || 'CHALLENGES';
+
+ if (dashboard[id]) {
+ return {
+ challenges: dashboard[id].challenges,
+ loading: dashboard[id].loading,
+ };
+ }
+
+ return state;
+}
const mapDispatchToProps = dispatch => ({
- getChallenges: (query) => {
+ getChallenges: (title, query) => {
const a = actions.dashboard;
- dispatch(a.fetchChallengesInit());
- dispatch(a.fetchChallengesDone(query));
+ dispatch(a.fetchChallengesInit(title));
+ dispatch(a.fetchChallengesDone(title, query));
},
});
diff --git a/src/shared/containers/Dashboard/index.jsx b/src/shared/containers/Dashboard/index.jsx
index 1997e25331..085d98a129 100644
--- a/src/shared/containers/Dashboard/index.jsx
+++ b/src/shared/containers/Dashboard/index.jsx
@@ -24,11 +24,16 @@ import darkTheme from './themes/dark.scss';
const THEMES = {
dark: darkTheme,
};
+const { INNOVATION_CHALLENGES_TAG } = config;
function SlashTCContainer(props) {
const theme = THEMES.dark; // for v1 only dark theme
const isTabletOrMobile = useMediaQuery({ maxWidth: 768 });
const title = 'Home | Topcoder';
+ const challengeListingQuery = {
+ search: INNOVATION_CHALLENGES_TAG,
+ isInnovationChallenge: true,
+ };
useEffect(() => {
if (props.tokenV3 && !isTokenExpired(props.tokenV3)) return;
@@ -49,7 +54,15 @@ function SlashTCContainer(props) {
-
+
+
@@ -70,7 +83,15 @@ function SlashTCContainer(props) {
{/* Center column */}
-
+
+
diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx
index 28df87fab0..6d146e1338 100644
--- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx
+++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx
@@ -253,7 +253,12 @@ class RecruitCRMJobsContainer extends React.Component {
-
+
diff --git a/src/shared/reducers/dashboard.js b/src/shared/reducers/dashboard.js
index c0fe816909..d04254c0f3 100644
--- a/src/shared/reducers/dashboard.js
+++ b/src/shared/reducers/dashboard.js
@@ -5,6 +5,17 @@
import actions from 'actions/dashboard';
import { redux } from 'topcoder-react-utils';
+function onInit(state, { payload }) {
+ return {
+ ...state,
+ [payload]: {
+ details: null,
+ failed: false,
+ loading: true,
+ },
+ };
+}
+
/**
* Handles done actions.
* @param {Object} state Previous state.
@@ -13,9 +24,11 @@ import { redux } from 'topcoder-react-utils';
function onDone(state, action) {
return {
...state,
- challenges: action.error ? null : action.payload,
- failed: action.error,
- loading: false,
+ [action.payload.title]: {
+ challenges: action.error ? null : action.payload.challenges,
+ failed: action.error,
+ loading: false,
+ },
};
}
@@ -26,14 +39,7 @@ function onDone(state, action) {
*/
function create(initialState) {
return redux.handleActions({
- [actions.dashboard.fetchChallengesInit](state) {
- return {
- ...state,
- details: null,
- failed: false,
- loading: true,
- };
- },
+ [actions.dashboard.fetchChallengesInit]: onInit,
[actions.dashboard.fetchChallengesDone]: onDone,
}, initialState || {});
}
diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx
index 0cdc494442..4ad5f597b5 100644
--- a/src/shared/utils/challenge-detail/helper.jsx
+++ b/src/shared/utils/challenge-detail/helper.jsx
@@ -62,15 +62,17 @@ export function getTimeLeft(
fullText = false,
) {
const STALLED_TIME_LEFT_MSG = 'Challenge is currently on hold';
- const REGISTRATION_PHASE_MESSAGE = 'Open For Registration';
+ const REGISTRATION_PHASE_MESSAGE = 'Register';
const FF_TIME_LEFT_MSG = 'Winner is working on fixes';
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
- if (!phase) return { late: false, text: STALLED_TIME_LEFT_MSG };
- if (isRegistrationPhase(phase)) return { late: false, text: REGISTRATION_PHASE_MESSAGE };
+ if (!phase) return { late: false, text: STALLED_TIME_LEFT_MSG, canTrimText: false };
+ if (isRegistrationPhase(phase)) {
+ return { late: false, text: REGISTRATION_PHASE_MESSAGE, canTrimText: false };
+ }
if (phase.phaseType === 'Final Fix') {
- return { late: false, text: FF_TIME_LEFT_MSG };
+ return { late: false, text: FF_TIME_LEFT_MSG, canTrimText: false };
}
let time = moment(phaseEndDate(phase)).diff();
@@ -84,7 +86,7 @@ export function getTimeLeft(
time = moment.duration(time).format(format);
time = late ? `${time} Past Due` : `${time} ${toGoText}`;
- return { late, text: time };
+ return { late, text: time, canTrimText: true };
}
/**