diff --git a/.circleci/config.yml b/.circleci/config.yml index 135811ac35..b7319d204c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -355,13 +355,14 @@ workflows: - remove_submission_review - standardised_skills - TSJR-275 + - skills_updates # This is alternate dev env for parallel testing - "build-test": context : org-global filters: branches: only: - - PROD-4251 + - IC-13 # This is alternate dev env for parallel testing - "build-qa": context : org-global diff --git a/config/backup-default.js b/config/backup-default.js index ab48e02fea..b5d276dfd6 100644 --- a/config/backup-default.js +++ b/config/backup-default.js @@ -453,7 +453,7 @@ module.exports = { GUIKIT: { DEBOUNCE_ON_CHANGE_TIME: 150, }, - ENABLE_RECOMMENDER: true, + ENABLE_RECOMMENDER: false, OPTIMIZELY: { SDK_KEY: '7V4CJhurXT3Y3bnzv1hv1', }, diff --git a/config/default.js b/config/default.js index c43129ceb7..571a5d4645 100644 --- a/config/default.js +++ b/config/default.js @@ -454,7 +454,7 @@ module.exports = { GUIKIT: { DEBOUNCE_ON_CHANGE_TIME: 150, }, - ENABLE_RECOMMENDER: true, + ENABLE_RECOMMENDER: false, OPTIMIZELY: { SDK_KEY: '7V4CJhurXT3Y3bnzv1hv1', }, @@ -476,4 +476,5 @@ module.exports = { MEMBER_PROFILE_REDIRECT_URL: 'https://profiles.topcoder-dev.com', MEMBER_SEARCH_REDIRECT_URL: 'https://talent-search.topcoder-dev.com', ACCOUNT_SETTINGS_REDIRECT_URL: 'https://account-settings.topcoder-dev.com', + INNOVATION_CHALLENGES_TAG: 'Innovation Challenge', }; diff --git a/config/production.js b/config/production.js index e62eb9eec6..2d9a619dc4 100644 --- a/config/production.js +++ b/config/production.js @@ -229,7 +229,7 @@ module.exports = { TC_EDU_ARTICLES_PATH: '/articles', TC_EDU_SEARCH_PATH: '/search', TC_EDU_SEARCH_BAR_MAX_RESULTS_EACH_GROUP: 3, - ENABLE_RECOMMENDER: true, + ENABLE_RECOMMENDER: false, PLATFORM_SITE_URL: 'https://platform.topcoder.com', PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder.com', DICE_VERIFY_URL: 'https://accounts-auth0.topcoder.com', diff --git a/src/shared/actions/dashboard.js b/src/shared/actions/dashboard.js index ce468c8e19..2810b4b042 100644 --- a/src/shared/actions/dashboard.js +++ b/src/shared/actions/dashboard.js @@ -1,16 +1,24 @@ -import _ from 'lodash'; import { createActions } from 'redux-actions'; import { getService } from '../services/dashboard'; const service = getService(); -function fetchChallenges(query) { - return service.getChallenges(query); +function fetchChallengesInit(title) { + return title; +} + +async function fetchChallenges(title, query) { + const challenges = await service.getChallenges(query); + + return { + challenges, + title, + }; } export default createActions({ DASHBOARD: { - FETCH_CHALLENGES_INIT: _.noop, + FETCH_CHALLENGES_INIT: fetchChallengesInit, FETCH_CHALLENGES_DONE: fetchChallenges, }, }); diff --git a/src/shared/components/Dashboard/Challenges/index.jsx b/src/shared/components/Dashboard/Challenges/index.jsx index bc8096a786..4f2829228b 100644 --- a/src/shared/components/Dashboard/Challenges/index.jsx +++ b/src/shared/components/Dashboard/Challenges/index.jsx @@ -2,6 +2,7 @@ import _ from 'lodash'; import LoadingIndicator from 'components/LoadingIndicator'; import PT from 'prop-types'; import React from 'react'; +import qs from 'qs'; import { config } from 'topcoder-react-utils'; @@ -11,14 +12,16 @@ export default function ChallengesFeed({ challenges, loading, theme, + title, + challengeListingQuery, }) { - return ( + return challenges && challenges.length ? (
- CHALLENGES + {title} View all challenges @@ -26,7 +29,7 @@ export default function ChallengesFeed({
{loading ?
- : challenges.map(challenge => ( + : (challenges || []).map(challenge => (
- ); + ) : null; } ChallengesFeed.defaultProps = { challenges: [], theme: 'light', + title: 'CHALLENGES', + challengeListingQuery: undefined, }; ChallengesFeed.propTypes = { challenges: PT.arrayOf(PT.shape()), loading: PT.bool.isRequired, theme: PT.oneOf(['dark', 'light']), + title: PT.string, + challengeListingQuery: PT.shape(), }; diff --git a/src/shared/components/GUIKit/DropdownSingleSkills/index.jsx b/src/shared/components/GUIKit/DropdownSingleSkills/index.jsx new file mode 100644 index 0000000000..2c43181844 --- /dev/null +++ b/src/shared/components/GUIKit/DropdownSingleSkills/index.jsx @@ -0,0 +1,134 @@ +/* eslint-disable jsx-a11y/label-has-for */ +/** + * Dropdown terms component. + */ +import React, { + useRef, + useEffect, +} from 'react'; +import PT from 'prop-types'; +import { AsyncCreatable } from 'react-select'; +import './style.scss'; + +function DropdownSingleSkills({ + terms, + placeholder, + label, + required, + onChange, + errorMsg, + cacheOptions, + loadOptions, + createText, +}) { + const containerRef = useRef(null); + useEffect(() => { + const selectInput = containerRef.current.getElementsByClassName('Select-input'); + if (selectInput && selectInput.length) { + const inputField = selectInput[0].getElementsByTagName('input'); + inputField[0].style.border = 'none'; + inputField[0].style.boxShadow = 'none'; + selectInput[0].style.borderTop = 'none'; + } + }, [terms]); + + const CustomReactSelectRow = React.forwardRef(({ + className, + option, + children, + onSelect, + }, ref) => (children ? ( + { + event.preventDefault(); + event.stopPropagation(); + onSelect(option, event); + }} + title={option.title} + tabIndex={-1} + > + {children} + + ) : null)); + + CustomReactSelectRow.defaultProps = { + children: null, + className: '', + onSelect: () => {}, + }; + + CustomReactSelectRow.propTypes = { + children: PT.node, + className: PT.string, + onSelect: PT.func, + option: PT.object.isRequired, + }; + + return ( +
+
+ { + onChange(value ? (value.value || '') : ''); + }} + defaultValue={terms ? { + value: terms, + label: terms, + } : null} + promptTextCreator={value => `${createText} "${value}"`} + placeholder={`${placeholder}${placeholder && required ? ' *' : ''}`} + cacheOptions={cacheOptions} + loadOptions={loadOptions} + /> +
+ {label ? ( + + {label} + {required ?  * : null} + + ) : null} + {errorMsg ? {errorMsg} : null} +
+ ); +} + +DropdownSingleSkills.defaultProps = { + terms: '', + placeholder: '', + label: '', + required: false, + cacheOptions: false, + onChange: () => {}, + errorMsg: '', + createText: 'Select', + loadOptions: undefined, +}; + +DropdownSingleSkills.propTypes = { + terms: PT.string, + placeholder: PT.string, + label: PT.string, + required: PT.bool, + cacheOptions: PT.bool, + onChange: PT.func, + errorMsg: PT.string, + createText: PT.string, + loadOptions: PT.func, +}; + +export default DropdownSingleSkills; diff --git a/src/shared/components/GUIKit/DropdownSingleSkills/style.scss b/src/shared/components/GUIKit/DropdownSingleSkills/style.scss new file mode 100644 index 0000000000..e1433342b8 --- /dev/null +++ b/src/shared/components/GUIKit/DropdownSingleSkills/style.scss @@ -0,0 +1,71 @@ +@import '../Dropdown/style.scss'; + +.label { + z-index: 6; +} + +.relative { + position: relative; +} + +.errorMessage, +.haveValue, +.haveError { + font-family: Roboto, sans-serif; +} + +.container { + font-family: Roboto, sans-serif; + + :global { + .Select-control { + min-height: 52px; + height: auto; + display: flex !important; + overflow: visible !important; + } + + .Select-clear-zone, + .Select-clear { + font-size: 21px; + line-height: 40px; + + &:hover { + color: $dashboard-teal; + } + } + + .Select-value { + font-size: 14px !important; + padding-right: 42px !important; + overflow: hidden; + + .Select-value-label { + font-size: 14px; + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .Select-input { + input { + font-size: 14px; + + &::-webkit-input-placeholder { + /* Edge */ + color: $gui-kit-gray-30; + } + + &:-ms-input-placeholder { + /* Internet Explorer 10-11 */ + color: $gui-kit-gray-30; + } + + &::placeholder { + color: $gui-kit-gray-30; + } + } + } + } +} diff --git a/src/shared/components/GUIKit/JobListCard/index.jsx b/src/shared/components/GUIKit/JobListCard/index.jsx index 270dda12de..26510b0c10 100644 --- a/src/shared/components/GUIKit/JobListCard/index.jsx +++ b/src/shared/components/GUIKit/JobListCard/index.jsx @@ -54,7 +54,7 @@ function JobListCard({ {job.country}
- ${job.min_annual_salary} - {job.max_annual_salary} (USD) / {getSalaryType(job.salary_type)} + ${job.min_annual_salary} - {job.max_annual_salary} (USD) / {getSalaryType(job.salary_type || {})}
{/^\d+$/.test(duration) ? `${duration} Weeks` : duration} diff --git a/src/shared/components/GUIKit/SearchCombo/index.jsx b/src/shared/components/GUIKit/SearchCombo/index.jsx index b180c1deb0..3be805bf38 100644 --- a/src/shared/components/GUIKit/SearchCombo/index.jsx +++ b/src/shared/components/GUIKit/SearchCombo/index.jsx @@ -1,43 +1,52 @@ /** * SearchCombo component. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useMemo } from 'react'; +import DropdownSingleSkills from 'components/GUIKit/DropdownSingleSkills'; import PT from 'prop-types'; +import _ from 'lodash'; import './style.scss'; -import IconClearSearch from 'assets/images/icon-clear-search.svg'; +import { getService } from 'services/skills'; function SearchCombo({ term, placeholder, - btnText, onSearch, + auth, }) { - const [inputVal, setVal] = useState(term); - useEffect(() => setVal(term), [term]); - const clearSearch = () => { - setVal(''); - onSearch(''); - }; - const onKeyDown = (e) => { - if (e.which === 13) { - onSearch(inputVal); + const [skills, setSkills] = useState(term); + + const fetchSkills = useMemo(() => _.debounce((inputValue, callback) => { + if (!inputValue) { + callback(null); + } else { + getService(auth.tokenV3).getSkills(inputValue).then( + (response) => { + const suggestedOptions = (response || []).map(skillItem => ({ + label: skillItem.name, + value: skillItem.name, + })); + return callback(null, { + options: suggestedOptions, + }); + }, + ).catch(() => callback(null)); } - }; + }, 150), [auth.tokenV3]); return (
-
- { - !inputVal ? {placeholder} : null - } - setVal(event.target.value)} onKeyDown={onKeyDown} /> - { - inputVal ? : null - } -
- + { + setSkills(newSkill); + onSearch(newSkill); + }} + cacheOptions + loadOptions={fetchSkills} + createText="Search" + />
); } @@ -45,14 +54,14 @@ function SearchCombo({ SearchCombo.defaultProps = { term: '', placeholder: '', - btnText: 'SEARCH', + auth: {}, }; SearchCombo.propTypes = { term: PT.string, placeholder: PT.string, - btnText: PT.string, onSearch: PT.func.isRequired, + auth: PT.object, }; export default SearchCombo; diff --git a/src/shared/components/GUIKit/SearchCombo/style.scss b/src/shared/components/GUIKit/SearchCombo/style.scss index ce03a3219c..db963a0b17 100644 --- a/src/shared/components/GUIKit/SearchCombo/style.scss +++ b/src/shared/components/GUIKit/SearchCombo/style.scss @@ -3,75 +3,20 @@ .container { display: flex; align-items: center; + gap: 10px; width: 100%; - .input-wrap { - width: 100%; - position: relative; - margin-right: 10px; + :global { + .dropdownContainer { + padding-top: 0; - input.input { - background: transparent; - border: 1px solid #aaa; - border-radius: 6px; - height: 39px; - margin: 0; - position: relative; - z-index: 1; - } - - .search-placeholder { - color: #aaa; - font-size: 14px; - font-family: Roboto, sans-serif; - line-height: 22px; - text-transform: none; - position: absolute; - z-index: 0; - top: 8px; - left: 15px; - overflow: hidden; - white-space: nowrap; - max-width: calc(100% - 20px); - } - - .clear-search { - position: absolute; - top: calc(50% - 5px); - right: 15px; - cursor: pointer; - z-index: 2; - } - } - - button.primary-green-md { - outline: none; - display: flex; - align-items: center; - - @include primary-green; - @include md; - - &:hover { - @include primary-green; - } - - &:disabled, - &:hover:disabled { - background-color: #e9e9e9 !important; - border: none !important; - text-decoration: none !important; - color: #fafafb !important; - box-shadow: none !important; - } + .Select-control { + min-height: 42px; - &::before { - content: ''; - background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: sketchtool 63.1 (101010) - https://sketch.com --%3E%3Ctitle%3E8A0513C9-B831-4C33-8BAA-A13D6D42B150%3C/title%3E%3Cdesc%3ECreated with sketchtool.%3C/desc%3E%3Cdefs%3E%3Cpath d='M12.7,11.2298137 C13.6,10.0372671 14.1,8.64596273 14.1,7.05590062 C14.1,3.18012422 11,0 7.1,0 C3.2,0 0,3.18012422 0,7.05590062 C0,10.931677 3.2,14.1118012 7.1,14.1118012 C8.7,14.1118012 10.2,13.6149068 11.3,12.7204969 L14.3,15.7018634 C14.5,15.9006211 14.8,16 15,16 C15.2,16 15.5,15.9006211 15.7,15.7018634 C16.1,15.3043478 16.1,14.7080745 15.7,14.310559 L12.7,11.2298137 Z M7.1,12.0248447 C4.3,12.0248447 2,9.83850932 2,7.05590062 C2,4.27329193 4.3,1.98757764 7.1,1.98757764 C9.9,1.98757764 12.2,4.27329193 12.2,7.05590062 C12.2,9.83850932 9.9,12.0248447 7.1,12.0248447 L7.1,12.0248447 Z' id='path-1'%3E%3C/path%3E%3C/defs%3E%3Cg id='TaaS' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='02-2-Gig-Work-Listing-Page-2----Approved' transform='translate(-773.000000, -308.000000)'%3E%3Cg id='UI-Kit/Button/Medium/primary' transform='translate(753.000000, 296.000000)'%3E%3Cg id='button-md'%3E%3Cg id='Stacked-Group' transform='translate(20.000000, 0.000000)'%3E%3Cg id='UI-Kit/Icons/magnifying-glass/normal' transform='translate(0.000000, 12.000000)'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='icon-color' fill='%23FFFFFF' fill-rule='evenodd' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E") !important; - background-repeat: no-repeat; - width: 16px; - height: 16px; - margin-right: 5px; + .Select-multi-value-wrapper { + padding-top: 8px !important; + } + } } } } diff --git a/src/shared/components/challenge-detail/Header/ChallengeTags.jsx b/src/shared/components/challenge-detail/Header/ChallengeTags.jsx index 4c211c9c8e..2c85a903a5 100644 --- a/src/shared/components/challenge-detail/Header/ChallengeTags.jsx +++ b/src/shared/components/challenge-detail/Header/ChallengeTags.jsx @@ -16,7 +16,6 @@ import { DevelopmentTrackTag, } from 'topcoder-react-ui-kit'; -import { COMPETITION_TRACKS } from 'utils/tc'; import VerifiedTag from 'components/challenge-listing/VerifiedTag'; import MatchScore from 'components/challenge-listing/ChallengeCard/MatchScore'; import { calculateScore } from '../../../utils/challenge-listing/helper'; @@ -27,10 +26,10 @@ export default function ChallengeTags(props) { isSelfService, challengeId, challengesUrl, - track, challengeType, events, technPlatforms, + skills, setChallengeListingFilter, openForRegistrationChallenges, } = props; @@ -57,10 +56,10 @@ export default function ChallengeTags(props) { } return ( -
+
{ abbreviation && ( -
+
( setImmediate(() => setChallengeListingFilter( @@ -79,7 +78,7 @@ export default function ChallengeTags(props) { abbreviation ? events.map(event => (
( - +
+ +
)) } { isSelfService && ( - - On Demand - +
+ + On Demand + +
) } { tags.map(tag => ( tag && ( - + setImmediate(() => setChallengeListingFilter({ search: tag })) @@ -129,6 +132,24 @@ export default function ChallengeTags(props) { ) )) } + { + skills.map(skill => ( + skill + && ( + + setImmediate(() => setChallengeListingFilter({ search: skill })) + } + to={`${challengesUrl}?search=${ + encodeURIComponent(skill)}`} + > + {skill} + + + ) + )) + }
); } @@ -136,6 +157,7 @@ export default function ChallengeTags(props) { ChallengeTags.defaultProps = { events: [], technPlatforms: [], + skills: [], isSelfService: false, }; @@ -143,9 +165,9 @@ ChallengeTags.propTypes = { isSelfService: PT.bool, challengeId: PT.string.isRequired, challengesUrl: PT.string.isRequired, - track: PT.string.isRequired, events: PT.arrayOf(PT.string), technPlatforms: PT.arrayOf(PT.string), + skills: PT.arrayOf(PT.string), setChallengeListingFilter: PT.func.isRequired, challengeType: PT.shape().isRequired, openForRegistrationChallenges: PT.shape().isRequired, diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx index c4331cb64b..6e4bb1097a 100644 --- a/src/shared/components/challenge-detail/Header/index.jsx +++ b/src/shared/components/challenge-detail/Header/index.jsx @@ -126,11 +126,11 @@ export default function ChallengeHeader(props) { const miscTags = useMemo(() => { const tags = challenge.tags || []; const challengeTags = _.isArray(tags) ? tags : (tags || '').split(', '); - return _.uniq([ - ...challengeTags, - ...(challenge.skills || []).map(skill => skill.name), - ]); - }, [challenge.tags, challenge.skills]); + return _.uniq(challengeTags); + }, [challenge.tags]); + const skills = useMemo(() => _.uniq((challenge.skills || []).map(skill => skill.name)), [ + challenge.skills, + ]); let bonusType = ''; if (numberOfCheckpointsPrizes && topCheckPointPrize) { @@ -308,6 +308,7 @@ export default function ChallengeHeader(props) { challengesUrl={challengesUrl} events={eventNames} technPlatforms={miscTags} + skills={skills} setChallengeListingFilter={setChallengeListingFilter} openForRegistrationChallenges={openForRegistrationChallenges} /> diff --git a/src/shared/components/challenge-detail/Header/style.scss b/src/shared/components/challenge-detail/Header/style.scss index 471d30f915..fc6db81abc 100644 --- a/src/shared/components/challenge-detail/Header/style.scss +++ b/src/shared/components/challenge-detail/Header/style.scss @@ -242,114 +242,6 @@ } } -.type-tag { - display: inline-block; - - a { - color: $tc-white; - - &:active, - &:focus, - &:hover { - color: $tc-white; - } - } - - &.CH { - a { - background: $track-code-green; - - &:active, - &:focus, - &:hover { - background-color: $track-code-green; - } - } - } - - &.F2F { - a { - background: $track-code-blue; - - &:active, - &:focus, - &:hover { - background-color: $track-code-blue; - } - } - } - - &.TSK { - a { - background: $track-code-turquose; - - &:active, - &:focus, - &:hover { - background-color: $track-code-turquose; - } - } - } -} - -.event-tag { - display: inline-block; - - &.CH { - a { - background-color: $track-code-green-light; - color: $track-code-green; - - &:active, - &:focus, - &:hover { - background-color: $track-code-green-light; - color: $track-code-green; - } - } - } - - &.F2F { - a { - background-color: $track-code-blue-light; - color: $track-code-blue; - - &:active, - &:focus, - &:hover { - background-color: $track-code-blue-light; - color: $track-code-blue; - } - } - } - - &.TSK { - a { - background-color: $track-code-turquose-light; - color: $track-code-turquose; - - &:active, - &:focus, - &:hover { - background-color: $track-code-turquose-light; - color: $track-code-turquose; - } - } - } -} - -.qa { - :global { - a { - background-color: #0ab88a !important; - - &:hover { - background-color: darken(#0ab88a, 10%) !important; - } - } - } -} - .challenge-outer-container { background: $tc-white; border-radius: (3 * $corner-radius) (3 * $corner-radius) 0 0; @@ -759,18 +651,50 @@ padding: 0; } -.qaTrackEventTag { - color: #0ab88a; - background: #c1f5e7; +.block-tags-container { + display: flex; + flex-wrap: wrap; + column-gap: 5px; + row-gap: 2px; + margin-bottom: 2px; +} + +.skill { + & > a { + color: $color-black-100; + background-color: $color-black-10 !important; + border: none; + margin: 0; + + &:hover { + background-color: #d4d4d4 !important; + } + } +} - &:active, - &:focus, - &:hover { - color: lighten(#0ab88a, 2%); - background: darken(#c1f5e7, 5%); +.tag { + svg { + display: none; } - &:visited { - color: lighten(#0ab88a, 2%); + span { + margin-left: 0; + color: $color-black-80; + } + + div { + margin: 0; + } + + button, + a { + background-color: white !important; + border: 1px solid $color-black-60; + color: $color-black-80; + margin: 0; + + &:hover { + background-color: #d4d4d4 !important; + } } } diff --git a/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx b/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx index bc380abe0a..3bdeb882f6 100644 --- a/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx +++ b/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import _ from 'lodash'; import PT from 'prop-types'; import { Link } from 'topcoder-react-utils'; @@ -41,12 +41,16 @@ export default function ChallengesCard({ const statusPhase = phases .filter(p => p.name !== 'Registration') .sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0]; + const skills = useMemo(() => _.uniq((challenge.skills || []).map(skill => skill.name)), [ + challenge.skills, + ]); return (
expandTag(challenge.id)} challengesUrl={challengesUrl} diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index ed5fe9bfc9..8110f76e14 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -143,11 +143,13 @@ export default function ChallengeStatus(props) { * generates a string like "H MM to go"; here we want to render just * H MM part, so we cut the last 6 symbols. Not a good code. */ let lateNote; - if (!timeDiff.late) { - timeNote = timeNote.substring(0, timeNote.length - 6); - } else { - lateNote = timeNote.substring(timeNote.length - 8); - timeNote = timeNote.substring(0, timeNote.length - 9); + if (timeDiff.canTrimText) { + if (!timeDiff.late) { + timeNote = timeNote.substring(0, timeNote.length - 6); + } else { + lateNote = timeNote.substring(timeNote.length - 8); + timeNote = timeNote.substring(0, timeNote.length - 9); + } } return ( ) } - - to Register - ); } diff --git a/src/shared/components/challenge-listing/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/index.jsx index bf63f045cc..934a6831f8 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/index.jsx @@ -51,10 +51,9 @@ function ChallengeCard({ const registrationPhase = (challenge.phases || []).filter(phase => phase.name === 'Registration')[0]; const isRegistrationOpen = registrationPhase ? registrationPhase.isOpen : false; const isRecommendedChallenge = !!challenge.jaccard_index; - const tags = useMemo(() => _.uniq([ - ...(challenge.tags || []), - ...(challenge.skills || []).map(skill => skill.name), - ]), [challenge.tags, challenge.skills]); + const skills = useMemo(() => _.uniq((challenge.skills || []).map(skill => skill.name)), [ + challenge.skills, + ]); return (
@@ -98,7 +97,8 @@ function ChallengeCard({ && challenge.match_skills.length > 0 && ( expandTag(challenge.id)} @@ -115,10 +115,11 @@ function ChallengeCard({ ) } { !isRecommendedChallenge - && challenge.tags.length > 0 + && (challenge.tags.length + skills.length) > 0 && ( expandTag(challenge.id)} diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx index eb7409f10e..c71550717e 100644 --- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx @@ -5,7 +5,7 @@ import _ from 'lodash'; import { Link } from 'topcoder-react-utils'; import moment from 'moment'; -import React from 'react'; +import React, { useMemo } from 'react'; import PT from 'prop-types'; import TrackIcon from 'components/TrackIcon'; @@ -49,6 +49,9 @@ function ReviewOpportunityCard({ }) { const { challenge } = opportunity; let tags = challenge.tags || challenge.technologies; + const skills = useMemo(() => _.uniq((challenge.skills || []).map(skill => skill.name)), [ + challenge.skills, + ]); tags = tags.filter(tag => tag.trim().length); const { track } = challenge.track; const start = moment(opportunity.startDate); @@ -85,10 +88,11 @@ function ReviewOpportunityCard({ {' '} {start.format('MMM DD')} - { tags.length > 0 + { (tags.length + skills.length) > 0 && ( expandTag(challenge.id)} onTechTagClicked={onTechTagClicked} diff --git a/src/shared/components/challenge-listing/Tags.jsx b/src/shared/components/challenge-listing/Tags.jsx index 9f938ee9c3..8252327907 100644 --- a/src/shared/components/challenge-listing/Tags.jsx +++ b/src/shared/components/challenge-listing/Tags.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PT from 'prop-types'; import { Tag } from 'topcoder-react-ui-kit'; import Tooltip from 'components/Tooltip'; +import cn from 'classnames'; import VerifiedTag from './VerifiedTag'; import './style.scss'; @@ -16,7 +17,7 @@ const VISIBLE_TAGS = 3; * Implements component */ export default function Tags({ - expand, isExpanded, tags, onTechTagClicked, challengesUrl, recommended, verifiedTags, + expand, isExpanded, tags, skills, onTechTagClicked, challengesUrl, recommended, verifiedTags, }) { const onClick = (item) => { // resolved conflict with c++ tag @@ -43,7 +44,7 @@ export default function Tags({ return ( @@ -51,14 +52,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}
) @@ -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] === '+' ? (
- onClick(item.trim())} - key={item} - role="button" - to={tagRedirectLink(item)} - > - {item} - +
+ onClick(item.value.trim())} + key={`${item.type}_${item.value}`} + role="button" + to={tagRedirectLink(item.value)} + > + {item.value} + +
) : ( @@ -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 }; } /**