diff --git a/components/ChallengeCard/Tooltips/Tooltip/Tooltip.jsx b/components/ChallengeCard/Tooltips/Tooltip/Tooltip.jsx index 7247b0158..20c83b147 100644 --- a/components/ChallengeCard/Tooltips/Tooltip/Tooltip.jsx +++ b/components/ChallengeCard/Tooltips/Tooltip/Tooltip.jsx @@ -47,6 +47,7 @@ class Tooltip extends React.Component { render() { return ( this.showTooltip()} onMouseEnter={() => this.showTooltip()} onMouseLeave={() => this.hideTooltip()} ref={(node) => { this.wrapper = node; }} diff --git a/components/ChallengeCard/Tooltips/UserAvatarTooltip/UserAvatarTooltip.jsx b/components/ChallengeCard/Tooltips/UserAvatarTooltip/UserAvatarTooltip.jsx index bdddf59a0..e7b3bc3b8 100644 --- a/components/ChallengeCard/Tooltips/UserAvatarTooltip/UserAvatarTooltip.jsx +++ b/components/ChallengeCard/Tooltips/UserAvatarTooltip/UserAvatarTooltip.jsx @@ -7,11 +7,12 @@ * the 'user' prop. */ -import React, { PropTypes as PT } from 'react'; -import moment from 'moment'; +import React, { Component, PropTypes as PT } from 'react'; +// import moment from 'moment'; import Tooltip from '../Tooltip'; import './UserAvatarTooltip.scss'; +const MOCK_PHOTO = 'https://acrobatusers.com/assets/images/template/author_generic.jpg'; /** * Renders the tooltip's content. * It includes: user profile picture, handle, his country and the TC registration @@ -21,17 +22,27 @@ import './UserAvatarTooltip.scss'; * efficient way to query those. */ function Tip(props) { - const joined = moment(props.user.memberSince).format('MMM YYYY'); + /* const joined = moment(props.user.memberSince).format('MMM YYYY'); const rating = props.user.ratingSummary.map(item => ( {item.name} {item.rating} - )); + ));*/ + const { photoLink } = props.user; + const src = photoLink.startsWith('https') ? photoLink : `https://topcoder.com/${photoLink}`; + return (
- User avatar + User avatar
{props.user.handle}
+ {/* Below block is commented out as it's not possible to get this information + // as of now.
{props.user.country}  / 257 wins  @@ -40,12 +51,13 @@ function Tip(props) {

Ratings

{rating} -
+
*/}
); } Tip.propTypes = { + handleError: PT.func.isRequired, user: PT.shape({ country: PT.string, handle: PT.string, @@ -58,13 +70,28 @@ Tip.propTypes = { /** * Renders the tooltip. */ -function UserAvatarTooltip(props) { - const tip = ; - return ( - - {props.children} - - ); +class UserAvatarTooltip extends Component { + constructor(props) { + super(props); + this.state = { + user: props.user, + }; + this.handleError = this.handleError.bind(this); + } + handleError() { + const user = this.state.user; + user.photoLink = MOCK_PHOTO; + this.setState({ user }); + } + + render() { + const tip = ; + return ( + + {this.props.children} + + ); + } } UserAvatarTooltip.defaultProps = { diff --git a/components/ChallengeStatus/ChallengeStatus.jsx b/components/ChallengeStatus/ChallengeStatus.jsx index 677f90194..306051e59 100644 --- a/components/ChallengeStatus/ChallengeStatus.jsx +++ b/components/ChallengeStatus/ChallengeStatus.jsx @@ -1,7 +1,7 @@ -/* eslint eqeqeq: 0 */ // The non-strict comparisons here may be necessary - -import React from 'react'; +import React, { Component, PropTypes } from 'react'; +import fetch from 'isomorphic-fetch'; import moment from 'moment'; +import _ from 'lodash'; import LeaderboardAvatar from '../LeaderboardAvatar/LeaderboardAvatar'; import ChallengeProgressBar from '../ChallengeProgressBar/ChallengeProgressBar'; import ProgressBarTooltip from '../ChallengeCard/Tooltips/ProgressBarTooltip'; @@ -13,38 +13,14 @@ import ForumIcon from '../Icons/ForumIcon'; import './ChallengeStatus.scss'; // Constants -const MM_LONGCONTEST = 'https://community.topcoder.com/longcontest/?module'; -const MM_REG = `${MM_LONGCONTEST}=ViewRegistrants&rd=`; -const MM_SUB = `${MM_LONGCONTEST}=ViewStandings&rd=`; const ID_LENGTH = 6; - -// Mock winners array -let MOCK_WINNERS = [ - { - handle: 'tc1', - position: 1, - }, - { - handle: 'tc2', - position: 2, - photoURL: 'https://acrobatusers.com/assets/images/template/author_generic.jpg', - }, - { - handle: 'tc3', - position: 3, - }, - { - handle: 'tc4', - position: 4, - }, -]; const MAX_VISIBLE_WINNERS = 3; -const FORUM_URL = 'https://apps.topcoder.com/forums/?module=Category&categoryID='; -const CHALLENGE_URL = 'https://www.topcoder.com/challenge-details/'; +const MOCK_PHOTO = 'https://acrobatusers.com/assets/images/template/author_generic.jpg'; const STALLED_MSG = 'Stalled'; const STALLED_TIME_LEFT_MSG = 'Challenge is currently on hold'; const FF_TIME_LEFT_MSG = 'Winner is working on fixes'; + const getTimeLeft = (date, currentPhase) => { if (!currentPhase || currentPhase === 'Stalled') { return { @@ -62,12 +38,12 @@ const getTimeLeft = (date, currentPhase) => { const d = duration.days(); const m = duration.minutes(); const late = (d < 0 || h < 0 || m < 0); - const suffix = h != 0 ? 'h' : 'min'; + const suffix = h !== 0 ? 'h' : 'min'; let text = ''; - if (d != 0) text += `${Math.abs(d)}d `; - if (h != 0) text += `${Math.abs(h)}`; - if (h != 0 && m != 0) text += ':'; - if (m != 0) text += `${Math.abs(m)}`; + if (d !== 0) text += `${Math.abs(d)}d `; + if (h !== 0) text += `${Math.abs(h)}`; + if (h !== 0 && m !== 0) text += ':'; + if (m !== 0) text += `${Math.abs(m)}`; text += suffix; if (late) { text = `Late by ${text}`; @@ -80,6 +56,58 @@ const getTimeLeft = (date, currentPhase) => { }; }; +function numRegistrantsTipText(number) { + switch (number) { + case 0: return 'No registrants'; + case 1: return '1 total registrant'; + default: return `${number} total registrants`; + } +} + +function numSubmissionsTipText(number) { + switch (number) { + case 0: return 'No submissions'; + case 1: return '1 total submission'; + default: return `${number} total submissions`; + } +} + +const getStatusPhase = (challenge) => { + switch (challenge.currentPhaseName) { + case 'Registration': { + if (challenge.checkpointSubmissionEndDate && !getTimeLeft(challenge.checkpointSubmissionEndDate, 'Checkpoint').late) { + return { + currentPhaseName: 'Checkpoint', + currentPhaseEndDate: challenge.checkpointSubmissionEndDate, + }; + } + + return { + currentPhaseName: 'Submission', + currentPhaseEndDate: challenge.submissionEndDate, + }; + } + case 'Submission': { + if (challenge.checkpointSubmissionEndDate && !getTimeLeft(challenge.checkpointSubmissionEndDate, 'Checkpoint').late) { + return { + currentPhaseName: 'Checkpoint', + currentPhaseEndDate: challenge.checkpointSubmissionEndDate, + }; + } + + return { + currentPhaseName: 'Submission', + currentPhaseEndDate: challenge.submissionEndDate, + }; + } + default: + return { + currentPhaseName: challenge.currentPhaseName, + currentPhaseEndDate: challenge.currentPhaseEndDate, + }; + } +}; + const getTimeToGo = (start, end) => { const percentageComplete = ( (moment() - moment(start)) / (moment(end) - moment(start)) @@ -87,24 +115,61 @@ const getTimeToGo = (start, end) => { return (Math.round(percentageComplete * 100) / 100); }; -const { object } = React.PropTypes; -function ChallengeStatus({ challenge, config, sampleWinnerProfile }) { - const lastItem = { - handle: `+${MOCK_WINNERS.length - MAX_VISIBLE_WINNERS}`, +/** + * Returns an user profile object as expected by the UserAvatarTooltip + * @param {String} handle + */ +function getSampleProfile(user) { + const { handle } = user; + return { + handle, + country: '', + memberSince: '', + photoLink: `i/m/${handle}.jpeg`, + ratingSummary: [], }; - MOCK_WINNERS = MOCK_WINNERS.slice(0, MAX_VISIBLE_WINNERS); - MOCK_WINNERS.push(lastItem); +} + + +class ChallengeStatus extends Component { + constructor(props) { + super(props); + const CHALLENGE_URL = `${props.config.MAIN_URL}/challenge-details/`; + const DS_CHALLENGE_URL = `https:${props.config.COMMUNITY_URL}/longcontest/stats/?module=ViewOverview&rd=`; + const FORUM_URL = `https:${props.config.FORUMS_APP_URL}/?module=Category&categoryID=`; + this.state = { + winners: '', + CHALLENGE_URL, + DS_CHALLENGE_URL, + FORUM_URL, + }; + this.handleHover = this.handleHover.bind(this); + this.getDevelopmentWinners = this.getDevelopmentWinners.bind(this); + this.getDesignWinners = this.getDesignWinners.bind(this); + this.registrantsLink = this.registrantsLink.bind(this); + } - const renderLeaderboard = MOCK_WINNERS.map(winner => ( -
- - - -
- )); + renderLeaderboard() { + const { challenge } = this.props; + const { DS_CHALLENGE_URL, CHALLENGE_URL } = this.state; + const { challengeId, challengeCommunity } = challenge; + const challengeURL = challengeCommunity.toLowerCase() === 'data' ? DS_CHALLENGE_URL : CHALLENGE_URL; + const leaderboard = this.state.winners && this.state.winners.map(winner => ( +
+ + + +
+ )); + return leaderboard || ( + + Winners + ); + } - const renderRegisterButton = () => { + renderRegisterButton() { + const { challenge } = this.props; const lng = getTimeLeft( challenge.registrationEndDate || challenge.submissionEndDate, challenge.currentPhaseName, @@ -125,24 +190,10 @@ function ChallengeStatus({ challenge, config, sampleWinnerProfile }) { to register ); - }; - - function numRegistrantsTipText(number) { - switch (number) { - case 0: return 'No registrants'; - case 1: return '1 total registrant'; - default: return `${number} total registrants`; - } } - function numSubmissionsTipText(number) { - switch (number) { - case 0: return 'No submissions'; - case 1: return '1 total submission'; - default: return `${number} total submissions`; - } - } - const registrantsLink = (registrantsChallenge, type) => { + registrantsLink(registrantsChallenge, type) { + const { CHALLENGE_URL } = this.state; if (registrantsChallenge.track === 'DATA_SCIENCE') { const id = `${registrantsChallenge.challengeId}`; if (id.length < ID_LENGTH) { @@ -151,72 +202,43 @@ function ChallengeStatus({ challenge, config, sampleWinnerProfile }) { return `${CHALLENGE_URL}${registrantsChallenge.challengeId}/?type=develop#viewRegistrant`; } return `${CHALLENGE_URL}${registrantsChallenge.challengeId}/?type=${registrantsChallenge.track.toLowerCase()}#viewRegistrant`; - }; - const getStatusPhase = () => { - switch (challenge.currentPhaseName) { - case 'Registration': { - if (challenge.checkpointSubmissionEndDate && !getTimeLeft(challenge.checkpointSubmissionEndDate, 'Checkpoint').late) { - return { - currentPhaseName: 'Checkpoint', - currentPhaseEndDate: challenge.checkpointSubmissionEndDate, - }; - } - - return { - currentPhaseName: 'Submission', - currentPhaseEndDate: challenge.submissionEndDate, - }; - } - case 'Submission': { - if (challenge.checkpointSubmissionEndDate && !getTimeLeft(challenge.checkpointSubmissionEndDate, 'Checkpoint').late) { - return { - currentPhaseName: 'Checkpoint', - currentPhaseEndDate: challenge.checkpointSubmissionEndDate, - }; - } - - return { - currentPhaseName: 'Submission', - currentPhaseEndDate: challenge.submissionEndDate, - }; - } - default: - return { - currentPhaseName: challenge.currentPhaseName, - currentPhaseEndDate: challenge.currentPhaseEndDate, - }; - } - }; + } - const activeChallenge = () => ( -
- - { - challenge.currentPhaseName - ? getStatusPhase().currentPhaseName - : STALLED_MSG - } - - - - - - - {challenge.numRegistrants} - - - - - - - {challenge.numSubmissions} - - + activeChallenge() { + const { challenge, config } = this.props; + const { FORUM_URL } = this.state; + const MM_LONGCONTEST = `https:${config.COMMUNITY_URL}/longcontest/?module`; + const MM_REG = `${MM_LONGCONTEST}=ViewRegistrants&rd=`; + const MM_SUB = `${MM_LONGCONTEST}=ViewStandings&rd=`; + return ( +
+ + { + challenge.currentPhaseName + ? getStatusPhase(challenge).currentPhaseName + : STALLED_MSG + } - { + + + + + + {challenge.numRegistrants} + + + + + + + {challenge.numSubmissions} + + + + { challenge.myChallenge && @@ -224,9 +246,9 @@ function ChallengeStatus({ challenge, config, sampleWinnerProfile }) { } - - - { + + + { challenge.status === 'Active' ?
{ getTimeLeft( - getStatusPhase().currentPhaseEndDate, - getStatusPhase().currentPhaseName, + getStatusPhase(challenge).currentPhaseEndDate, + getStatusPhase(challenge).currentPhaseName, ).text }
@@ -256,46 +278,133 @@ function ChallengeStatus({ challenge, config, sampleWinnerProfile }) { : } - - {challenge.registrationOpen === 'Yes' && renderRegisterButton()} -
+
+ {challenge.registrationOpen === 'Yes' && this.renderRegisterButton()} +
); + } - const completedChallenge = () => ( -
- {renderLeaderboard} - - - - - {challenge.numRegistrants} - - - - - - - {challenge.numSubmissions} - - - - { + completedChallenge() { + const { challenge } = this.props; + const { CHALLENGE_URL, FORUM_URL } = this.state; + return ( +
+ {this.renderLeaderboard()} + + + + + {challenge.numRegistrants} + + + + + + + {challenge.numSubmissions} + + + + { challenge.myChallenge && } - -
+
+
); + } + + getDevelopmentWinners(challengeId) { + return new Promise((resolve, reject) => { + fetch(`${this.props.config.API_URL_V2}/develop/challenges/${challengeId}`) + .then(res => res.json()) + .then((data) => { + let winners = data.submissions.filter(sub => sub.placement) + .map(winner => ({ + handle: winner.handle, + position: winner.placement, + photoURL: MOCK_PHOTO, + })); + winners = _.uniqWith(winners, _.isEqual); + if (winners.length > MAX_VISIBLE_WINNERS) { + const lastItem = { + handle: `+${winners.length - MAX_VISIBLE_WINNERS}`, + }; + winners = winners.slice(0, MAX_VISIBLE_WINNERS); + winners.push(lastItem); + } + resolve(winners); + }) + .catch(err => reject(err)); + }); + } + + getDesignWinners(challengeId) { + return new Promise((resolve, reject) => { + fetch(`${this.props.config.API_URL_V2}/design/challenges/result/${challengeId}`) + .then(res => res.json()) + .then((data) => { + let winners = data.results.filter(sub => sub.placement) + .map(winner => ({ + handle: winner.handle, + position: winner.placement, + photoURL: MOCK_PHOTO, + })); + winners = _.uniqWith(winners, _.isEqual); + if (winners.length > MAX_VISIBLE_WINNERS) { + const lastItem = { + handle: `+${winners.length - MAX_VISIBLE_WINNERS}`, + }; + winners = winners.slice(0, MAX_VISIBLE_WINNERS); + winners.push(lastItem); + } + resolve(winners); + }) + .catch(err => reject(err)); + }); + } + - const status = challenge.status === 'Completed' ? 'completed' : ''; + getWinners(challengeType, challengeId) { + switch (challengeType) { + case 'develop': + return this.getDevelopmentWinners(challengeId); + case 'design': + return this.getDesignWinners(challengeId); + default: + return this.getDevelopmentWinners(challengeId); + } + } - return ( -
- {challenge.status === 'Completed' ? completedChallenge() : activeChallenge()} -
- ); + /** + * Get the list of winners when the user hovers + * over the status + */ + handleHover() { + if (!this.state.winners) { + const { challenge } = this.props; + const { challengeId, challengeCommunity } = challenge; + + // We don't have the API for data science challenge + if (challengeCommunity.toLowerCase() === 'data') { + return; + } + const results = this.getWinners(challengeCommunity.toLowerCase(), challengeId); + results.then(winners => this.setState({ winners })); + } + } + + render() { + const { challenge } = this.props; + const status = challenge.status === 'Completed' ? 'completed' : ''; + return ( +
+ {challenge.status === 'Completed' ? this.completedChallenge() : this.activeChallenge()} +
+ ); + } } ChallengeStatus.defaultProps = { @@ -305,9 +414,8 @@ ChallengeStatus.defaultProps = { }; ChallengeStatus.propTypes = { - challenge: object, - config: object, - sampleWinnerProfile: object, + challenge: PropTypes.object, + config: PropTypes.object, }; export default ChallengeStatus; diff --git a/components/ChallengeStatus/ChallengeStatus.scss b/components/ChallengeStatus/ChallengeStatus.scss index bd3127a95..f64bf755b 100644 --- a/components/ChallengeStatus/ChallengeStatus.scss +++ b/components/ChallengeStatus/ChallengeStatus.scss @@ -31,6 +31,7 @@ $status-radius-4: $corner-radius * 2; > div { display: flex; align-items: center; + height: 30px; @include xs-to-md { display: flex; align-items: center; diff --git a/components/SideBarFilters/FilterItems/FilterItems.jsx b/components/SideBarFilters/FilterItems/FilterItems.jsx index f1c641cde..2df48226b 100644 --- a/components/SideBarFilters/FilterItems/FilterItems.jsx +++ b/components/SideBarFilters/FilterItems/FilterItems.jsx @@ -75,6 +75,7 @@ function FilterItem(props) { FilterItem.defaultProps = { highlighted: false, onClick: _.noop, + myFilter: false, }; FilterItem.propTypes = {