Skip to content

Commit dd3826e

Browse files
Merge pull request #5912 from topcoder-platform/thrive-rss
Thrive rss
2 parents 61cafe5 + eaabe32 commit dd3826e

File tree

13 files changed

+359
-15
lines changed

13 files changed

+359
-15
lines changed

.circleci/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,14 +357,14 @@ workflows:
357357
filters:
358358
branches:
359359
only:
360-
- free
360+
- thrive-rss
361361
# This is beta env for production soft releases
362362
- "build-prod-beta":
363363
context : org-global
364364
filters:
365365
branches:
366366
only:
367-
- free
367+
- mm-leaderboard-theme
368368
# This is stage env for production QA releases
369369
- "build-prod-staging":
370370
context : org-global

config/default.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ module.exports = {
110110
HOME: '/home',
111111
BLOG: 'https://www.topcoder-dev.com/blog',
112112
BLOG_FEED: 'https://www.topcoder.com/blog/feed/',
113+
THRIVE_FEED: 'https://topcoder-dev.com/api/feeds/thrive',
113114
COMMUNITY: 'https://community.topcoder-dev.com',
114115
FORUMS: 'https://apps.topcoder-dev.com/forums',
115116
FORUMS_VANILLA: 'https://vanilla.topcoder-dev.com',

config/production.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module.exports = {
5858
CS: 'https://cs.topcoder.com',
5959
},
6060
EMAIL_VERIFY_URL: 'http://www.topcoder.com/settings/account/changeEmail',
61+
THRIVE_FEED: 'https://topcoder.com/api/feeds/thrive',
6162
},
6263
/* Filestack configuration for uploading Submissions
6364
* These are for the production back end */

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
"redux-promise": "^0.6.0",
141141
"request-ip": "^2.0.2",
142142
"require-context": "^1.1.0",
143+
"rss": "^1.2.2",
143144
"rss-parser": "^3.12.0",
144145
"serialize-javascript": "^2.1.1",
145146
"serve-favicon": "^2.5.0",

src/server/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import mmLeaderboardRouter from './routes/mmLeaderboard';
3131
import growsurfRouter from './routes/growsurf';
3232
import gSheetsRouter from './routes/gSheet';
3333
import blogRouter from './routes/blog';
34+
import feedsRouter from './routes/feeds';
3435

3536
/* Dome API for topcoder communities */
3637
import tcCommunitiesDemoApi from './tc-communities';
@@ -143,6 +144,7 @@ async function onExpressJsSetup(server) {
143144
server.use('/api/growsurf', growsurfRouter);
144145
server.use('/api/gsheets', gSheetsRouter);
145146
server.use('/api/blog', blogRouter);
147+
server.use('/api/feeds', feedsRouter);
146148

147149
// serve demo api
148150
server.use(

src/server/routes/feeds.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* The routes that expose assets and content from Contentful CMS to the CDN.
3+
*/
4+
5+
import express from 'express';
6+
import RSS from 'rss';
7+
import ReactDOMServer from 'react-dom/server';
8+
import md from 'utils/markdown';
9+
import {
10+
getService,
11+
} from '../services/contentful';
12+
13+
const cors = require('cors');
14+
15+
const routes = express.Router();
16+
17+
// Enables CORS on those routes according config above
18+
// ToDo configure CORS for set of our trusted domains
19+
routes.use(cors());
20+
routes.options('*', cors());
21+
22+
routes.get('/thrive', async (req, res, next) => {
23+
try {
24+
const data = await getService('EDU', 'master', true).queryEntries({
25+
content_type: 'article',
26+
limit: 20,
27+
order: '-sys.createdAt',
28+
include: 2,
29+
});
30+
const feed = new RSS({
31+
title: 'Topcoder Thrive',
32+
description: 'Tutorials And Workshops That Matter | Thrive | Topcoder',
33+
feed_url: 'https://topcoder.com/api/feeds/thrive',
34+
site_url: 'https://topcoder.com/thrive',
35+
image_url: 'https://www.topcoder.com/wp-content/uploads/2020/05/cropped-TC-Icon-32x32.png',
36+
docs: 'https://www.topcoder.com/thrive/tracks?track=Topcoder',
37+
webMaster: '<kiril@wearetopcoder.com> Kiril Kartunov',
38+
copyright: '2021 - today, Topcoder',
39+
language: 'en',
40+
categories: ['Competitive Programming', 'Data Science', 'Design', 'Development', 'QA', 'Gig work', 'Topcoder'],
41+
ttl: '60',
42+
});
43+
if (data && data.total) {
44+
data.items.forEach((entry) => {
45+
feed.item({
46+
title: entry.fields.title,
47+
description: ReactDOMServer.renderToString(md(entry.fields.content)),
48+
url: `https://topcoder.com/thrive/articles/${entry.fields.slug || encodeURIComponent(entry.fields.title)}?utm_source=community&utm_campaign=thrive-feed&utm_medium=promotion`,
49+
date: entry.fields.creationDate,
50+
categories: entry.fields.tags,
51+
author: entry.fields.contentAuthor[0].fields.name,
52+
});
53+
});
54+
}
55+
res.set('Content-Type', 'application/rss+xml');
56+
res.send(feed.xml({ indent: true }));
57+
} catch (e) {
58+
next(e);
59+
}
60+
});
61+
62+
export default routes;

src/shared/actions/mmLeaderboard.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { redux } from 'topcoder-react-utils';
1+
import { redux, config } from 'topcoder-react-utils';
22
import Service from 'services/mmLeaderboard';
33
import _ from 'lodash';
44

5+
56
/**
67
* Fetch init
78
*/
@@ -34,11 +35,22 @@ async function getMMLeaderboardDone(id) {
3435
score: scores && scores.length ? scores[0].score : '...',
3536
});
3637
});
37-
data = _.orderBy(data, [d => (Number(d.score) ? Number(d.score) : 0)], ['desc']).map((r, i) => ({
38+
data = _.orderBy(data, [d => (Number(d.score) ? Number(d.score) : 0), d => new Date(d.updated) - new Date()], ['desc']).map((r, i) => ({
3839
...r,
3940
rank: i + 1,
4041
score: r.score % 1 ? Number(r.score).toFixed(5) : r.score,
4142
}));
43+
// Fetch member photos and rating for top 10
44+
const results = await Promise.all(
45+
_.take(data, 10).map(d => fetch(`${config.API.V5}/members/${d.createdBy}`)),
46+
);
47+
const memberData = await Promise.all(results.map(r => r.json()));
48+
// merge with data
49+
// eslint-disable-next-line array-callback-return
50+
memberData.map((member, indx) => {
51+
data[indx].photoUrl = member.photoURL;
52+
data[indx].rating = member.maxRating && member.maxRating.rating;
53+
});
4254
}
4355
return {
4456
id,

src/shared/components/MMatchLeaderboard/index.jsx

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@ import PT from 'prop-types';
2121
import _ from 'lodash';
2222
import React, { Component } from 'react';
2323
import { fixStyle } from 'utils/contentful';
24+
import { getRatingColor } from 'utils/tc';
2425
import cn from 'classnames';
2526
import { Scrollbars } from 'react-custom-scrollbars';
26-
import './style.scss';
27+
import { config } from 'topcoder-react-utils';
28+
import { PrimaryButton } from 'topcoder-react-ui-kit';
29+
import tc from 'components/buttons/themed/tc.scss';
30+
import defaultStyles from './style.scss';
31+
32+
const DEFAULT_AVATAR_URL = 'https://images.ctfassets.net/b5f1djy59z3a/4PTwZVSf3W7qgs9WssqbVa/4c51312671a4b9acbdfd7f5e22320b62/default_avatar.svg';
2733

2834
export default class MMLeaderboard extends Component {
2935
constructor(props) {
@@ -46,6 +52,8 @@ export default class MMLeaderboard extends Component {
4652
tableHeight,
4753
tableWidth,
4854
headerIndexCol,
55+
theme,
56+
challengeId,
4957
} = this.props;
5058

5159
let {
@@ -76,6 +84,73 @@ export default class MMLeaderboard extends Component {
7684
}
7785

7886
const renderData = () => {
87+
if (data.length && theme && theme === 'Podium') {
88+
return (
89+
<div className={defaultStyles.podiumTheme}>
90+
<div className={defaultStyles.top3}>
91+
<div className={defaultStyles.swapper}>
92+
{_.take(data, 2).map((member, indx) => (
93+
<div className={defaultStyles[`topMember${indx + 1}`]}>
94+
<div className={defaultStyles.topMemberPhotoAndPlace}>
95+
<img src={member.photoUrl || DEFAULT_AVATAR_URL} className={defaultStyles.topMemberPhoto} alt={`Avatar of ${member.createdBy}`} />
96+
<div className={defaultStyles[`topMemberRank${indx + 1}`]}>{member.rank}</div>
97+
</div>
98+
<div className={defaultStyles.topMemberLink}>
99+
<a href={`${config.URL.BASE}/members/${member.createdBy}`} target="_blank" rel="noreferrer" style={{ color: getRatingColor(member.rating) }}>{member.createdBy}</a>
100+
<p className={defaultStyles.topMemberScore}>{member.score}</p>
101+
</div>
102+
</div>
103+
))}
104+
</div>
105+
<div className={defaultStyles.topMember3}>
106+
<div className={defaultStyles.topMemberPhotoAndPlace}>
107+
<img src={data[2].photoUrl || DEFAULT_AVATAR_URL} className={defaultStyles.topMemberPhoto} alt={`Avatar of ${data[2].createdBy}`} />
108+
<div className={defaultStyles.topMemberRank3}>{data[2].rank}</div>
109+
</div>
110+
<div className={defaultStyles.topMemberLink}>
111+
<a href={`${config.URL.BASE}/members/${data[2].createdBy}`} target="_blank" rel="noreferrer" style={{ color: getRatingColor(data[2].rating) }}>{data[2].createdBy}</a>
112+
<p className={defaultStyles.topMemberScore}>{data[2].score}</p>
113+
</div>
114+
</div>
115+
</div>
116+
<div className={defaultStyles.followers}>
117+
<div className={defaultStyles.followersLeft}>
118+
{_.slice(data, 3, 7).map(member => (
119+
<div className={defaultStyles.follower}>
120+
<span>{member.rank}.&nbsp;</span>
121+
<a href={`${config.URL.BASE}/members/${member.createdBy}`} target="_blank" rel="noreferrer" style={{ color: getRatingColor(member.rating) }}>{member.createdBy}</a>
122+
<span className={defaultStyles.followerScore}>{member.score}</span>
123+
</div>
124+
))}
125+
</div>
126+
{
127+
data.length > 7 && (
128+
<div className={defaultStyles.followersRight}>
129+
{_.slice(data, 7, 10).map(member => (
130+
<div className={defaultStyles.follower}>
131+
<span>{member.rank}.&nbsp;</span>
132+
<a href={`${config.URL.BASE}/members/${member.createdBy}`} target="_blank" rel="noreferrer" style={{ color: getRatingColor(member.rating) }}>{member.createdBy}</a>
133+
<span className={defaultStyles.followerScore}>{member.score}</span>
134+
</div>
135+
))}
136+
{
137+
data.length > 10 && (
138+
<PrimaryButton
139+
to={`${config.URL.BASE}/challenges/${challengeId}?tab=submissions`}
140+
openNewTab
141+
theme={{
142+
button: tc['primary-green-sm'],
143+
}}
144+
>
145+
See Full Leaderbord
146+
</PrimaryButton>
147+
)}
148+
</div>
149+
)}
150+
</div>
151+
</div>
152+
);
153+
}
79154
if (property) {
80155
if (data.length > 0 && data[0][property]) {
81156
if (typeof data[0][property] === 'string') {
@@ -107,17 +182,17 @@ export default class MMLeaderboard extends Component {
107182

108183
const header = cols => (
109184
<tr>
110-
{ countRows && (<th styleName="header-cell"><span>{headerIndexCol}</span></th>) }
185+
{ countRows && (<th className={defaultStyles['header-cell']}><span>{headerIndexCol}</span></th>) }
111186
{
112187
cols.map((c) => {
113188
const name = c.headerName;
114189
const { styles } = c;
115190
return name ? (
116-
<th key={name} style={fixStyle(styles)} styleName="header-cell">
117-
<div styleName="header-table-content">
191+
<th key={name} style={fixStyle(styles)} className={defaultStyles['header-cell']}>
192+
<div className={defaultStyles['header-table-content']}>
118193
<span>{ name }</span>
119194
<button
120-
styleName="sort-container"
195+
className={defaultStyles['sort-container']}
121196
onClick={() => {
122197
if (!sortParam.field || sortParam.field !== c.property) {
123198
sortParam.field = c.property;
@@ -147,7 +222,7 @@ export default class MMLeaderboard extends Component {
147222
);
148223
const bodyRow = (record, cols, i) => (
149224
<tr key={Object.values(record)}>
150-
{ (countRows && (limit <= 0 || i < limit)) ? <td styleName="body-row"> {i + 1} </td> : ' ' }
225+
{ (countRows && (limit <= 0 || i < limit)) ? <td className={defaultStyles['body-row']}> {i + 1} </td> : ' ' }
151226
{
152227
cols.map((c) => {
153228
const prop = c.property;
@@ -169,8 +244,8 @@ export default class MMLeaderboard extends Component {
169244
}
170245
}
171246
return value ? (
172-
<td key={record[prop]} style={fixStyle(styles)} title={value} styleName="body-row">
173-
{memberLinks ? (<a styleName="handle-link" href={`${window.origin}/members/${value}`} target={`${_.includes(window.origin, 'www') ? '_self' : '_blank'}`}>{value}</a>) : value}
247+
<td key={record[prop]} style={fixStyle(styles)} title={value} className={defaultStyles['body-row']}>
248+
{memberLinks ? (<a className={defaultStyles['handle-link']} href={`${window.origin}/members/${value}`} target={`${_.includes(window.origin, 'www') ? '_self' : '_blank'}`}>{value}</a>) : value}
174249
</td>
175250
) : null;
176251
})
@@ -179,10 +254,10 @@ export default class MMLeaderboard extends Component {
179254
);
180255
return (
181256
<Scrollbars
182-
styleName="component-container"
257+
className={defaultStyles['component-container']}
183258
style={{ height: tableHeight, width: tableWidth }}
184259
>
185-
<table styleName="table-container">
260+
<table className={defaultStyles['table-container']}>
186261
<thead>
187262
{ header(columns) }
188263
</thead>
@@ -221,7 +296,7 @@ export default class MMLeaderboard extends Component {
221296
};
222297
return (
223298
<React.Fragment>
224-
{ data.length ? renderData() : <h4 styleName="no-data-title">No data available yet.</h4> }
299+
{ data.length ? renderData() : <h4 className={defaultStyles['no-data-title']}>No data available yet.</h4> }
225300
</React.Fragment>
226301
);
227302
}
@@ -236,6 +311,7 @@ MMLeaderboard.defaultProps = {
236311
tableHeight: '100%',
237312
tableWidth: '100%',
238313
headerIndexCol: '',
314+
theme: null,
239315
};
240316

241317
MMLeaderboard.propTypes = {
@@ -257,4 +333,6 @@ MMLeaderboard.propTypes = {
257333
PT.func,
258334
]),
259335
headerIndexCol: PT.string,
336+
theme: PT.string,
337+
challengeId: PT.string.isRequired,
260338
};

0 commit comments

Comments
 (0)