diff --git a/.circleci/config.yml b/.circleci/config.yml index cada900bd7..1e58d6ae06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -236,15 +236,15 @@ workflows: context : org-global filters: branches: - only: - - develop + only: + - hot-fix # This is alternate dev env for parallel testing - "build-qa": context : org-global filters: branches: only: - - develop + - listing-develop-sync # This is beta env for production soft releases - "build-prod-beta": context : org-global @@ -260,6 +260,7 @@ workflows: branches: only: - develop + - listing-develop-sync # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration diff --git a/__tests__/shared/reducers/challenge-listing/sidebar.js b/__tests__/shared/reducers/challenge-listing/sidebar.js index f305b30aa7..8c304e5c1a 100644 --- a/__tests__/shared/reducers/challenge-listing/sidebar.js +++ b/__tests__/shared/reducers/challenge-listing/sidebar.js @@ -1,7 +1,7 @@ const defaultReducer = require('reducers/challenge-listing/sidebar').default; const expectedState = { - activeBucket: 'all', + activeBucket: 'openForRegistration', }; function testReducer(reducer) { diff --git a/package.json b/package.json index 34428acf96..eb006f35d7 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", "tc-ui": "^1.0.12", - "topcoder-react-lib": "1000.24.6", + "topcoder-react-lib": "1.0.8", "topcoder-react-ui-kit": "2.0.1", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2", diff --git a/src/server/index.js b/src/server/index.js index ed9afd3008..b4892eb12f 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -27,6 +27,7 @@ import cdnRouter from './routes/cdn'; import mailChimpRouter from './routes/mailchimp'; import mockDocuSignFactory from './__mocks__/docu-sign-mock'; import recruitCRMRouter from './routes/recruitCRM'; +import mmLeaderboardRouter from './routes/mmLeaderboard'; /* Dome API for topcoder communities */ import tcCommunitiesDemoApi from './tc-communities'; @@ -135,6 +136,7 @@ async function onExpressJsSetup(server) { server.use('/api/cdn', cdnRouter); server.use('/api/mailchimp', mailChimpRouter); server.use('/api/recruit', recruitCRMRouter); + server.use('/api/mml', mmLeaderboardRouter); // serve demo api server.use( diff --git a/src/server/routes/mmLeaderboard.js b/src/server/routes/mmLeaderboard.js new file mode 100644 index 0000000000..a9498e5818 --- /dev/null +++ b/src/server/routes/mmLeaderboard.js @@ -0,0 +1,19 @@ +/** + * The routes related to MMLeaderboard integration + */ + +import express from 'express'; +import MMLService from '../services/mmLeaderboard'; + +const cors = require('cors'); + +const routes = express.Router(); + +// Enables CORS on those routes according config above +// ToDo configure CORS for set of our trusted domains +routes.use(cors()); +routes.options('*', cors()); + +routes.get('/:id', (req, res, next) => new MMLService().getLeaderboard(req, res, next)); + +export default routes; diff --git a/src/server/services/mmLeaderboard.js b/src/server/services/mmLeaderboard.js new file mode 100644 index 0000000000..b3d812ec75 --- /dev/null +++ b/src/server/services/mmLeaderboard.js @@ -0,0 +1,34 @@ +/* eslint-disable class-methods-use-this */ +/** + * Server-side functions necessary for effective integration with MMLeaderboard + */ +import { services } from 'topcoder-react-lib'; + +const { api, submissions } = services; + +/** + * Auxiliary class that handles communication with MMLeaderboard + */ +export default class MMLService { + /** + * getLeaderboard endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async getLeaderboard(req, res, next) { + try { + const m2mToken = await api.getTcM2mToken(); + const subSrv = submissions.getService(m2mToken); + const reviewIds = await subSrv.getScanReviewIds(); + const v5api = api.getApiV5(m2mToken); + const subs = await v5api.get(`/submissions?challengeId=${req.params.id}&page=1&perPage=500`); + return res.send({ + id: req.params.id, + subs: await subs.json(), + reviewIds, + }); + } catch (err) { + return next(err); + } + } +} diff --git a/src/server/tc-communities/blockchain/metadata.json b/src/server/tc-communities/blockchain/metadata.json index e7055fefb4..82298c32ea 100644 --- a/src/server/tc-communities/blockchain/metadata.json +++ b/src/server/tc-communities/blockchain/metadata.json @@ -5,6 +5,9 @@ "tags": ["Blockchain", "Ethereum"] }] }, + "challengeListing": { + "ignoreCommunityFilterByDefault": true + }, "communityId": "blockchain", "communityName": "Blockchain Community", "groupIds": ["20000010"], diff --git a/src/server/tc-communities/cognitive/metadata.json b/src/server/tc-communities/cognitive/metadata.json index 8c26e43e68..9c0e23333d 100644 --- a/src/server/tc-communities/cognitive/metadata.json +++ b/src/server/tc-communities/cognitive/metadata.json @@ -6,6 +6,7 @@ }] }, "challengeListing": { + "ignoreCommunityFilterByDefault": true, "openChallengesInNewTabs": false }, "communityId": "cognitive", diff --git a/src/shared/actions/mmLeaderboard.js b/src/shared/actions/mmLeaderboard.js new file mode 100644 index 0000000000..4f838d4126 --- /dev/null +++ b/src/shared/actions/mmLeaderboard.js @@ -0,0 +1,54 @@ +import { redux } from 'topcoder-react-utils'; +import Service from 'services/mmLeaderboard'; +import _ from 'lodash'; + +/** + * Fetch init + */ +function getMMLeaderboardInit(id) { + return { id }; +} + +/** + * Fetch done + */ +async function getMMLeaderboardDone(id) { + const ss = new Service(); + const res = await ss.getMMLeaderboard(id); + let data = []; + if (res) { + const groupedData = _.groupBy(res.subs, 'createdBy'); + _.each(groupedData, (subs, handle) => { + // filter member subs from reviewIds + const filteredSubs = _.map(subs, (sub) => { + // eslint-disable-next-line no-param-reassign + sub.review = _.filter(sub.review, r => res.reviewIds.indexOf(r.typeId) === -1); + return sub; + }); + const sortedSubs = _.orderBy(filteredSubs, ['updated'], ['desc']); + const scores = _.orderBy(_.compact(sortedSubs[0].review), ['updated'], ['desc']); + data.push({ + createdBy: handle, + updated: sortedSubs[0].submittedDate, + id: sortedSubs[0].id, + score: scores && scores.length ? scores[0].score : '...', + }); + }); + data = _.orderBy(data, [d => (Number(d.score) ? Number(d.score) : 0)], ['desc']).map((r, i) => ({ + ...r, + rank: i + 1, + score: r.score % 1 ? Number(r.score).toFixed(5) : r.score, + })); + } + return { + id, + data, + }; +} + +export default redux.createActions({ + MMLEADERBOARD: { + GET_MML_INIT: getMMLeaderboardInit, + GET_MML_DONE: getMMLeaderboardDone, + }, +}); diff --git a/src/shared/components/Contentful/AppComponent/index.jsx b/src/shared/components/Contentful/AppComponent/index.jsx index bc9971fa63..47251bfcfc 100644 --- a/src/shared/components/Contentful/AppComponent/index.jsx +++ b/src/shared/components/Contentful/AppComponent/index.jsx @@ -10,6 +10,8 @@ import React from 'react'; import { errors } from 'topcoder-react-lib'; import Leaderboard from 'containers/tco/Leaderboard'; import RecruitCRMJobs from 'containers/Gigs/RecruitCRMJobs'; +import EmailSubscribeForm from 'containers/EmailSubscribeForm'; + const { fireErrorMessage } = errors; @@ -34,6 +36,9 @@ export function AppComponentSwitch(appComponent) { if (appComponent.fields.type === 'RecruitCRM-Jobs') { return ; } + if (appComponent.fields.type === 'EmailSubscribeForm') { + return ; + } fireErrorMessage('Unsupported app component type from contentful', ''); return null; } diff --git a/src/shared/components/Gigs/GigDetails/index.jsx b/src/shared/components/Gigs/GigDetails/index.jsx index 2471687f42..017418648f 100644 --- a/src/shared/components/Gigs/GigDetails/index.jsx +++ b/src/shared/components/Gigs/GigDetails/index.jsx @@ -43,6 +43,8 @@ export default function GigDetails(props) { } let skills = getCustomField(job.custom_fields, 'Technologies Required'); if (skills !== 'n/a') skills = skills.split(',').join(', '); + const hPerW = getCustomField(job.custom_fields, 'Hours per week'); + const compens = job.min_annual_salary === job.max_annual_salary ? job.max_annual_salary : `${job.min_annual_salary} - ${job.max_annual_salary}`; return (
@@ -70,7 +72,7 @@ export default function GigDetails(props) {
Compensation - ${job.min_annual_salary} - ${job.max_annual_salary} / {getSalaryType(job.salary_type)} + ${compens} / {getSalaryType(job.salary_type)}
@@ -84,7 +86,7 @@ export default function GigDetails(props) {
Hours - {getCustomField(job.custom_fields, 'Hours per week')} hours / week + {hPerW === 'n/a' ? hPerW : `${hPerW} hours / week`}
@@ -108,7 +110,10 @@ export default function GigDetails(props) { * Topcoder does not provide visa sponsorship nor will we work with Staffing Agencies. - ** Topcoder and Wipro employees are not eligible for Gig work opportunities. Do not apply and send questions to support@topcoder.com. + ** USA Visa Holders - Please consult an attorney before applying to any Topcoder Gig. Some visa statuses will or will not allow you to conduct freelance work with Topcoder. + + + *** Topcoder and Wipro employees are not eligible for Gig work opportunities. Do not apply and send questions to support@topcoder.com.
diff --git a/src/shared/components/MMatchLeaderboard/index.jsx b/src/shared/components/MMatchLeaderboard/index.jsx new file mode 100644 index 0000000000..6ed9c4a95b --- /dev/null +++ b/src/shared/components/MMatchLeaderboard/index.jsx @@ -0,0 +1,259 @@ +/** + * MMatchLeaderboard component. + * ## Parameters + * - property, indicating which property of a record to return. + * + * - table, table columns information, can support both object and string. + * 1. The styles are react inline style, style names should be in camelcase. + * 2. When it's a string, it firstly should be from a valid json, + * then replace `"` and `'` with `&q;`, it should not have unnessesary white space. + * 3. If pass `table=""`, it will return a table using property name as header name; + * + * - render, custom render function, can support both object and string. + * 1. When it's a string, it firstly should be from valid function source code, + * then replace `"` and `'` with `&q;`, it should not have unnessesary white space. + * 2. In string, it should NOT be an arrow function. + * + */ +/* eslint-disable no-eval */ + +import PT from 'prop-types'; +import _ from 'lodash'; +import React, { Component } from 'react'; +import { fixStyle } from 'utils/contentful'; +import cn from 'classnames'; +import { Scrollbars } from 'react-custom-scrollbars'; +import './style.scss'; + +export default class MMLeaderboard extends Component { + constructor(props) { + super(props); + + this.state = { + sortParam: { + order: '', + field: '', + }, + }; + } + + render() { + const { + property, + table, + render, + limit, + tableHeight, + tableWidth, + headerIndexCol, + } = this.props; + + let { + countRows, + } = this.props; + if (countRows === 'true') { + countRows = true; + } + if (countRows === 'false') { + countRows = false; + } + + let { + leaderboard: { + data, + }, + } = this.props; + + const { sortParam } = this.state; + + if (sortParam.field) { + // Use Lodash to sort array + data = _.orderBy( + data, + [d => String(d[sortParam.field]).toLowerCase()], [sortParam.order], + ); + } + + const renderData = () => { + if (property) { + if (data.length > 0 && data[0][property]) { + if (typeof data[0][property] === 'string') { + return data[0][property]; + } + if (typeof data[0][property] === 'number') { + return data[0][property].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + } + return 'empty array!'; + } if (table || table === '') { + let columns = table; + if (table === '') { + columns = []; + if (data.length > 0) { + const keys = Object.keys(data[0]); + columns = keys.map(k => ({ property: k })); + } + } + + if (typeof table === 'string' && table.length > 0) { + try { + const s = table.replace(/'/g, '"'); + columns = JSON.parse(s); + } catch (error) { + return `table json parse error: ${error}`; + } + } + + const header = cols => ( + + { countRows && ({headerIndexCol}) } + { + cols.map((c) => { + const name = c.headerName; + const { styles } = c; + return name ? ( + +
+ { name } + +
+ + ) : null; + }) + } + + ); + const bodyRow = (record, cols, i) => ( + + { (countRows && (limit <= 0 || i < limit)) ? {i + 1} : ' ' } + { + cols.map((c) => { + const prop = c.property; + let { memberLinks } = c; + if (memberLinks === 'true') { + memberLinks = true; + } + if (memberLinks === 'false') { + memberLinks = false; + } + const { styles } = c; + let value = ''; + if (limit <= 0 || i < limit) { + if (typeof record[prop] === 'string') { + value = record[prop]; + } + if (typeof record[prop] === 'number') { + value = record[prop].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + } + return value ? ( + + {memberLinks ? ({value}) : value} + + ) : null; + }) + } + + ); + return ( + + + + { header(columns) } + + + { data.map((record, i) => bodyRow(record, columns, i)) } + +
+
+ ); + } if (render) { + let f = render; + if (typeof render === 'string') { + const s = render.replace(/&q;/g, '"'); + try { + f = eval(`(${s})`); + } catch (error) { + return `render function parse error: ${error}`; + } + } + if (typeof f !== 'function') { + return 'render is not a function'; + } + try { + const retValue = f(data); + if (typeof retValue === 'string') { + return retValue; + } + if (typeof retValue === 'number') { + return retValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + } catch (error) { + return `error happened while rendering: ${error}`; + } + } + return 'invalid prop, use property, table or render'; + }; + return ( + + { data.length ? renderData() :

No data available yet.

} +
+ ); + } +} + +MMLeaderboard.defaultProps = { + property: null, + table: null, + render: null, + limit: 0, + countRows: false, + tableHeight: '100%', + tableWidth: '100%', + headerIndexCol: '', +}; + +MMLeaderboard.propTypes = { + leaderboard: PT.shape().isRequired, + property: PT.string, + limit: PT.number, + countRows: PT.oneOfType([ + PT.string, + PT.bool, + ]), + tableHeight: PT.string, + tableWidth: PT.string, + table: PT.oneOfType([ + PT.string, + PT.arrayOf(PT.shape()), + ]), + render: PT.oneOfType([ + PT.string, + PT.func, + ]), + headerIndexCol: PT.string, +}; diff --git a/src/shared/components/MMatchLeaderboard/style.scss b/src/shared/components/MMatchLeaderboard/style.scss new file mode 100644 index 0000000000..97ef6ffada --- /dev/null +++ b/src/shared/components/MMatchLeaderboard/style.scss @@ -0,0 +1,119 @@ +@import '~styles/mixins'; + +$light-gray: #d4d4d4; + +.no-data-title { + text-align: center; +} + +.body-row, +.header-cell { + border-bottom: 1px solid $light-gray; +} + +.sort-container > div { + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; +} + +.component-container { + > div:last-child { + width: 7px !important; + border-radius: 4.5px !important; + } + + > div:last-child > div { + background-color: rgba(#2a2a2a, 0.3) !important; + border-radius: 4.5px !important; + } + + /* width */ + > div:first-child::-webkit-scrollbar { + width: 7px; + } + + /* Track */ + > div:first-child::-webkit-scrollbar-track { + box-shadow: none; + } + + /* Handle */ + > div:first-child::-webkit-scrollbar-thumb { + background: rgba(#2a2a2a, 0.3); + border-radius: 4.5px; + width: 7px; + } + + /* Handle on hover */ + > div:first-child::-webkit-scrollbar-thumb:hover { + background: rgba(#2a2a2a, 0.5); + } + + .table-container { + width: 100%; + + th { + @include roboto-medium; + } + + td, + th { + padding: 0 5px; + font-size: 14px; + text-align: left; + color: #2a2a2a; + line-height: 51px; + letter-spacing: 0.5px; + + &:last-child { + padding-right: 20px; + } + } + } +} + +.header-table-content { + display: flex; + align-items: center; +} + +.sort-container { + display: flex; + flex-direction: column; + margin-left: 5px; + padding: 0; + border: none; + outline: none; + background: transparent; +} + +.sort-up { + border-bottom: 4px solid $light-gray; + margin-bottom: 2px; + + &.active { + border-bottom: 4px solid $tc-black; + } +} + +.sort-down { + border-top: 4px solid $light-gray; + + &.active { + border-top: 4px solid $tc-black; + } +} + +.handle-link { + @include roboto-medium; + + font-weight: 500; + color: #0d61bf !important; + text-decoration: underline; + + &:hover { + text-decoration: none; + } +} diff --git a/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx b/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx index 3e69c4b904..6bc2bd13c7 100644 --- a/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx +++ b/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx @@ -53,7 +53,7 @@ export default function ChallengeFilters({ filterRulesCount += 1; } if (isReviewOpportunitiesBucket && filterState.reviewOpportunityType) filterRulesCount += 1; - if (selectedCommunityId !== '' && selectedCommunityId !== 'All') filterRulesCount += 1; + // if (selectedCommunityId !== '' && selectedCommunityId !== 'All') filterRulesCount += 1; const isTrackOn = track => filterState.tracks && filterState.tracks[track]; const switchTrack = (track, on) => { diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx index 38175add29..371326cc55 100644 --- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx @@ -127,7 +127,7 @@ export default function Bucket({ challengeType={_.find(challengeTypes, { name: challenge.type })} challengesUrl={challengesUrl} newChallengeDetails={newChallengeDetails} - onTechTagClicked={tag => setFilterState({ ..._.clone(filterState), tags: [tag] })} + onTechTagClicked={tag => setFilterState({ ..._.clone(filterState), tags: [tag], types: [] })} openChallengesInNewTabs={openChallengesInNewTabs} prizeMode={prizeMode} key={challenge.id} diff --git a/src/shared/containers/Dashboard/index.jsx b/src/shared/containers/Dashboard/index.jsx index 866cc83742..552fa5cd59 100644 --- a/src/shared/containers/Dashboard/index.jsx +++ b/src/shared/containers/Dashboard/index.jsx @@ -445,7 +445,7 @@ function mapDispatchToProps(dispatch) { const cl = challengeListingActions.challengeListing; const cls = challengeListingSidebarActions.challengeListing.sidebar; dispatch(cl.setFilter(filter)); - dispatch(cls.selectBucket(BUCKETS.ALL)); + dispatch(cls.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, showXlBadge: name => dispatch(dash.showXlBadge(name)), switchChallengeFilter: filter => dispatch(dash.switchChallengeFilter(filter)), diff --git a/src/shared/containers/EmailSubscribeForm/index.jsx b/src/shared/containers/EmailSubscribeForm/index.jsx new file mode 100644 index 0000000000..5b9fe1d654 --- /dev/null +++ b/src/shared/containers/EmailSubscribeForm/index.jsx @@ -0,0 +1,221 @@ +/** + * Genetic subscribe for MailChimp tags component + */ +import React from 'react'; +import PT from 'prop-types'; +import { isValidEmail } from 'utils/tc'; +import TextInput from 'components/GUIKit/TextInput'; +import _ from 'lodash'; +import LoadingIndicator from 'components/LoadingIndicator'; +import { Link } from 'topcoder-react-utils'; +import defaulTheme from './style.scss'; + +/* Holds the base URL of Community App endpoints that proxy HTTP request to + * mailchimp APIs. */ +const PROXY_ENDPOINT = '/api/mailchimp'; + +class SubscribeMailChimpTagContainer extends React.Component { + constructor(props) { + super(props); + this.state = { + formErrors: {}, + formData: {}, + }; + this.onSubscribeClick = this.onSubscribeClick.bind(this); + this.onFormInputChange = this.onFormInputChange.bind(this); + this.validateForm = this.validateForm.bind(this); + } + + onSubscribeClick() { + this.validateForm(); + // eslint-disable-next-line consistent-return + this.setState((state) => { + const { formData, formErrors } = state; + if (_.isEmpty(formErrors)) { + const { listId, tags } = this.props; + const fetchUrl = `${PROXY_ENDPOINT}/${listId}/members/${formData.email}/tags`; + const data = { + email_address: formData.email, + status: 'subscribed', + tags: tags.map(t => ({ name: t, status: 'active' })), + merge_fields: { + FNAME: formData.fname, + LNAME: formData.lname, + }, + }; + fetch(fetchUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }).then(result => result.json()).then((dataResponse) => { + if (dataResponse.status === 204) { + // regist success + return this.setState({ + subscribing: false, + subsribed: true, + error: '', + }); + } + if (dataResponse.status === 404) { + // new email register it for list and add tags + data.tags = tags; + return fetch(`${PROXY_ENDPOINT}/${listId}/members`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then(result => result.json()).then((rsp) => { + this.setState({ + subscribing: false, + subsribed: !rsp.detail, + error: rsp.detail ? rsp.title : '', + }); + }); + } + return this.setState({ + subscribing: false, + subsribed: false, + error: `Error ${dataResponse.status} when assigning tags to ${formData.email}`, + }); + }) + .catch((e) => { + this.setState({ + subscribing: false, + subsribed: false, + error: e.message, + }); + }); + return { subscribing: true }; + } + }); + } + + onFormInputChange(key, val) { + this.setState((state) => { + const { formData } = state; + formData[key] = val; + return { + ...state, + formData, + }; + }); + this.validateForm(key); + } + + validateForm(key) { + this.setState((state) => { + const { formData, formErrors } = state; + if (key) { + // validate only the key + if (!formData[key] || !_.trim(formData[key])) formErrors[key] = 'Required field'; + else if (key === 'email' && !(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; + else delete formErrors[key]; + } else { + _.each(['fname', 'lname', 'email'], (rkey) => { + if (!formData[rkey] || !_.trim(formData[rkey])) formErrors[rkey] = 'Required field'; + else if (rkey === 'email' && !(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; + else delete formErrors[key]; + }); + } + // updated state + return { + ...state, + formErrors, + }; + }); + } + + render() { + const { + formData, formErrors, subscribing, subsribed, error, + } = this.state; + const { + btnText, title, successTitle, successText, successLink, successLinkText, + } = this.props; + return ( +
+ { + subscribing ? ( +
+ +

+ Processing your subscription... +

+
+ ) : null + } + { + subsribed || error ? ( +
+

{error ? 'OOPS!' : successTitle}

+

{error || successText}

+ { + error + ? + : {successLinkText} + } +
+ ) : null + } + { + !subscribing && !subsribed && !error ? ( + +
{title}
+ this.onFormInputChange('fname', val)} + errorMsg={formErrors.fname} + value={formData.fname} + required + /> + this.onFormInputChange('lname', val)} + errorMsg={formErrors.lname} + value={formData.lname} + required + /> + this.onFormInputChange('email', val)} + errorMsg={formErrors.email} + value={formData.email} + required + /> + +
+ ) : null + } +
+ ); + } +} + +SubscribeMailChimpTagContainer.defaultProps = { + title: '', + btnText: '', + successTitle: 'Success!', + successText: '', + successLink: '', + successLinkText: '', +}; + +SubscribeMailChimpTagContainer.propTypes = { + listId: PT.string.isRequired, + tags: PT.arrayOf(PT.string).isRequired, + title: PT.string, + btnText: PT.string, + successTitle: PT.string, + successText: PT.string, + successLink: PT.string, + successLinkText: PT.string, +}; + +export default SubscribeMailChimpTagContainer; diff --git a/src/shared/containers/EmailSubscribeForm/style.scss b/src/shared/containers/EmailSubscribeForm/style.scss new file mode 100644 index 0000000000..4355e4cfd6 --- /dev/null +++ b/src/shared/containers/EmailSubscribeForm/style.scss @@ -0,0 +1,98 @@ +@import "~components/Contentful/default"; + +.loadingWrap { + margin: 0; + padding: 0 34px; + + @include gui-kit-headers; + @include gui-kit-content; + @include roboto-regular; + + .loadingText { + font-family: Roboto, sans-serif; + color: #2a2a2a; + text-align: center; + margin-top: 26px; + } +} + +.subscribedWrap { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 34px; + + @include gui-kit-headers; + @include gui-kit-content; + @include roboto-regular; + + > h4 { + margin-bottom: 16px !important; + } + + > p { + font-size: 24px !important; + line-height: 36px !important; + text-align: center; + + &.errorMsg { + color: #ef476f; + } + } +} + +.wrapper { + display: flex; + flex-direction: column; + color: #2a2a2a; + + @include gui-kit-headers; + @include gui-kit-content; + @include roboto-regular; + + h6 { + margin-top: 41px; + + @include xs-to-sm { + margin-top: 0; + } + } + + > div { + margin-bottom: 8px; + } + + .button { + background-color: #137d60; + border-radius: 20px; + color: #fff !important; + font-size: 14px; + font-weight: bolder; + text-decoration: none; + text-transform: uppercase; + line-height: 40px; + padding: 0 20px; + border: none; + outline: none; + margin-top: 13px; + margin-bottom: 41px; + max-width: 150px; + + &:hover { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + background-color: #0ab88a; + } + + @include xs-to-sm { + margin-bottom: 20px; + } + + &:disabled { + background-color: #e9e9e9 !important; + border: none !important; + text-decoration: none !important; + color: #fafafb !important; + box-shadow: none !important; + } + } +} diff --git a/src/shared/containers/MMLeaderboard.jsx b/src/shared/containers/MMLeaderboard.jsx new file mode 100644 index 0000000000..70e9fd88ea --- /dev/null +++ b/src/shared/containers/MMLeaderboard.jsx @@ -0,0 +1,62 @@ +/** + * MMatch leaderboard container + * Used v5/submissions API to fetch data + */ +import React from 'react'; +import PT from 'prop-types'; +import { connect } from 'react-redux'; +import MMatchLeaderboard from 'components/MMatchLeaderboard'; +import actions from 'actions/mmLeaderboard'; +import LoadingIndicator from 'components/LoadingIndicator'; + +class MMLeaderboard extends React.Component { + componentDidMount() { + const { + getMMLeaderboard, + challengeId, + leaderboard, + } = this.props; + if (!leaderboard || (!leaderboard.loading && !leaderboard.data)) { + getMMLeaderboard(challengeId); + } + } + + render() { + const { leaderboard } = this.props; + return leaderboard && leaderboard.data ? ( + + ) : ; + } +} + +MMLeaderboard.propTypes = { + challengeId: PT.string.isRequired, + getMMLeaderboard: PT.func.isRequired, + leaderboard: PT.shape().isRequired, +}; + +function mapStateToProps(state, ownProps) { + const { challengeId } = ownProps; + return { + leaderboard: state.mmLeaderboard ? state.mmLeaderboard[challengeId] : {}, + }; +} + +function mapDispatchToProps(dispatch) { + const a = actions.mmleaderboard; + return { + getMMLeaderboard: async (challengeId) => { + dispatch(a.getMmlInit(challengeId)); + dispatch(a.getMmlDone(challengeId)); + }, + }; +} + +const Container = connect( + mapStateToProps, + mapDispatchToProps, +)(MMLeaderboard); + +export default Container; diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 5e18d5256c..1dc074aee0 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -878,8 +878,9 @@ const mapDispatchToProps = (dispatch) => { setChallengeListingFilter: (filter) => { const cl = challengeListingActions.challengeListing; const cls = challengeListingSidebarActions.challengeListing.sidebar; - dispatch(cl.setFilter(filter)); - dispatch(cls.selectBucket(BUCKETS.ALL)); + const newFilter = _.assign({}, { types: [], tags: [] }, filter); + dispatch(cls.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); + dispatch(cl.setFilter(newFilter)); }, setSpecsTabState: state => dispatch(pageActions.page.challengeDetails.setSpecsTabState(state)), unregisterFromChallenge: (auth, challengeId) => { diff --git a/src/shared/containers/challenge-listing/Listing/index.jsx b/src/shared/containers/challenge-listing/Listing/index.jsx index c045ee844d..7cc11cfbe1 100644 --- a/src/shared/containers/challenge-listing/Listing/index.jsx +++ b/src/shared/containers/challenge-listing/Listing/index.jsx @@ -50,8 +50,10 @@ export class ListingContainer extends React.Component { getCommunitiesList, markHeaderMenu, selectBucket, + setFilter, selectCommunity, queryBucket, + filter, } = this.props; markHeaderMenu(); @@ -65,8 +67,10 @@ export class ListingContainer extends React.Component { getCommunitiesList(auth); } + let selectedCommunity; if (communityId) { selectCommunity(communityId); + selectedCommunity = communitiesList.data.find(item => item.communityId === communityId); } if (mounted) { @@ -76,7 +80,14 @@ export class ListingContainer extends React.Component { // if (BUCKETS.PAST !== activeBucket) { // dropChallenges(); // this.loadChallenges(); - this.reloadChallenges(); + if (!selectedCommunity) { + this.reloadChallenges(); + } else { + const groups = selectedCommunity.groupIds && selectedCommunity.groupIds.length + ? [selectedCommunity.groupIds[0]] : []; + // update the challenge listing filter for selected community + setFilter({ ..._.clone(filter), groups, events: [] }); + } // } } @@ -561,7 +572,7 @@ export class ListingContainer extends React.Component { setFilter(state); setSearchText(state.name || ''); // if (activeBucket === BUCKETS.SAVED_FILTER) { - // selectBucket(BUCKETS.ALL); + // selectBucket(BUCKETS.OPEN_FOR_REGISTRATION); // } else if (activeBucket === BUCKETS.SAVED_REVIEW_OPPORTUNITIES_FILTER) { // selectBucket(BUCKETS.REVIEW_OPPORTUNITIES); // } @@ -598,7 +609,7 @@ ListingContainer.defaultProps = { openChallengesInNewTabs: false, preListingMsg: null, prizeMode: 'money-usd', - queryBucket: BUCKETS.ALL, + queryBucket: BUCKETS.OPEN_FOR_REGISTRATION, meta: {}, expanding: false, // isBucketSwitching: false, diff --git a/src/shared/containers/tc-communities/cs/Home.js b/src/shared/containers/tc-communities/cs/Home.js index 4eab16db01..69d3402a93 100644 --- a/src/shared/containers/tc-communities/cs/Home.js +++ b/src/shared/containers/tc-communities/cs/Home.js @@ -11,7 +11,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/About.js b/src/shared/containers/tc-communities/iot/About.js index 2eeb7d4034..1002e21d53 100644 --- a/src/shared/containers/tc-communities/iot/About.js +++ b/src/shared/containers/tc-communities/iot/About.js @@ -18,7 +18,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/AssetDetail.js b/src/shared/containers/tc-communities/iot/AssetDetail.js index 018792df46..7614d6bf58 100644 --- a/src/shared/containers/tc-communities/iot/AssetDetail.js +++ b/src/shared/containers/tc-communities/iot/AssetDetail.js @@ -20,7 +20,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/Assets.js b/src/shared/containers/tc-communities/iot/Assets.js index f4141e9c43..2b8a637b45 100644 --- a/src/shared/containers/tc-communities/iot/Assets.js +++ b/src/shared/containers/tc-communities/iot/Assets.js @@ -20,7 +20,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, toggleGrid: () => { const a = actions.page.communities.iot.assets; diff --git a/src/shared/containers/tc-communities/iot/GetStarted.js b/src/shared/containers/tc-communities/iot/GetStarted.js index 3efc23235f..ccf0e0ab80 100644 --- a/src/shared/containers/tc-communities/iot/GetStarted.js +++ b/src/shared/containers/tc-communities/iot/GetStarted.js @@ -18,7 +18,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/Home.js b/src/shared/containers/tc-communities/iot/Home.js index a9901d8b0f..359ec304d9 100644 --- a/src/shared/containers/tc-communities/iot/Home.js +++ b/src/shared/containers/tc-communities/iot/Home.js @@ -18,7 +18,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/containers/tc-communities/wipro/Home.js b/src/shared/containers/tc-communities/wipro/Home.js index 24f4a01ad3..42ca680476 100644 --- a/src/shared/containers/tc-communities/wipro/Home.js +++ b/src/shared/containers/tc-communities/wipro/Home.js @@ -18,7 +18,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/containers/tc-communities/zurich/Home.js b/src/shared/containers/tc-communities/zurich/Home.js index 61515fe8b2..9e6cc82dbb 100644 --- a/src/shared/containers/tc-communities/zurich/Home.js +++ b/src/shared/containers/tc-communities/zurich/Home.js @@ -11,7 +11,7 @@ function mapDispatchToProps(dispatch) { const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(BUCKETS.OPEN_FOR_REGISTRATION)); }, }; } diff --git a/src/shared/reducers/challenge-listing/sidebar.js b/src/shared/reducers/challenge-listing/sidebar.js index e12ddd13b1..bb3822811f 100644 --- a/src/shared/reducers/challenge-listing/sidebar.js +++ b/src/shared/reducers/challenge-listing/sidebar.js @@ -201,7 +201,7 @@ function create(initialState = {}) { // }), // [a.updateSavedFilter]: onUpdateSavedFilter, }, _.defaults(initialState, { - activeBucket: BUCKETS.ALL, + activeBucket: BUCKETS.OPEN_FOR_REGISTRATION, // activeSavedFilter: 0, // editSavedFiltersMode: false, // savedFilters: [], diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index 6c6baa6ddc..758a8f6d6d 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -36,6 +36,7 @@ import { factory as leaderboardFactory } from './leaderboard'; import { factory as scoreboardFactory } from './tco/scoreboard'; import { factory as termsFactory } from './terms'; import newsletterPreferences from './newsletterPreferences'; +import mmLeaderboard from './mmLeaderboard'; import recruitCRM from './recruitCRM'; /** @@ -142,6 +143,7 @@ export function factory(req) { policyPages, newsletterPreferences, recruitCRM, + mmLeaderboard, })); } diff --git a/src/shared/reducers/mmLeaderboard.js b/src/shared/reducers/mmLeaderboard.js new file mode 100644 index 0000000000..2cb84a921a --- /dev/null +++ b/src/shared/reducers/mmLeaderboard.js @@ -0,0 +1,49 @@ +/** + * Reducer for state.mmleaderboard + */ +import actions from 'actions/mmLeaderboard'; +import { handleActions } from 'redux-actions'; + +/** + * Handles getMmleaderboardInit action. + * @param {Object} state Previous state. + */ +function onInit(state, { payload }) { + return { + ...state, + [payload.id]: { + loading: true, + }, + }; +} + +/** + * Handles getMmleaderboardDone action. + * @param {Object} state Previous state. + * @param {Object} action The action. + */ +function onDone(state, { payload }) { + return { + ...state, + [payload.id]: { + loading: false, + data: payload.data, + }, + }; +} + +/** + * Creates mmleaderboard reducer with the specified initial state. + * @param {Object} state Optional. If not given, the default one is + * generated automatically. + * @return {Function} Reducer. + */ +function create(state = {}) { + return handleActions({ + [actions.mmleaderboard.getMmlInit]: onInit, + [actions.mmleaderboard.getMmlDone]: onDone, + }, state); +} + +/* Reducer with the default initial state. */ +export default create(); diff --git a/src/shared/services/mmLeaderboard.js b/src/shared/services/mmLeaderboard.js new file mode 100644 index 0000000000..5a88b434ab --- /dev/null +++ b/src/shared/services/mmLeaderboard.js @@ -0,0 +1,21 @@ +import fetch from 'isomorphic-fetch'; +import { logger } from 'topcoder-react-lib'; + +const PROXY_ENDPOINT = '/api/mml'; + +export default class Service { + baseUrl = PROXY_ENDPOINT; + + /** + * Get MMLeaderboard by id + * @param {*} id The request id + */ + async getMMLeaderboard(id) { + const res = await fetch(`${this.baseUrl}/${id}`); + if (!res.ok) { + const error = new Error(`Failed to get leaderboard ${id}`); + logger.error(error, res); + } + return res.json(); + } +} diff --git a/src/shared/utils/gigs.js b/src/shared/utils/gigs.js index 63d42a1201..fa08f36579 100644 --- a/src/shared/utils/gigs.js +++ b/src/shared/utils/gigs.js @@ -12,6 +12,7 @@ export function getSalaryType(data) { switch (data.id) { case 2: return 'annual'; case 3: return 'week'; + case 5: return 'hourly'; default: return 'n/a'; } } diff --git a/src/shared/utils/markdown.js b/src/shared/utils/markdown.js index 1507bc3cd5..a7cdb5cd8c 100644 --- a/src/shared/utils/markdown.js +++ b/src/shared/utils/markdown.js @@ -25,6 +25,7 @@ import Looker from 'containers/Looker'; import AnchorLink from 'react-anchor-link-smooth-scroll'; import Modal from 'components/Contentful/Modal'; import NewsletterArchive from 'containers/NewsletterArchive'; +import MMLeaderboard from 'containers/MMLeaderboard'; import tco19SecLg from 'components/buttons/outline/tco/tco19-sec-lg.scss'; import tco19Lg from 'components/buttons/outline/tco/tco19-lg.scss'; @@ -127,6 +128,7 @@ const customComponents = { }, }; }, + MMLeaderboard: attrs => ({ type: MMLeaderboard, props: attrs }), }; /**