From 5cd686eed736ee3a14f0a5384beb61e9b9d037b9 Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Tue, 18 Aug 2020 04:56:05 +0300 Subject: [PATCH 1/2] fix(challenge-listing): sorting by dates & phase displaying * Add `phaseStartDate` and `phaseEndDate` helper functions at `challenge-detail/helper` to get the correct start/end date of a phase. * `TIME_TO_REGISTER`, `TIME_TO_SUBMIT` and `MOST_RECENT` sorting functions updated to reflect above change. * `ProgressBarTooltip` and `ChallengeCard/Status` updated to display correct phase start/end dates. Addresses topcoder-platform/community-app#4715, topcoder-platform/community-app#4716 --- .../ChallengeCard/Status/index.jsx | 4 +- .../Tooltips/ProgressBarTooltip/index.jsx | 14 ++++--- src/shared/utils/challenge-detail/helper.jsx | 34 ++++++++++++++- src/shared/utils/challenge-listing/sort.js | 41 +++++++++++++++---- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index 3cb06f6c46..921aa21076 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -7,7 +7,7 @@ import { config, Link } from 'topcoder-react-utils'; import { TABS as DETAIL_TABS } from 'actions/page/challenge-details'; import 'moment-duration-format'; import { - getTimeLeft, + getTimeLeft, phaseEndDate, } from 'utils/challenge-detail/helper'; import ChallengeProgressBar from '../../ChallengeProgressBar'; @@ -271,7 +271,7 @@ export default function ChallengeStatus(props) {
{getTimeLeft(statusPhase, 'to go').text} diff --git a/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx b/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx index 675e181bc4..ef83cee06e 100644 --- a/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx +++ b/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx @@ -18,6 +18,7 @@ import React from 'react'; import PT from 'prop-types'; import Tooltip from 'components/Tooltip'; import LoaderIcon from '../../../Loader/Loader'; +import { phaseStartDate, phaseEndDate } from '../../../../utils/challenge-detail/helper'; import './style.scss'; const getDate = date => moment(date).format('MMM DD'); @@ -95,31 +96,32 @@ function Tip(props) { if (!c || _.isEmpty(c)) return
; const allPhases = c.phases || []; - const endPhaseDate = Math.max(...allPhases.map(d => new Date(d.scheduledEndDate))); + const endPhaseDate = Math.max(...allPhases.map(d => phaseEndDate(d))); const registrationPhase = allPhases.find(phase => phase.name === 'Registration'); const submissionPhase = allPhases.find(phase => phase.name === 'Submission'); + const checkpointPhase = allPhases.find(phase => phase.name === 'Checkpoint Submission'); if (registrationPhase) { steps.push({ - date: new Date(registrationPhase.scheduledStartDate), + date: phaseStartDate(registrationPhase), name: 'Start', }); } - if (c.checkpointSubmissionEndDate) { + if (checkpointPhase) { steps.push({ - date: new Date(c.checkpointSubmissionEndDate), + date: phaseEndDate(checkpointPhase), name: 'Checkpoint', }); } const iterativeReviewPhase = allPhases.find(phase => phase.isOpen && phase.name === 'Iterative Review'); if (iterativeReviewPhase) { steps.push({ - date: new Date(iterativeReviewPhase.scheduledEndDate), + date: phaseEndDate(iterativeReviewPhase), name: 'Iterative Review', }); } else if (submissionPhase) { steps.push({ - date: new Date(submissionPhase.scheduledEndDate), + date: phaseEndDate(submissionPhase), name: 'Submission', }); } diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx index 2a6fb4b8be..9e78f055a0 100644 --- a/src/shared/utils/challenge-detail/helper.jsx +++ b/src/shared/utils/challenge-detail/helper.jsx @@ -32,6 +32,36 @@ export function getChallengeTypeAbbr(track, challengeTypes) { return null; } +/** + * Returns phase's end date. + * @param {Object} phase + * @return {Date} + */ +export function phaseEndDate(phase) { + // Case 1: phase is still open. take the `scheduledEndDate` + // Case 2: phase is not open but `scheduledStartDate` is a future date. + // This means phase is not yet started. So take the `scheduledEndDate` + if (phase.isOpen || moment(phase.scheduledStartDate).isAfter()) { + return new Date(phase.scheduledEndDate); + } + // for other cases, take the `actualEndDate` as phase is already closed + return new Date(phase.actualEndDate); +} + +/** + * Returns phase's start date. + * @param {Object} phase + * @return {Date} + */ +export function phaseStartDate(phase) { + // Case 1: Phase is not yet started. take the `scheduledStartDate` + if (phase.isOpen !== true && moment(phase.scheduledStartDate).isAfter()) { + return new Date(phase.scheduledStartDate); + } + // For all other cases, take the `actualStartDate` as phase is already started + return new Date(phase.actualStartDate); +} + /** * Get end date * @param {Object} challenge challenge info @@ -42,7 +72,7 @@ export function getEndDate(challenge) { if (type === 'First2Finish' && challenge.status === 'Completed') { phases = challenge.phases.filter(p => p.phaseType === 'Iterative Review' && p.phaseStatus === 'Closed'); } - const endPhaseDate = Math.max(...phases.map(d => new Date(d.scheduledEndDate))); + const endPhaseDate = Math.max(...phases.map(d => phaseEndDate(d))); return moment(endPhaseDate).format('MMM DD'); } @@ -65,7 +95,7 @@ export function getTimeLeft( return { late: false, text: FF_TIME_LEFT_MSG }; } - let time = moment(phase.scheduledEndDate).diff(); + let time = moment(phaseEndDate(phase)).diff(); const late = time < 0; if (late) time = -time; diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js index 4ef586d1f8..d1990931b9 100644 --- a/src/shared/utils/challenge-listing/sort.js +++ b/src/shared/utils/challenge-listing/sort.js @@ -4,6 +4,7 @@ import moment from 'moment'; import { find, sumBy } from 'lodash'; +import { phaseStartDate, phaseEndDate } from '../challenge-detail/helper'; export const SORTS = { CURRENT_PHASE: 'current-phase', @@ -27,12 +28,14 @@ export default { [SORTS.MOST_RECENT]: { func: (a, b) => { const getRegistrationStartDate = (challenge) => { + // extract the registration phase from `challenge.phases`, + // as `challenge.registrationStartDate` returned from API is not reliable const registrationPhase = find(challenge.phases, p => p.name === 'Registration'); - return registrationPhase.actualStartDate || registrationPhase.scheduledStartDate; + return moment(phaseStartDate(registrationPhase)); }; const aRegistrationStartDate = getRegistrationStartDate(a); const bRegistrationStartDate = getRegistrationStartDate(b); - return moment(bRegistrationStartDate).diff(aRegistrationStartDate); + return bRegistrationStartDate.diff(aRegistrationStartDate); }, name: 'Most recent', }, @@ -51,12 +54,20 @@ export default { [SORTS.TIME_TO_REGISTER]: { func: (a, b) => { const getRegistrationEndDate = (challenge) => { + // extract the registration phase from `challenge.phases`, + // as `challenge.registrationEndDate` returned from API is not reliable const registrationPhase = find(challenge.phases, p => p.name === 'Registration'); - return registrationPhase.actualEndDate || registrationPhase.scheduledEndDate; + const submissionPhase = find(challenge.phases, p => p.name === 'Submission'); + // case 1: registration phase exists + if (registrationPhase) { + return moment(phaseEndDate(registrationPhase)); + } + // case 2: registration phase doesn't exist. Take submission phase instead. + return moment(phaseEndDate(submissionPhase)); }; - const aDate = moment(getRegistrationEndDate(a) || a.submissionEndTimestamp); - const bDate = moment(getRegistrationEndDate(b) || b.submissionEndTimestamp); + const aDate = getRegistrationEndDate(a); + const bDate = getRegistrationEndDate(b); if (aDate.isBefore() && bDate.isAfter()) return 1; if (aDate.isAfter() && bDate.isBefore()) return -1; @@ -68,11 +79,23 @@ export default { }, [SORTS.TIME_TO_SUBMIT]: { func: (a, b) => { - function nextSubEndDate(o) { - if (o.checkpointSubmissionEndDate && moment(o.checkpointSubmissionEndDate).isAfter()) { - return moment(o.checkpointSubmissionEndDate); + function nextSubEndDate(challenge) { + // extract the submission and checkpoint (if any) phases from `challenge.phases`, + // as `challenge.submissionEndDate` returned from API is not reliable + const checkpointPhase = find(challenge.phases, p => p.name === 'Checkpoint Submission'); + const submissionPhase = find(challenge.phases, p => p.name === 'Submission'); + // Case 1: challenge has checkpoint submission phase + if (!!checkpointPhase === true) { + // Case 1.1: checkpoint submission phase is still open. + // then take the `scheduledEndDate` of this phase. + // Case 1.2: checkpoint submission phase is closed + // but its `scheduledStartDate` is a future date. + // This means this phase is not yet started. Take the `scheduledEndDate` of this phase. + if (checkpointPhase.isOpen || moment(checkpointPhase.scheduledStartDate).isAfter()) { + return moment(checkpointPhase.scheduledEndDate); + } } - return moment(o.submissionEndTimestamp); + return moment(phaseEndDate(submissionPhase)); } const aDate = nextSubEndDate(a); From f8c7b4c73657ac15e78afc033f31377b5da62b35 Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Fri, 21 Aug 2020 01:54:36 +0300 Subject: [PATCH 2/2] fix(challenge-listing): fix failing test --- .../ChallengeCard/Status/index.jsx | 3 +- .../Tooltips/ProgressBarTooltip/index.jsx | 2 +- src/shared/utils/challenge-detail/helper.jsx | 31 +------------------ src/shared/utils/challenge-listing/helper.js | 31 +++++++++++++++++++ src/shared/utils/challenge-listing/sort.js | 20 +++++++----- 5 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 src/shared/utils/challenge-listing/helper.js diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index 921aa21076..d3315863aa 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -6,8 +6,9 @@ import LeaderboardAvatar from 'components/challenge-listing/LeaderboardAvatar'; import { config, Link } from 'topcoder-react-utils'; import { TABS as DETAIL_TABS } from 'actions/page/challenge-details'; import 'moment-duration-format'; +import { phaseEndDate } from 'utils/challenge-listing/helper'; import { - getTimeLeft, phaseEndDate, + getTimeLeft, } from 'utils/challenge-detail/helper'; import ChallengeProgressBar from '../../ChallengeProgressBar'; diff --git a/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx b/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx index ef83cee06e..432afcde8b 100644 --- a/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx +++ b/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx @@ -17,8 +17,8 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; import Tooltip from 'components/Tooltip'; +import { phaseStartDate, phaseEndDate } from 'utils/challenge-listing/helper'; import LoaderIcon from '../../../Loader/Loader'; -import { phaseStartDate, phaseEndDate } from '../../../../utils/challenge-detail/helper'; import './style.scss'; const getDate = date => moment(date).format('MMM DD'); diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx index 9e78f055a0..38b8317f90 100644 --- a/src/shared/utils/challenge-detail/helper.jsx +++ b/src/shared/utils/challenge-detail/helper.jsx @@ -9,6 +9,7 @@ import { challenge as challengeUtils } from 'topcoder-react-lib'; import { config } from 'topcoder-react-utils'; import Prize from 'components/challenge-listing/ChallengeCard/Prize'; import { BUCKETS, getBuckets } from 'utils/challenge-listing/buckets'; +import { phaseEndDate } from 'utils/challenge-listing/helper'; const Filter = challengeUtils.filter; @@ -32,36 +33,6 @@ export function getChallengeTypeAbbr(track, challengeTypes) { return null; } -/** - * Returns phase's end date. - * @param {Object} phase - * @return {Date} - */ -export function phaseEndDate(phase) { - // Case 1: phase is still open. take the `scheduledEndDate` - // Case 2: phase is not open but `scheduledStartDate` is a future date. - // This means phase is not yet started. So take the `scheduledEndDate` - if (phase.isOpen || moment(phase.scheduledStartDate).isAfter()) { - return new Date(phase.scheduledEndDate); - } - // for other cases, take the `actualEndDate` as phase is already closed - return new Date(phase.actualEndDate); -} - -/** - * Returns phase's start date. - * @param {Object} phase - * @return {Date} - */ -export function phaseStartDate(phase) { - // Case 1: Phase is not yet started. take the `scheduledStartDate` - if (phase.isOpen !== true && moment(phase.scheduledStartDate).isAfter()) { - return new Date(phase.scheduledStartDate); - } - // For all other cases, take the `actualStartDate` as phase is already started - return new Date(phase.actualStartDate); -} - /** * Get end date * @param {Object} challenge challenge info diff --git a/src/shared/utils/challenge-listing/helper.js b/src/shared/utils/challenge-listing/helper.js new file mode 100644 index 0000000000..192dafcc04 --- /dev/null +++ b/src/shared/utils/challenge-listing/helper.js @@ -0,0 +1,31 @@ +import moment from 'moment'; + +/** + * Returns phase's end date. + * @param {Object} phase + * @return {Date} + */ +export function phaseEndDate(phase) { + // Case 1: phase is still open. take the `scheduledEndDate` + // Case 2: phase is not open but `scheduledStartDate` is a future date. + // This means phase is not yet started. So take the `scheduledEndDate` + if (phase.isOpen || moment(phase.scheduledStartDate).isAfter()) { + return new Date(phase.scheduledEndDate); + } + // for other cases, take the `actualEndDate` as phase is already closed + return new Date(phase.actualEndDate); +} + +/** + * Returns phase's start date. + * @param {Object} phase + * @return {Date} + */ +export function phaseStartDate(phase) { + // Case 1: Phase is not yet started. take the `scheduledStartDate` + if (phase.isOpen !== true && moment(phase.scheduledStartDate).isAfter()) { + return new Date(phase.scheduledStartDate); + } + // For all other cases, take the `actualStartDate` as phase is already started + return new Date(phase.actualStartDate); +} diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js index d1990931b9..bf6b52818f 100644 --- a/src/shared/utils/challenge-listing/sort.js +++ b/src/shared/utils/challenge-listing/sort.js @@ -4,7 +4,7 @@ import moment from 'moment'; import { find, sumBy } from 'lodash'; -import { phaseStartDate, phaseEndDate } from '../challenge-detail/helper'; +import { phaseStartDate, phaseEndDate } from './helper'; export const SORTS = { CURRENT_PHASE: 'current-phase', @@ -27,15 +27,21 @@ export default { }, [SORTS.MOST_RECENT]: { func: (a, b) => { - const getRegistrationStartDate = (challenge) => { - // extract the registration phase from `challenge.phases`, + const getChallengeStartDate = (challenge) => { + // extract the phases from `challenge.phases`, // as `challenge.registrationStartDate` returned from API is not reliable const registrationPhase = find(challenge.phases, p => p.name === 'Registration'); - return moment(phaseStartDate(registrationPhase)); + const submissionPhase = find(challenge.phases, p => p.name === 'Submission'); + // registration phase exists + if (registrationPhase) { + return moment(phaseStartDate(registrationPhase)); + } + // registration phase doesnt exist, This is possibly a F2F or TSK. Take submission phase + return moment(phaseStartDate(submissionPhase)); }; - const aRegistrationStartDate = getRegistrationStartDate(a); - const bRegistrationStartDate = getRegistrationStartDate(b); - return bRegistrationStartDate.diff(aRegistrationStartDate); + const aChallengeStartDate = getChallengeStartDate(a); + const bChallengeStartDate = getChallengeStartDate(b); + return bChallengeStartDate.diff(aChallengeStartDate); }, name: 'Most recent', },