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 (
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 }
+
{
+ if (!sortParam.field || sortParam.field !== c.property) {
+ sortParam.field = c.property;
+ sortParam.order = 'desc';
+ } else {
+ sortParam.order = sortParam.order === 'asc' ? 'desc' : 'asc';
+ }
+ this.setState({ sortParam });
+ }}
+ type="button"
+ >
+
+
+
+
+
+ ) : 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
+ ?
{ window.location.reload(); }} className={defaulTheme.button}>TRY AGAIN
+ :
{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
+ />
+ {btnText}
+
+ ) : 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 }),
};
/**