diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9fcaf7177a..a6edc20235 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -349,8 +349,7 @@ workflows:
filters:
branches:
only:
- - develop
- - latex_support
+ - sprig-lib
# This is alternate dev env for parallel testing
- "build-test":
context : org-global
@@ -365,6 +364,7 @@ workflows:
branches:
only:
- universal_nav
+ - marathon_match_submission_download
- feat/badges-box
# This is beta env for production soft releases
- "build-prod-beta":
@@ -380,8 +380,6 @@ workflows:
branches:
only:
- develop
- - old-mm-fix
- - reskin-profile-settings
# Production builds are exectuted
# when PR is merged to the master
# Don't change anything in this configuration
diff --git a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap
new file mode 100644
index 0000000000..22c7fa66fe
--- /dev/null
+++ b/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Matches shallow shapshot shapshot 1 1`] = `
+
- {currentPhases && `${currentPhases.name} Ends: `}
+ {currentPhases && `${currentPhases.name} Ends In: `}
{timeDiff.text}
diff --git a/src/shared/components/challenge-detail/Header/style.scss b/src/shared/components/challenge-detail/Header/style.scss
index a3daf5e533..471d30f915 100644
--- a/src/shared/components/challenge-detail/Header/style.scss
+++ b/src/shared/components/challenge-detail/Header/style.scss
@@ -635,7 +635,6 @@
border: 0 hidden $tc-white;
overflow: hidden;
margin: 0 14px 0 5px;
- min-height: 96px;
.deadlines-overview {
background-color: #555;
@@ -644,13 +643,14 @@
height: 60px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
+ display: flex;
+ padding-right: 16px;
&.opened {
border-radius: 0 !important;
}
@include xs-to-md {
- display: flex;
padding: 0 16px;
}
diff --git a/src/shared/components/challenge-detail/Specification/SpecificationComponent/index.jsx b/src/shared/components/challenge-detail/Specification/SpecificationComponent/index.jsx
index ce3a3aef1f..6360f5f069 100644
--- a/src/shared/components/challenge-detail/Specification/SpecificationComponent/index.jsx
+++ b/src/shared/components/challenge-detail/Specification/SpecificationComponent/index.jsx
@@ -3,6 +3,13 @@ import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
+import remarkGfm from 'remark-gfm';
+import remarkParse from 'remark-parse';
+import rehypeStringify from 'rehype-stringify';
+import remarkFrontmatter from 'remark-frontmatter';
+import rehypeRaw from 'rehype-raw';
+import remarkBreaks from 'remark-breaks';
+import style from './styles.scss';
// eslint-disable-next-line import/no-extraneous-dependencies
import 'katex/dist/katex.min.css';
@@ -14,8 +21,15 @@ export default function SpecificationComponent({
if (format === 'markdown') {
return (
{bodyText}
diff --git a/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss b/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss
new file mode 100644
index 0000000000..a1be329593
--- /dev/null
+++ b/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss
@@ -0,0 +1,27 @@
+@import "~styles/mixins";
+
+.container {
+ :global {
+ table {
+ display: block;
+ width: 100%;
+ overflow: auto;
+ box-sizing: border-box;
+
+ * {
+ box-sizing: border-box;
+ }
+
+ tr {
+ background-color: #fff;
+ border-top: 1px solid #c6cbd1;
+ }
+
+ th,
+ td {
+ padding: 6px 13px;
+ border: 1px solid #dfe2e5;
+ }
+ }
+ }
+}
diff --git a/src/shared/components/challenge-detail/Specification/styles.scss b/src/shared/components/challenge-detail/Specification/styles.scss
index bd21a44f4a..7ecaaf250f 100644
--- a/src/shared/components/challenge-detail/Specification/styles.scss
+++ b/src/shared/components/challenge-detail/Specification/styles.scss
@@ -169,10 +169,10 @@ $tc-link-visited: #0c4e98;
li::before {
display: table-cell;
- font-weight: 700;
text-align: right;
content: counter(item) ".";
padding: 0 10px 0 0;
+ white-space: nowrap;
}
}
@@ -352,10 +352,10 @@ $tc-link-visited: #0c4e98;
&::before {
display: table-cell;
- font-weight: 700;
text-align: right;
content: counter(item) ".";
padding: 0 10px 0 0;
+ white-space: nowrap;
}
}
}
diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
index 90cdf96e3d..750458887b 100644
--- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
@@ -6,14 +6,19 @@
import React from 'react';
import PT from 'prop-types';
+import { services } from 'topcoder-react-lib';
import moment from 'moment';
+import { CHALLENGE_STATUS } from 'utils/tc';
import FailedSubmissionTooltip from '../../FailedSubmissionTooltip';
// import Completed from '../../../icons/completed.svg';
import InReview from '../../../icons/in-review.svg';
import Queued from '../../../icons/queued.svg';
+import DownloadIcon from '../../../../SubmissionManagement/Icons/IconSquareDownload.svg';
import './style.scss';
+const { getService } = services.submissions;
+
export default function SubmissionHistoryRow({
isMM,
submission,
@@ -22,6 +27,11 @@ export default function SubmissionHistoryRow({
submissionTime,
isReviewPhaseComplete,
status,
+ challengeStatus,
+ auth,
+ numWinners,
+ submissionId,
+ isLoggedIn,
}) {
const getInitialReviewResult = () => {
if (provisionalScore && provisionalScore < 0) return ;
@@ -70,6 +80,33 @@ export default function SubmissionHistoryRow({
{moment(submissionTime).format('DD MMM YYYY')} {moment(submissionTime).format('HH:mm:ss')}
+ {
+ isLoggedIn && isMM
+ && (numWinners > 0 || challengeStatus === CHALLENGE_STATUS.COMPLETED) && (
+
+ 0 || challengeStatus === CHALLENGE_STATUS.COMPLETED) ? style.download : ''}` }}
+ >
Submission History
@@ -162,7 +166,14 @@ export default function SubmissionRow({
Time
{
- isMM && (
+ isMM && (numWinners > 0 || challengeStatus === CHALLENGE_STATUS.COMPLETED) && (
+
+ Action
+
+ )
+ }
+ {
+ isMM && isLoggedIn && (
)
}
@@ -174,17 +185,24 @@ export default function SubmissionRow({
))
}
-
@@ -201,12 +219,14 @@ SubmissionRow.defaultProps = {
finalRank: null,
provisionalRank: null,
rating: null,
+ isLoggedIn: false,
};
SubmissionRow.propTypes = {
isMM: PT.bool.isRequired,
openHistory: PT.bool.isRequired,
member: PT.string.isRequired,
+ challengeStatus: PT.string.isRequired,
submissions: PT.arrayOf(PT.shape({
provisionalScore: PT.oneOfType([
PT.number,
@@ -241,4 +261,7 @@ SubmissionRow.propTypes = {
provisionalRank: PT.number,
onShowPopup: PT.func.isRequired,
viewAsTable: PT.bool.isRequired,
+ isLoggedIn: PT.bool,
+ numWinners: PT.number.isRequired,
+ auth: PT.shape().isRequired,
};
diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss b/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss
index 67703ed926..6719dc0291 100644
--- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss
+++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss
@@ -1,5 +1,23 @@
@import "~styles/mixins";
+.modal {
+ background: #fff;
+ border-radius: 4;
+ max-height: 95vh;
+ overflow: hidden;
+ padding: 40;
+ width: 800px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ transform: translate((calc(50vw - 50%), calc(50vh - 50%)));
+ z-index: 999;
+
+ &.download {
+ width: 900px;
+ }
+}
+
.history {
padding-bottom: 10px;
border-radius: 8px;
@@ -50,11 +68,15 @@
}
&.col-4 {
- flex: 30;
+ flex: 32;
}
&.col-5 {
- flex: 30;
+ flex: 28;
+ }
+
+ &.center {
+ text-align: center;
}
}
@@ -121,7 +143,7 @@
}
.col-3 {
- flex: 30;
+ flex: 32;
}
.col-4 {
@@ -351,16 +373,22 @@ hr {
}
}
+.close-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
.close-btn {
cursor: pointer;
display: flex;
- justify-content: flex-end;
+ align-items: center;
+ justify-content: center;
margin-top: 24px;
- margin-bottom: 32px;
background-color: #137d60;
padding: 8px 24px;
+ width: 91px;
border-radius: 24px;
- float: right;
span {
@include roboto-bold;
@@ -374,7 +402,6 @@ hr {
@include xs-to-sm {
border-top: 1px solid #e9e9e9;
position: sticky;
- margin-bottom: 16px;
}
}
diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx
index 486427c0c6..8fa9b7d730 100644
--- a/src/shared/components/challenge-detail/Submissions/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/index.jsx
@@ -10,17 +10,19 @@ import { isMM as checkIsMM } from 'utils/challenge';
import _ from 'lodash';
import { connect } from 'react-redux';
import { config } from 'topcoder-react-utils';
-import { submission as submissionUtils } from 'topcoder-react-lib';
+import { submission as submissionUtils, services } from 'topcoder-react-lib';
import { isTokenExpired } from '@topcoder-platform/tc-auth-lib';
import cn from 'classnames';
import { Button } from 'topcoder-react-ui-kit';
import DateSortIcon from 'assets/images/icon-date-sort.svg';
import SortIcon from 'assets/images/icon-sort.svg';
+import { getSubmissionId } from 'utils/submissions';
+import { compressFiles } from 'utils/files';
import sortList from 'utils/challenge-detail/sort';
import challengeDetailsActions from 'actions/page/challenge-details';
import LoadingIndicator from 'components/LoadingIndicator';
-import { goToLogin, getRatingLevel } from 'utils/tc';
+import { goToLogin, getRatingLevel, CHALLENGE_STATUS } from 'utils/tc';
import Lock from '../icons/lock.svg';
import ViewAsListActive from '../icons/view-as-list-active.svg';
import ViewAsListInactive from '../icons/view-as-list-inactive.svg';
@@ -32,6 +34,8 @@ import style from './style.scss';
const { getProvisionalScore, getFinalScore } = submissionUtils;
+const { getService } = services.submissions;
+
class SubmissionsComponent extends React.Component {
constructor(props) {
super(props);
@@ -47,6 +51,7 @@ class SubmissionsComponent extends React.Component {
finalRankClicked: false,
provisionalRankClicked: false,
provisionalScoreClicked: false,
+ downloadingAll: false,
};
this.onHandleInformationPopup = this.onHandleInformationPopup.bind(this);
this.getSubmissionsSortParam = this.getSubmissionsSortParam.bind(this);
@@ -292,6 +297,8 @@ class SubmissionsComponent extends React.Component {
challengesUrl,
viewAsTable,
setViewAsTable,
+ numWinners,
+ auth,
} = this.props;
const {
checkpoints,
@@ -302,6 +309,7 @@ class SubmissionsComponent extends React.Component {
} = challenge;
const isMM = this.isMM();
+ const isLoggedIn = !_.isEmpty(auth.tokenV3);
const isReviewPhaseComplete = this.checkIsReviewPhaseComplete();
const { field, sort } = this.getSubmissionsSortParam(isMM, isReviewPhaseComplete);
@@ -319,6 +327,7 @@ class SubmissionsComponent extends React.Component {
finalRankClicked,
provisionalRankClicked,
provisionalScoreClicked,
+ downloadingAll,
} = this.state;
const sortOptionClicked = {
@@ -593,6 +602,58 @@ class SubmissionsComponent extends React.Component {
)
}
+ {
+ ((numWinners > 0 || challenge.status === CHALLENGE_STATUS.COMPLETED)
+ && isMM && isLoggedIn) && (
+
+
+
+ )
+ }
{
isMM && (
@@ -786,6 +847,7 @@ class SubmissionsComponent extends React.Component {
isMM={isMM}
key={submission.member}
{...submission}
+ challengeStatus={challenge.status}
toggleHistory={() => { toggleSubmissionHistory(index); }}
openHistory={(submissionHistoryOpen[index.toString()] || false)}
isLoadingSubmissionInformation={isLoadingSubmissionInformation}
@@ -795,6 +857,9 @@ class SubmissionsComponent extends React.Component {
onGetFlagImageFail={onGetFlagImageFail}
submissionDetail={submission}
viewAsTable={viewAsTable}
+ numWinners={numWinners}
+ auth={auth}
+ isLoggedIn={isLoggedIn}
/>
))
)
@@ -908,6 +973,7 @@ SubmissionsComponent.propTypes = {
type: PT.string.isRequired,
tags: PT.arrayOf(PT.string),
registrants: PT.any,
+ status: PT.string.isRequired,
phases: PT.any,
}).isRequired,
toggleSubmissionHistory: PT.func.isRequired,
@@ -936,6 +1002,7 @@ SubmissionsComponent.propTypes = {
challengesUrl: PT.string.isRequired,
viewAsTable: PT.bool.isRequired,
setViewAsTable: PT.func.isRequired,
+ numWinners: PT.number.isRequired,
};
function mapDispatchToProps(dispatch) {
diff --git a/src/shared/components/challenge-detail/Submissions/style.scss b/src/shared/components/challenge-detail/Submissions/style.scss
index 76185c7ff6..3fb513c1fc 100644
--- a/src/shared/components/challenge-detail/Submissions/style.scss
+++ b/src/shared/components/challenge-detail/Submissions/style.scss
@@ -466,6 +466,39 @@
padding-top: 5px;
}
+.block-download-all {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+
+ .download {
+ @include roboto-bold;
+
+ font-size: 15px;
+ line-height: 16px;
+ color: #137d60;
+ background-color: transparent;
+
+ &:hover {
+ color: #0ab88a;
+ }
+
+ &:disabled {
+ pointer-events: none;
+ background-color: transparent;
+ color: #137d60;
+ }
+
+ &.MM {
+ cursor: pointer;
+ }
+
+ @include xs-to-sm {
+ font-size: 13px;
+ }
+ }
+}
+
.col-arrow {
display: flex;
padding-left: 5px;
diff --git a/src/shared/components/challenge-detail/Winners/Winner/index.jsx b/src/shared/components/challenge-detail/Winners/Winner/index.jsx
index f5f7b999ea..d61c04fc82 100644
--- a/src/shared/components/challenge-detail/Winners/Winner/index.jsx
+++ b/src/shared/components/challenge-detail/Winners/Winner/index.jsx
@@ -1,22 +1,30 @@
import { Avatar } from 'topcoder-react-ui-kit';
import PT from 'prop-types';
+import { services } from 'topcoder-react-lib';
import React, { useEffect, useState } from 'react';
import _ from 'lodash';
import { config } from 'topcoder-react-utils';
import { formatOrdinals, numberWithCommas } from 'utils/challenge-detail/helper';
+import { getMMSubmissionId } from 'utils/submissions';
+import DownloadIcon from '../../../SubmissionManagement/Icons/IconSquareDownload.svg';
import style from './style.scss';
+const { getService } = services.submissions;
+
function getId(submissions, placement) {
return submissions.find(s => s.placement === placement).submissionId;
}
export default function Winner({
isDesign,
+ isMM,
prizes,
submissions,
viewable,
winner,
+ isLoggedIn,
+ auth,
}) {
const [windowOrigin, setWindowOrigin] = useState();
useEffect(() => {
@@ -24,6 +32,7 @@ export default function Winner({
}, []);
const submissionId = viewable && getId(submissions, winner.placement);
+ const mmSubmissionId = isMM && getMMSubmissionId(submissions, winner.handle);
let avatarUrl = winner.photoURL;
if (avatarUrl) {
@@ -69,28 +78,6 @@ export default function Winner({
)
}
- {
- (winner.submissionDownloadLink && viewable)
- && (
-
- Download
-
- )
- }
- {
- /*
-
- Submitted on:
- {moment(winner.submissionDate).format('MMM DD, YYYY HH:mm')}
-
- */
- }
@@ -100,6 +87,53 @@ export default function Winner({
{numberWithCommas(prize)}
+
+ {
+ ((!winner.submissionDownloadLink || !viewable) && isMM && isLoggedIn) && (
+
+ )
+ }
+ {
+ (winner.submissionDownloadLink && viewable)
+ && (
+
+
+
+ )
+ }
+ {
+ /*
+
+ Submitted on:
+ {moment(winner.submissionDate).format('MMM DD, YYYY HH:mm')}
+
+ */
+ }
+
);
}
@@ -110,7 +144,8 @@ Winner.defaultProps = {
Winner.propTypes = {
isDesign: PT.bool.isRequired,
- prizes: PT.arrayOf(PT.number),
+ isMM: PT.bool.isRequired,
+ prizes: PT.arrayOf(PT.shape()),
submissions: PT.arrayOf(PT.object).isRequired,
viewable: PT.bool.isRequired,
winner: PT.shape({
@@ -119,4 +154,6 @@ Winner.propTypes = {
photoURL: PT.any,
submissionDownloadLink: PT.any,
}).isRequired,
+ isLoggedIn: PT.bool.isRequired,
+ auth: PT.shape().isRequired,
};
diff --git a/src/shared/components/challenge-detail/Winners/Winner/style.scss b/src/shared/components/challenge-detail/Winners/Winner/style.scss
index 865263cc3a..d63ffe83ff 100644
--- a/src/shared/components/challenge-detail/Winners/Winner/style.scss
+++ b/src/shared/components/challenge-detail/Winners/Winner/style.scss
@@ -3,7 +3,6 @@
.avatar-prize {
display: flex;
align-items: center;
- margin-bottom: 20px;
@include xs-to-sm {
margin-bottom: 10px;
@@ -66,7 +65,7 @@
display: flex;
justify-content: space-between;
border-top: 1px solid $tc-gray-10;
- padding: 17px 17px 0 17px;
+ padding: 17px;
@include xs-to-sm {
padding: 16px 0;
@@ -183,16 +182,6 @@
}
}
- .download {
- font-size: 15px;
- line-height: 22px;
- color: $tc-dark-blue;
-
- @include xs-to-sm {
- font-size: 13px;
- }
- }
-
.date {
font-size: 15px;
line-height: 22px;
@@ -221,6 +210,7 @@
.left {
display: flex;
+ flex: 1;
}
.right {
@@ -239,3 +229,13 @@
}
}
}
+
+.download-container {
+ display: flex;
+ align-items: center;
+ margin-left: 40px;
+
+ .download {
+ text-decoration: none;
+ }
+}
diff --git a/src/shared/components/challenge-detail/Winners/index.jsx b/src/shared/components/challenge-detail/Winners/index.jsx
index efe57fb954..ce850e36c3 100644
--- a/src/shared/components/challenge-detail/Winners/index.jsx
+++ b/src/shared/components/challenge-detail/Winners/index.jsx
@@ -3,29 +3,90 @@
* Winners tab component.
*/
-import React from 'react';
+import React, { useState } from 'react';
import PT from 'prop-types';
+import _ from 'lodash';
+import { services } from 'topcoder-react-lib';
+import { CHALLENGE_STATUS } from 'utils/tc';
+import { getMMSubmissionId } from 'utils/submissions';
+import { compressFiles } from 'utils/files';
import Winner from './Winner';
import './style.scss';
+const { getService } = services.submissions;
+
export default function Winners({
winners,
prizes,
submissions,
viewable,
isDesign,
+ isMM,
+ isLoggedIn,
+ auth,
+ challengeStatus,
}) {
+ const [downloadingAll, setDownloadingAll] = useState(false);
return (
+ {
+ ((winners.length > 0 || challengeStatus === CHALLENGE_STATUS.COMPLETED)
+ && isMM && isLoggedIn) && (
+
+
+
+ )
+ }
{
winners.map(w => (
))
}
@@ -39,12 +100,19 @@ Winners.defaultProps = {
submissions: [],
viewable: false,
isDesign: false,
+ isMM: false,
+ isLoggedIn: false,
+ challengeStatus: '',
};
Winners.propTypes = {
winners: PT.arrayOf(PT.shape()),
- prizes: PT.arrayOf(PT.number),
+ prizes: PT.arrayOf(PT.shape()),
submissions: PT.arrayOf(PT.shape()),
viewable: PT.bool,
isDesign: PT.bool,
+ isMM: PT.bool,
+ isLoggedIn: PT.bool,
+ challengeStatus: PT.string,
+ auth: PT.shape().isRequired,
};
diff --git a/src/shared/components/challenge-detail/Winners/style.scss b/src/shared/components/challenge-detail/Winners/style.scss
index 6e29c3e518..b99774a831 100644
--- a/src/shared/components/challenge-detail/Winners/style.scss
+++ b/src/shared/components/challenge-detail/Winners/style.scss
@@ -21,3 +21,36 @@
text-align: center;
width: 100%;
}
+
+.block-download-all {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 20px;
+}
+
+.download {
+ @include roboto-bold;
+
+ font-size: 15px;
+ line-height: 16px;
+ color: #137d60;
+ background-color: transparent;
+
+ &:hover {
+ color: #0ab88a;
+ }
+
+ &:disabled {
+ pointer-events: none;
+ background-color: transparent;
+ color: #137d60;
+ }
+
+ &.MM {
+ cursor: pointer;
+ }
+
+ @include xs-to-sm {
+ font-size: 13px;
+ }
+}
diff --git a/src/shared/containers/EDU/styles/home.scss b/src/shared/containers/EDU/styles/home.scss
index 46cf2c2e60..1dc0c6c6dd 100644
--- a/src/shared/containers/EDU/styles/home.scss
+++ b/src/shared/containers/EDU/styles/home.scss
@@ -5,6 +5,7 @@
flex: 1;
flex-direction: column;
width: 100%;
+ overflow: hidden;
.bannerContainer {
background-image: linear-gradient(98.81deg, #8b41b0 0%, #ef476f 100%);
diff --git a/src/shared/containers/TopcoderHeader/index.jsx b/src/shared/containers/TopcoderHeader/index.jsx
index 7ef484133c..074e29e045 100644
--- a/src/shared/containers/TopcoderHeader/index.jsx
+++ b/src/shared/containers/TopcoderHeader/index.jsx
@@ -1,22 +1,17 @@
/* global tcUniNav */
import React, { useEffect, useMemo, useRef } from 'react';
-import PT from 'prop-types';
import { connect } from 'react-redux';
import { config } from 'topcoder-react-utils';
import LoadingIndicator from 'components/LoadingIndicator';
-import _ from 'lodash';
-import { getInitials, getSubPageConfiguration } from '../../utils/url';
+import { getSubPageConfiguration } from '../../utils/url';
import { SSRPlaceholder } from '../../utils/SSR';
import './styles.scss';
let counter = 0;
const headerElIdTmpl = 'uninav-headerNav';
-const TopcoderHeader = ({ auth }) => {
+const TopcoderHeader = () => {
const uniNavInitialized = useRef(false);
- const user = _.get(auth, 'profile') || {};
- const authToken = _.get(auth, 'tokenV3');
- const isAuthenticated = !!authToken;
const authURLs = config.HEADER_AUTH_URLS;
const headerRef = useRef();
const headerElId = useRef(`${headerElIdTmpl}-${counter}`);
@@ -49,15 +44,11 @@ const TopcoderHeader = ({ auth }) => {
return type;
}, []);
- const navigationUserInfo = {
- ...user,
- initials: getInitials(user.firstName, user.lastName),
- };
-
useEffect(() => {
if (uniNavInitialized.current) {
return;
}
+ headerRef.current.id = headerElId.current;
uniNavInitialized.current = true;
counter += 1;
@@ -69,6 +60,7 @@ const TopcoderHeader = ({ auth }) => {
type: navType,
toolName: getSubPageConfiguration().toolName,
toolRoot: getSubPageConfiguration().toolRoot,
+ user: 'auto',
signOut: () => {
window.location = `${config.URL.BASE}/logout?ref=nav`;
},
@@ -81,23 +73,10 @@ const TopcoderHeader = ({ auth }) => {
});
}, [navType]);
- useEffect(() => {
- tcUniNav('update', headerElId.current, {
- user: isAuthenticated ? navigationUserInfo : null,
- });
- }, [isAuthenticated, navigationUserInfo]);
-
return (
);
};
-TopcoderHeader.defaultProps = {
- auth: {},
-};
-
-TopcoderHeader.propTypes = {
- auth: PT.shape(),
-};
const mapStateToProps = state => ({
auth: state.auth,
diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx
index 361b414712..3b36784723 100644
--- a/src/shared/containers/challenge-detail/index.jsx
+++ b/src/shared/containers/challenge-detail/index.jsx
@@ -155,7 +155,7 @@ class ChallengeDetailPageContainer extends React.Component {
this.apiService = getService({ spaceName: 'EDU' });
this.state = {
thriveArticles: [],
- showDeadlineDetail: true,
+ showDeadlineDetail: false,
registrantsSort: {
field: '',
sort: '',
@@ -604,6 +604,7 @@ class ChallengeDetailPageContainer extends React.Component {
challengesUrl={challengesUrl}
viewAsTable={viewAsTable && isMM}
setViewAsTable={value => this.setState({ viewAsTable: value })}
+ numWinners={isLegacyMM ? 0 : winners.length}
/>
)
}
@@ -659,6 +660,9 @@ class ChallengeDetailPageContainer extends React.Component {
viewable={submissionsViewable ? submissionsViewable.value === 'true' : false}
submissions={challenge.submissions}
isDesign={track.toLowerCase() === 'design'}
+ isMM={isMM}
+ isLoggedIn={isLoggedIn}
+ auth={auth}
/>
)
}
diff --git a/src/shared/containers/timeline-wall/index.jsx b/src/shared/containers/timeline-wall/index.jsx
index 6c1e5aca8e..4bdf3f863b 100644
--- a/src/shared/containers/timeline-wall/index.jsx
+++ b/src/shared/containers/timeline-wall/index.jsx
@@ -24,6 +24,8 @@ import './styles.scss';
const FETCHING_PENDING_APPROVAL_EVENTS_INTERVAL = _.get(config, 'TIMELINE.FETCHING_PENDING_APPROVAL_EVENTS_INTERVAL', 0);
const FORUM_LINK = _.get(config, 'TIMELINE.FORUM_LINK', '');
function TimelineWallContainer(props) {
+ const currentTime = new Date();
+ const thisYear = currentTime.getFullYear();
const [tab, setTab] = useState(0);
const fetchingApprovalsInterval = useRef(null);
const [showRightFilterMobile, setShowRightFilterMobile] = useState(false);
@@ -223,7 +225,7 @@ function TimelineWallContainer(props) {
type="button"
styleName="filter-dropdown hide-desktop show-mobile"
>
-
{selectedFilterValue.year ? selectedFilterValue.year : ''}
+
{selectedFilterValue.year ? selectedFilterValue.year : thisYear}
diff --git a/src/shared/containers/timeline-wall/modal-event-add/index.jsx b/src/shared/containers/timeline-wall/modal-event-add/index.jsx
index c4cde0570c..e4fd5517b9 100644
--- a/src/shared/containers/timeline-wall/modal-event-add/index.jsx
+++ b/src/shared/containers/timeline-wall/modal-event-add/index.jsx
@@ -3,6 +3,7 @@ import PT from 'prop-types';
import { Modal } from 'topcoder-react-ui-kit';
import IconCloseGreen from 'assets/images/icon-close-green.svg';
import LoadingIndicator from 'components/LoadingIndicator';
+import cn from 'classnames';
import style from './styles.scss';
@@ -17,14 +18,17 @@ function ModalEventAdd({
onCancel={onClose}
>
- Confirmation
+ {uploadResult ? 'Error' : 'Confirmation'}
{
uploading ? (
) : (
-
+
{
uploadResult || successMessage
}
diff --git a/src/shared/containers/timeline-wall/modal-event-add/styles.scss b/src/shared/containers/timeline-wall/modal-event-add/styles.scss
index 58b03c1cc9..7aed3cb770 100644
--- a/src/shared/containers/timeline-wall/modal-event-add/styles.scss
+++ b/src/shared/containers/timeline-wall/modal-event-add/styles.scss
@@ -44,6 +44,10 @@
strong {
@include roboto-bold;
}
+
+ &.error {
+ color: #ef476f;
+ }
}
.separator {
diff --git a/src/shared/containers/timeline-wall/styles.scss b/src/shared/containers/timeline-wall/styles.scss
index 5d4eb45b15..678e076183 100644
--- a/src/shared/containers/timeline-wall/styles.scss
+++ b/src/shared/containers/timeline-wall/styles.scss
@@ -86,6 +86,7 @@
border: none;
color: white;
+ &:visited,
&:hover {
color: white;
}
diff --git a/src/shared/containers/timeline-wall/timeline-events/right-filter/styles.scss b/src/shared/containers/timeline-wall/timeline-events/right-filter/styles.scss
index ce83f8f22d..db8d39a626 100644
--- a/src/shared/containers/timeline-wall/timeline-events/right-filter/styles.scss
+++ b/src/shared/containers/timeline-wall/timeline-events/right-filter/styles.scss
@@ -3,6 +3,7 @@
.container {
display: flex;
flex-direction: column;
+ z-index: 1;
@media (max-width: 768px) {
position: fixed;
@@ -53,16 +54,16 @@
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
padding: 10px 0;
- position: fixed;
- right: 0;
- left: 1374px;
+ position: sticky;
+ margin-bottom: 80px;
+ top: 0;
max-height: 70%;
overflow: scroll;
- -ms-overflow-style: none; /* Internet Explorer 10+ */
- scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* Internet Explorer 10+ */
+ scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
- display: none; /* Safari and Chrome */
+ display: none; /* Safari and Chrome */
}
@media (max-width: 1500px) {
@@ -72,6 +73,8 @@
@media (max-width: 768px) {
border-radius: 0;
width: calc(100% - 10px);
+ position: relative;
+ margin-bottom: unset;
}
span {
diff --git a/src/shared/containers/timeline-wall/timeline-events/styles.scss b/src/shared/containers/timeline-wall/timeline-events/styles.scss
index d55b957f54..476d845156 100644
--- a/src/shared/containers/timeline-wall/timeline-events/styles.scss
+++ b/src/shared/containers/timeline-wall/timeline-events/styles.scss
@@ -4,6 +4,7 @@
display: flex;
flex-direction: row;
justify-content: space-between;
+ max-width: 1492px;
}
.left-content {
diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js
index eda5f72328..c27bf01ad8 100644
--- a/src/shared/reducers/index.js
+++ b/src/shared/reducers/index.js
@@ -20,7 +20,7 @@ import { getCommunityId } from 'server/services/communities';
import { redux, config, isomorphy } from 'topcoder-react-utils';
import { reducer as toastrReducer } from 'react-redux-toastr';
import { reducerFactory } from 'topcoder-react-lib';
-import { getAuthTokens } from 'utils/tc';
+import { getAuthTokens, getM2mToken } from 'utils/tc';
import contentful from './contentful';
import topcoderHeader from './topcoder_header';
@@ -153,6 +153,10 @@ export function factory(req) {
const user = _.get(res, 'auth.user');
if (user && isomorphy.isServerSide()) {
res.auth.userIdHash = generateUserIdHash(user);
+ getM2mToken()
+ .then(((token) => {
+ res.auth.m2mToken = token;
+ }));
}
if (req) {
diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx
index c307e5f66a..ca9f5de982 100644
--- a/src/shared/utils/challenge-detail/helper.jsx
+++ b/src/shared/utils/challenge-detail/helper.jsx
@@ -55,6 +55,7 @@ export function getEndDate(challenge) {
export function getTimeLeft(
phase,
toGoText = 'to go',
+ fullText = false,
) {
const STALLED_TIME_LEFT_MSG = 'Challenge is currently on hold';
const FF_TIME_LEFT_MSG = 'Winner is working on fixes';
@@ -71,9 +72,9 @@ export function getTimeLeft(
if (late) time = -time;
let format;
- if (time > DAY_MS) format = 'D[d] H[h]';
- else if (time > HOUR_MS) format = 'H[h] m[min]';
- else format = 'm[min] s[s]';
+ if (time > DAY_MS) format = fullText ? 'D [day] H [hour]' : 'D[d] H[h]';
+ else if (time > HOUR_MS) format = fullText ? 'H [hour] m [minute]' : 'H[h] m[min]';
+ else format = fullText ? 'm [minute] s [second]' : 'm[min] s[s]';
time = moment.duration(time).format(format);
time = late ? `${time} Past Due` : `${time} ${toGoText}`;
diff --git a/src/shared/utils/challenge.js b/src/shared/utils/challenge.js
index 22dce73260..ce4679224b 100644
--- a/src/shared/utils/challenge.js
+++ b/src/shared/utils/challenge.js
@@ -9,7 +9,8 @@ import _ from 'lodash';
*/
export function isMM(challenge) {
const tags = _.get(challenge, 'tags') || [];
- return tags.includes('Marathon Match');
+ const isMMType = challenge ? challenge.type === 'Marathon Match' : false;
+ return tags.includes('Marathon Match') || isMMType;
}
/**
diff --git a/src/shared/utils/files.js b/src/shared/utils/files.js
new file mode 100644
index 0000000000..ccbde7fc21
--- /dev/null
+++ b/src/shared/utils/files.js
@@ -0,0 +1,67 @@
+/* eslint-disable import/prefer-default-export */
+import * as fflate from 'fflate';
+
+const fileToU8 = (file, cb) => {
+ const fr = new FileReader();
+ fr.onloadend = () => {
+ cb(new Uint8Array(fr.result));
+ };
+ fr.readAsArrayBuffer(file);
+};
+
+const download = (file, name) => {
+ const url = URL.createObjectURL(new Blob([file]));
+ const dl = document.createElement('a');
+ dl.download = name || (`compressed-file-${Date.now()}.zip`);
+ dl.href = url;
+ dl.click();
+ URL.revokeObjectURL(url);
+};
+
+/**
+ * Compress list of files
+ * @param {Array} files list of files
+ * @param {String} fileName file name
+ * @param {Function} finish finish callback
+ */
+export const compressFiles = (files, fileName, finish) => {
+ // fflate's ZIP API is asynchronous and parallelized (multithreaded)
+ let left = files.length;
+ const zipObj = {};
+ const ALREADY_COMPRESSED = [
+ 'zip', 'gz', 'png', 'jpg', 'jpeg', 'pdf', 'doc', 'docx', 'ppt', 'pptx',
+ 'xls', 'xlsx', 'heic', 'heif', '7z', 'bz2', 'rar', 'gif', 'webp', 'webm',
+ 'mp4', 'mov', 'mp3', 'aifc',
+ ];
+
+ // Yet again, this is necessary for parallelization.
+ const processFile = (i) => {
+ const file = files[i];
+ const ext = file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase();
+ fileToU8(file, (buf) => {
+ // With fflate, we can choose which files we want to compress
+ zipObj[file.name] = [buf, {
+ level: ALREADY_COMPRESSED.indexOf(ext) === -1 ? 6 : 0,
+ }];
+
+ // If we didn't want to specify options:
+ // zipObj[file.name] = buf;
+
+ // eslint-disable-next-line no-plusplus
+ if (!--left) {
+ fflate.zip(zipObj, {
+ // If you want to control options for every file, you can do so here
+ // They are merged with the per-file options (if they exist)
+ // mem: 9
+ }, (err, out) => {
+ download(out, fileName);
+ finish();
+ });
+ }
+ });
+ };
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < files.length; ++i) {
+ processFile(i);
+ }
+};
diff --git a/src/shared/utils/submissions.js b/src/shared/utils/submissions.js
new file mode 100644
index 0000000000..27cd62e800
--- /dev/null
+++ b/src/shared/utils/submissions.js
@@ -0,0 +1,27 @@
+/* eslint-disable import/prefer-default-export */
+
+/**
+ * Get submission id of marathon match challenge
+ * @param {Array} submissions list of submission
+ * @param {String} handle handle
+ * @returns String or null
+ */
+export function getMMSubmissionId(submissions, handle) {
+ const filterSubmissions = handle ? submissions.filter(s => s.createdBy === handle) : submissions;
+ const sortedSubmissions = filterSubmissions.sort((a, b) => (a.created < b.created ? 1 : -1));
+
+ return sortedSubmissions.length > 0 ? sortedSubmissions[0].id : null;
+}
+
+/**
+ * Get submission id of history submissions
+ * @param {Array} submissions list of history submission
+ * @returns String or null
+ */
+export function getSubmissionId(submissions) {
+ const filterSubmissions = submissions;
+ const sortedSubmissions = filterSubmissions.sort((a, b) => (
+ a.submissionTime < b.submissionTime ? 1 : -1));
+
+ return sortedSubmissions.length > 0 ? sortedSubmissions[0].submissionId : null;
+}
diff --git a/src/shared/utils/tc.js b/src/shared/utils/tc.js
index e584cc9bde..c223f2818e 100644
--- a/src/shared/utils/tc.js
+++ b/src/shared/utils/tc.js
@@ -7,8 +7,9 @@ import _ from 'lodash';
import moment from 'moment-timezone';
import { isTokenExpired } from '@topcoder-platform/tc-auth-lib';
import { config, isomorphy } from 'topcoder-react-utils';
+import { services, tc } from 'topcoder-react-lib';
-import { tc } from 'topcoder-react-lib';
+const { api } = services;
export const {
OLD_COMPETITION_TRACKS,
@@ -196,6 +197,14 @@ export function getAuthTokens(req = {}) {
return { tokenV2, tokenV3 };
}
+/**
+ * Get M2M Token
+ * @return {Promise}
+ */
+export async function getM2mToken() {
+ return api.getTcM2mToken().then((m2mToken => m2mToken));
+}
+
/**
* At the client side it redirects to Topcoder login, with the current URL used
* as the return address. Does nothing at the server side.
diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js
index 5c7d43ca9c..2e12de0a77 100644
--- a/src/shared/utils/url.js
+++ b/src/shared/utils/url.js
@@ -190,7 +190,7 @@ export const getSubPageConfiguration = () => {
}
if (url.includes('/community/arena')) {
- toolName = 'SRMs (Arena)';
+ toolName = 'Single Round Matches (Arena)';
toolRoot = '/community/arena';
loginRedirect = '/community/arena';
type = 'tool';