diff --git a/.circleci/config.yml b/.circleci/config.yml index dfd34be43f..083e572fa9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -357,14 +357,14 @@ workflows: filters: branches: only: - - free + - thrive-rss # This is beta env for production soft releases - "build-prod-beta": context : org-global filters: branches: only: - - free + - mm-leaderboard-theme # This is stage env for production QA releases - "build-prod-staging": context : org-global @@ -372,7 +372,7 @@ workflows: branches: only: - develop - - feature/recommended-challenges-update + - ast-timeline-fix # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration diff --git a/automated-smoke-test/page-objects/pages/topcoder/challenge-detail/challenge-detail.helper.ts b/automated-smoke-test/page-objects/pages/topcoder/challenge-detail/challenge-detail.helper.ts index 23f4173703..49dc02efc1 100644 --- a/automated-smoke-test/page-objects/pages/topcoder/challenge-detail/challenge-detail.helper.ts +++ b/automated-smoke-test/page-objects/pages/topcoder/challenge-detail/challenge-detail.helper.ts @@ -294,7 +294,6 @@ export class ChallengeDetailPageHelper { 'Started', 'Registration', 'Submission', - 'Review', 'Winners', ]; for (let i = 0; i < childDivs.length; i++) { diff --git a/config/default.js b/config/default.js index 569d65b3ca..487939cd2d 100644 --- a/config/default.js +++ b/config/default.js @@ -110,6 +110,7 @@ module.exports = { HOME: '/home', BLOG: 'https://www.topcoder-dev.com/blog', BLOG_FEED: 'https://www.topcoder.com/blog/feed/', + THRIVE_FEED: 'https://topcoder-dev.com/api/feeds/thrive', COMMUNITY: 'https://community.topcoder-dev.com', FORUMS: 'https://apps.topcoder-dev.com/forums', FORUMS_VANILLA: 'https://vanilla.topcoder-dev.com', diff --git a/config/production.js b/config/production.js index 3e65ad253d..24ca88848a 100644 --- a/config/production.js +++ b/config/production.js @@ -58,6 +58,7 @@ module.exports = { CS: 'https://cs.topcoder.com', }, EMAIL_VERIFY_URL: 'http://www.topcoder.com/settings/account/changeEmail', + THRIVE_FEED: 'https://topcoder.com/api/feeds/thrive', }, /* Filestack configuration for uploading Submissions * These are for the production back end */ diff --git a/package.json b/package.json index c2d172eb45..c51c17c533 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "redux-promise": "^0.6.0", "request-ip": "^2.0.2", "require-context": "^1.1.0", + "rss": "^1.2.2", "rss-parser": "^3.12.0", "serialize-javascript": "^2.1.1", "serve-favicon": "^2.5.0", diff --git a/src/server/index.js b/src/server/index.js index 09f05c4fd7..31f949ca61 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -31,6 +31,7 @@ import mmLeaderboardRouter from './routes/mmLeaderboard'; import growsurfRouter from './routes/growsurf'; import gSheetsRouter from './routes/gSheet'; import blogRouter from './routes/blog'; +import feedsRouter from './routes/feeds'; /* Dome API for topcoder communities */ import tcCommunitiesDemoApi from './tc-communities'; @@ -143,6 +144,7 @@ async function onExpressJsSetup(server) { server.use('/api/growsurf', growsurfRouter); server.use('/api/gsheets', gSheetsRouter); server.use('/api/blog', blogRouter); + server.use('/api/feeds', feedsRouter); // serve demo api server.use( diff --git a/src/server/routes/feeds.js b/src/server/routes/feeds.js new file mode 100644 index 0000000000..c6046f2743 --- /dev/null +++ b/src/server/routes/feeds.js @@ -0,0 +1,62 @@ +/** + * The routes that expose assets and content from Contentful CMS to the CDN. + */ + +import express from 'express'; +import RSS from 'rss'; +import ReactDOMServer from 'react-dom/server'; +import md from 'utils/markdown'; +import { + getService, +} from '../services/contentful'; + +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('/thrive', async (req, res, next) => { + try { + const data = await getService('EDU', 'master', true).queryEntries({ + content_type: 'article', + limit: 20, + order: '-sys.createdAt', + include: 2, + }); + const feed = new RSS({ + title: 'Topcoder Thrive', + description: 'Tutorials And Workshops That Matter | Thrive | Topcoder', + feed_url: 'https://topcoder.com/api/feeds/thrive', + site_url: 'https://topcoder.com/thrive', + image_url: 'https://www.topcoder.com/wp-content/uploads/2020/05/cropped-TC-Icon-32x32.png', + docs: 'https://www.topcoder.com/thrive/tracks?track=Topcoder', + webMaster: ' Kiril Kartunov', + copyright: '2021 - today, Topcoder', + language: 'en', + categories: ['Competitive Programming', 'Data Science', 'Design', 'Development', 'QA', 'Gig work', 'Topcoder'], + ttl: '60', + }); + if (data && data.total) { + data.items.forEach((entry) => { + feed.item({ + title: entry.fields.title, + description: ReactDOMServer.renderToString(md(entry.fields.content)), + url: `https://topcoder.com/thrive/articles/${entry.fields.slug || encodeURIComponent(entry.fields.title)}?utm_source=community&utm_campaign=thrive-feed&utm_medium=promotion`, + date: entry.fields.creationDate, + categories: entry.fields.tags, + author: entry.fields.contentAuthor[0].fields.name, + }); + }); + } + res.set('Content-Type', 'application/rss+xml'); + res.send(feed.xml({ indent: true })); + } catch (e) { + next(e); + } +}); + +export default routes; diff --git a/src/shared/actions/mmLeaderboard.js b/src/shared/actions/mmLeaderboard.js index 4f838d4126..956671d831 100644 --- a/src/shared/actions/mmLeaderboard.js +++ b/src/shared/actions/mmLeaderboard.js @@ -1,7 +1,8 @@ -import { redux } from 'topcoder-react-utils'; +import { redux, config } from 'topcoder-react-utils'; import Service from 'services/mmLeaderboard'; import _ from 'lodash'; + /** * Fetch init */ @@ -34,11 +35,22 @@ async function getMMLeaderboardDone(id) { score: scores && scores.length ? scores[0].score : '...', }); }); - data = _.orderBy(data, [d => (Number(d.score) ? Number(d.score) : 0)], ['desc']).map((r, i) => ({ + data = _.orderBy(data, [d => (Number(d.score) ? Number(d.score) : 0), d => new Date(d.updated) - new Date()], ['desc']).map((r, i) => ({ ...r, rank: i + 1, score: r.score % 1 ? Number(r.score).toFixed(5) : r.score, })); + // Fetch member photos and rating for top 10 + const results = await Promise.all( + _.take(data, 10).map(d => fetch(`${config.API.V5}/members/${d.createdBy}`)), + ); + const memberData = await Promise.all(results.map(r => r.json())); + // merge with data + // eslint-disable-next-line array-callback-return + memberData.map((member, indx) => { + data[indx].photoUrl = member.photoURL; + data[indx].rating = member.maxRating && member.maxRating.rating; + }); } return { id, diff --git a/src/shared/components/InputSelect/index.jsx b/src/shared/components/InputSelect/index.jsx index a91c088093..bd43cbbdcc 100644 --- a/src/shared/components/InputSelect/index.jsx +++ b/src/shared/components/InputSelect/index.jsx @@ -111,7 +111,7 @@ export default class InputSelect extends Component { let i = 0; let node = e.target; const REG = new RegExp(_id); - while (node && i < 5) { + while (node && i < 20) { if (REG.test(node.className)) { return true; } @@ -131,6 +131,7 @@ export default class InputSelect extends Component { placeholder, labelKey, options, + onKeyPress, } = this.props; const { @@ -139,9 +140,10 @@ export default class InputSelect extends Component { filterVal, } = this.state; + const escapeRegExp = stringToGoIntoTheRegex => stringToGoIntoTheRegex.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); /* eslint-disable-line no-useless-escape */ let fiterList = options; if (filterVal) { - const REG = new RegExp(filterVal, 'i'); + const REG = new RegExp(escapeRegExp(filterVal), 'i'); fiterList = filter(options, o => REG.test(o[labelKey])); } const list = map(fiterList, o => ( @@ -171,7 +173,7 @@ export default class InputSelect extends Component {
- +
{list} @@ -195,6 +197,7 @@ InputSelect.defaultProps = { isLoading: false, onChange: () => {}, onLoadMore: () => {}, + onKeyPress: () => {}, }; InputSelect.propTypes = { @@ -205,6 +208,7 @@ InputSelect.propTypes = { placeholder: PT.string, onChange: PT.func, onLoadMore: PT.func, + onKeyPress: PT.func, hasMore: PT.bool, isLoading: PT.bool, disabled: PT.bool, diff --git a/src/shared/components/MMatchLeaderboard/index.jsx b/src/shared/components/MMatchLeaderboard/index.jsx index b2e3196944..a3dd95e5a9 100644 --- a/src/shared/components/MMatchLeaderboard/index.jsx +++ b/src/shared/components/MMatchLeaderboard/index.jsx @@ -21,9 +21,15 @@ import PT from 'prop-types'; import _ from 'lodash'; import React, { Component } from 'react'; import { fixStyle } from 'utils/contentful'; +import { getRatingColor } from 'utils/tc'; import cn from 'classnames'; import { Scrollbars } from 'react-custom-scrollbars'; -import './style.scss'; +import { config } from 'topcoder-react-utils'; +import { PrimaryButton } from 'topcoder-react-ui-kit'; +import tc from 'components/buttons/themed/tc.scss'; +import defaultStyles from './style.scss'; + +const DEFAULT_AVATAR_URL = 'https://images.ctfassets.net/b5f1djy59z3a/4PTwZVSf3W7qgs9WssqbVa/4c51312671a4b9acbdfd7f5e22320b62/default_avatar.svg'; export default class MMLeaderboard extends Component { constructor(props) { @@ -46,6 +52,8 @@ export default class MMLeaderboard extends Component { tableHeight, tableWidth, headerIndexCol, + theme, + challengeId, } = this.props; let { @@ -76,6 +84,73 @@ export default class MMLeaderboard extends Component { } const renderData = () => { + if (data.length && theme && theme === 'Podium') { + return ( +
+
+
+ {_.take(data, 2).map((member, indx) => ( +
+
+ {`Avatar +
{member.rank}
+
+
+ {member.createdBy} +

{member.score}

+
+
+ ))} +
+
+
+ {`Avatar +
{data[2].rank}
+
+
+ {data[2].createdBy} +

{data[2].score}

+
+
+
+
+
+ {_.slice(data, 3, 7).map(member => ( +
+ {member.rank}.  + {member.createdBy} + {member.score} +
+ ))} +
+ { + data.length > 7 && ( +
+ {_.slice(data, 7, 10).map(member => ( +
+ {member.rank}.  + {member.createdBy} + {member.score} +
+ ))} + { + data.length > 10 && ( + + See Full Leaderbord + + )} +
+ )} +
+
+ ); + } if (property) { if (data.length > 0 && data[0][property]) { if (typeof data[0][property] === 'string') { @@ -107,17 +182,17 @@ export default class MMLeaderboard extends Component { const header = cols => ( - { countRows && ({headerIndexCol}) } + { countRows && ({headerIndexCol}) } { cols.map((c) => { const name = c.headerName; const { styles } = c; return name ? ( - -
+ +
{ name }