+
+
{title}
+ {mandatory &&
*mandatory
}
+
+
+
+ {!fileName && !isChallengeBelongToTopgearGroup && (
+
+ Drag and drop your
+ {fileExtensions.join(" or ")} file here.
+
+ )}
+ {!fileName && !isChallengeBelongToTopgearGroup &&
or}
+ {fileName &&
{fileName}
}
+ {_.isNumber(uploadProgress) && uploadProgress < 100 && (
+
Uploading: {uploadProgress}%
+ )}
+ {isChallengeBelongToTopgearGroup && (
+
+ {invalidUrl && (
+
* Invalid URL
+ )}
+
+
+ )}
+
+ {isChallengeBelongToTopgearGroup ? "Set URL" : "Pick a File"}
+
+
+ {!isChallengeBelongToTopgearGroup && (
+
{
+ const path = generateFilePath();
+ filestackRef.current
+ .picker({
+ accept: fileExtensions,
+ fromSources: [
+ "local_file_system",
+ "googledrive",
+ "dropbox",
+ "onedrive",
+ "github",
+ "url",
+ ],
+ maxSize: 500 * 1024 * 1024,
+ onFileUploadFailed: () => setDragged(false),
+ onFileUploadFinished: (file) => {
+ setDragged(false);
+ onSuccess(file, path);
+ },
+ startUploadingWhenMaxFilesReached: true,
+ storeTo: {
+ container: config.FILESTACK.SUBMISSION_CONTAINER,
+ path,
+ region: config.FILESTACK.REGION,
+ },
+ })
+ .open();
+ }}
+ onKeyPress={() => {
+ const path = generateFilePath();
+ filestackRef.current
+ .picker({
+ accept: fileExtensions,
+ fromSources: [
+ "local_file_system",
+ "googledrive",
+ "dropbox",
+ "onedrive",
+ "github",
+ "url",
+ ],
+ maxSize: 500 * 1024 * 1024,
+ onFileUploadFailed: () => setDragged(false),
+ onFileUploadFinished: (file) => {
+ setDragged(false);
+ onSuccess(file, path);
+ },
+ startUploadingWhenMaxFilesReached: true,
+ storeTo: {
+ container: config.FILESTACK.SUBMISSION_CONTAINER,
+ path,
+ region: config.FILESTACK.REGION,
+ },
+ })
+ .open();
+ }}
+ onDragEnter={() => setDragged(true)}
+ onDragLeave={() => setDragged(false)}
+ onDragOver={(e) => e.preventDefault()}
+ onDrop={(e) => {
+ setDragged(false);
+ e.preventDefault();
+ const path = generateFilePath();
+ const filename = e.dataTransfer.files[0].name;
+ if (!fileExtensions.some((ext) => filename.endsWith(ext))) {
+ fireErrorMessage("Wrong file type!", "");
+ return;
+ }
+ setFileName(e.dataTransfer.files[0].name);
+ setUploadProgress(0);
+ filestackRef.current
+ .upload(
+ e.dataTransfer.files[0],
+ {
+ onProgress: ({ totalPercent }) => {
+ setUploadProgress(totalPercent);
+ },
+ progressInterval: 1000,
+ },
+ {
+ container: config.FILESTACK.SUBMISSION_CONTAINER,
+ path,
+ region: config.FILESTACK.REGION,
+ }
+ )
+ .then((file) => onSuccess(file, path));
+ }}
+ role="tab"
+ styleName="drop-zone-mask"
+ tabIndex={0}
+ aria-label="Select file to upload"
+ />
+ )}
+
+
+ {error &&
{error}
}
+
+ );
+};
+
+FilePicker.defaultProps = {};
+
+FilePicker.propTypes = {
+ mandatory: PT.bool,
+ title: PT.string,
+ fileExtensions: PT.arrayOf(PT.string),
+ challengeId: PT.string,
+ error: PT.string,
+ setError: PT.func,
+ fileName: PT.string,
+ uploadProgress: PT.number,
+ setFileName: PT.func,
+ setUploadProgress: PT.func,
+ dragged: PT.bool,
+ setDragged: PT.func,
+ setFilestackData: PT.func,
+ userId: PT.number,
+ isChallengeBelongToTopgearGroup: PT.bool,
+};
+
+export default FilePicker;
diff --git a/src/containers/Submission/Submit/FilePicker/styles.scss b/src/containers/Submission/Submit/FilePicker/styles.scss
new file mode 100644
index 0000000..46d8319
--- /dev/null
+++ b/src/containers/Submission/Submit/FilePicker/styles.scss
@@ -0,0 +1,136 @@
+@import "~styles/variables";
+@import "~styles/mixins";
+
+.container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ width: 33%;
+ margin-right: 2%;
+
+ .file-picker {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+ background-color: #fcfcfc;
+ border-radius: 6px;
+ border: 1px dashed #c3c3c8;
+ min-height: 190px;
+ position: relative;
+
+ @include xs-to-sm {
+ min-height: 150px;
+ }
+
+ p {
+ font-size: 15px;
+ color: $tc-gray-60;
+ line-height: 25px;
+ flex-basis: 0;
+ flex-grow: 1;
+ text-align: center;
+
+ @include xs-to-sm {
+ font-size: 13px;
+ line-height: 15px;
+ }
+ }
+
+ span {
+ font-size: 12px;
+ color: #c3c3c8;
+ flex-basis: 0;
+ flex-grow: 1;
+ position: absolute;
+ left: 50%;
+ top: calc(50% - 12px);
+ }
+
+ .submission-input {
+ position: relative;
+ top: -4000px;
+ }
+
+ .file-name {
+ display: flex;
+ align-items: center;
+ }
+
+ .url-input-container {
+ width: 100%;
+ margin-top: 40px;
+
+ input.invalid {
+ border-color: #f22f24;
+ }
+
+ ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
+ color: $tc-gray-40;
+ opacity: 1; /* Firefox */
+ }
+
+ ::-ms-input-placeholder { /* Microsoft Edge */
+ color: $tc-gray-40;
+ }
+
+ :-ms-input-placeholder { /* Internet Explorer 10-11 */
+ color: $tc-gray-40;
+ }
+ }
+
+ .invalid-url-message {
+ font-size: 12px;
+ color: #f22f24;
+ margin-bottom: 10px;
+ }
+ }
+
+ .file-picker.error {
+ border-color: #f22f24;
+ }
+
+ .file-picker.drag {
+ background-color: rgba(0, 0, 0, 0.1);
+ border-color: rgba(0, 0, 0, 0.4);
+ }
+
+ .desc {
+ font-size: 12px;
+ color: #37373c;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ }
+
+ .mandatory {
+ font-size: 10px;
+ color: #0b71e6;
+ }
+
+ @include xs-to-md {
+ button {
+ padding: 10px;
+ }
+ }
+
+ .error-container {
+ margin-top: 5px;
+ padding: 5px 13px;
+ background: #fff4f4;
+ border: 1px solid #ffd4d1;
+ color: #f22f24;
+ font-size: 13px;
+ border-radius: 2px;
+ font-style: italic;
+ }
+}
+
+.drop-zone-mask {
+ bottom: 0;
+ cursor: pointer;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+}
diff --git a/src/containers/Submission/Submit/Header/index.jsx b/src/containers/Submission/Submit/Header/index.jsx
new file mode 100644
index 0000000..946eef2
--- /dev/null
+++ b/src/containers/Submission/Submit/Header/index.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import PT from "prop-types";
+import { Link } from "@reach/router";
+import config from '../../../../../config'
+
+import "./styles.scss";
+
+const Header = ({ title, challengeId }) => {
+ return (
+
+ );
+};
+
+Header.defaultProps = {};
+
+Header.propTypes = {
+ title: PT.string,
+ challengeId: PT.string,
+};
+
+export default Header;
diff --git a/src/containers/Submission/Submit/Header/styles.scss b/src/containers/Submission/Submit/Header/styles.scss
new file mode 100644
index 0000000..35b525c
--- /dev/null
+++ b/src/containers/Submission/Submit/Header/styles.scss
@@ -0,0 +1,63 @@
+@import "~styles/variables";
+@import "~styles/mixins";
+
+.header {
+ @include roboto-regular;
+
+ display: flex;
+ padding: 30px 60px;
+ background-color: #fcfcfc;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #f0f0f0;
+
+ @include xs-to-md {
+ padding: 30px 20px;
+ }
+
+ a {
+ font-size: 18px;
+ color: $tc-gray-60;
+ font-weight: 300;
+ display: flex;
+ align-items: center;
+
+ span {
+ font-size: 54px;
+ display: block;
+ position: relative;
+ top: -3px;
+ margin-right: 6px;
+ }
+ }
+
+ h1 {
+ font-weight: 300;
+ font-size: 24px;
+ color: #3d3d3d;
+ line-height: 30px;
+
+ @include xs-to-md {
+ font-size: 20px;
+ }
+
+ @include xs-to-sm {
+ font-size: 16px;
+ width: 80%;
+ line-height: 24px;
+ }
+
+ @media (max-width: 720px) {
+ font-size: 20px;
+ width: 80%;
+ text-align: right;
+ }
+ }
+
+ p {
+ @include xs-to-sm {
+ display: none;
+ }
+ }
+}
diff --git a/src/containers/Submission/Submit/SubmitForm/index.jsx b/src/containers/Submission/Submit/SubmitForm/index.jsx
new file mode 100644
index 0000000..8d2798b
--- /dev/null
+++ b/src/containers/Submission/Submit/SubmitForm/index.jsx
@@ -0,0 +1,321 @@
+/**
+ * components.page.challenge-details.Submit
+ *
Component
+ *
+ * Description:
+ * Page that is shown when a user is trying to submit a Submission.
+ * Allows user to upload Submission.zip file using a Filestack plugin.
+ */
+/* eslint-env browser */
+
+import React, { useEffect, useRef } from "react";
+import PT from "prop-types";
+import _ from "lodash";
+import { PrimaryButton } from "components/Buttons";
+import LoadingIndicator from "components/LoadingIndicator";
+import { COMPETITION_TRACKS } from "../../../../constants";
+import FilePicker from "../FilePicker";
+import Uploading from "../Uploading";
+import * as util from "../../../../utils/submission";
+
+import "./styles.scss";
+
+const SubmitForm = ({
+ challengeId,
+ challengeName,
+ phases,
+ track,
+ agreed,
+ filePickers,
+ submissionFilestackData,
+ userId,
+ groups,
+ communityList,
+ isCommunityListLoaded,
+
+ errorMsg,
+ isSubmitting,
+ submitDone,
+ uploadProgress,
+
+ resetForm,
+ setAgreed,
+ setFilePickerError,
+ setFilePickerFileName,
+ setFilePickerUploadProgress,
+ setFilePickerDragged,
+ setSubmissionFilestackData,
+ submit,
+}) => {
+ const propsRef = useRef();
+ propsRef.current = { resetForm };
+
+ useEffect(() => {
+ return () => {
+ propsRef.current.resetForm();
+ };
+ }, []);
+
+ const getFormData = () => {
+ const subType = util.getSubmissionDetail({ phases });
+
+ const formData = new FormData();
+ formData.append("type", subType);
+ formData.append("url", submissionFilestackData.fileUrl);
+ formData.append("memberId", userId || "");
+ formData.append("challengeId", challengeId);
+
+ if (submissionFilestackData.fileType) {
+ formData.append("fileType", submissionFilestackData.fileType);
+ }
+
+ return formData;
+ };
+
+ const reset = () => {
+ setAgreed(false);
+ resetForm();
+ };
+
+ /* User has clicked to go retry the submission after an error */
+ const retry = () => {
+ submit(getFormData());
+ };
+
+ /* User has clicked to go back to a new submission after a successful submit */
+ const back = () => {
+ resetForm();
+ };
+
+ /* User has clicked submit, prepare formData for the V2 API and start upload */
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ submit(getFormData());
+ };
+
+ const id = "file-picker-submission";
+
+ const isLoadingCommunitiesList = !isCommunityListLoaded;
+ const isChallengeBelongToTopgearGroup = util.isChallengeBelongToTopgearGroup(
+ { groups },
+ communityList
+ );
+
+ // Find the state for FilePicker with id of 1 or assign default values
+ const fpState = filePickers.find((fp) => fp.id === id) || {
+ id,
+ error: "",
+ fileName: "",
+ dragged: false,
+ uploadProgress: 0,
+ };
+
+ const isUploadingState = !(!isSubmitting && !submitDone && !errorMsg);
+ if (isUploadingState) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+SubmitForm.defaultProps = {};
+
+SubmitForm.propTypes = {
+ challengeId: PT.string,
+ challengeName: PT.string,
+ phases: PT.arrayOf(PT.shape()),
+ track: PT.string,
+ agreed: PT.bool,
+ filePickers: PT.arrayOf(PT.shape()),
+ submissionFilestackData: PT.shape(),
+ userId: PT.number,
+ groups: PT.arrayOf(PT.shape()),
+ communityList: PT.arrayOf(PT.shape()),
+ isCommunityListLoaded: PT.bool,
+
+ errorMsg: PT.string,
+ isSubmitting: PT.bool,
+ submitDone: PT.bool,
+ uploadProgress: PT.number,
+
+ resetForm: PT.func,
+ setAgreed: PT.func,
+ setFilePickerError: PT.func,
+ setFilePickerFileName: PT.func,
+ setFilePickerUploadProgress: PT.func,
+ setFilePickerDragged: PT.func,
+ setSubmissionFilestackData: PT.func,
+ submit: PT.func,
+};
+
+export default SubmitForm;
diff --git a/src/containers/Submission/Submit/SubmitForm/styles.scss b/src/containers/Submission/Submit/SubmitForm/styles.scss
new file mode 100644
index 0000000..22cec4c
--- /dev/null
+++ b/src/containers/Submission/Submit/SubmitForm/styles.scss
@@ -0,0 +1,249 @@
+@import "~styles/variables";
+@import "~styles/mixins";
+
+.design-content {
+ @include roboto-regular;
+
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ background-color: white;
+ width: 100%;
+ padding: 5px 60px;
+
+ @include sm-to-md {
+ padding: 0;
+ }
+
+ .submission-hints {
+ margin-bottom: 2em;
+ font-size: 15px;
+ color: #37373c;
+
+ ol {
+ list-style: decimal;
+ margin-left: 15px;
+ margin-bottom: 1em;
+
+ li {
+ line-height: 20px;
+ }
+ }
+ }
+
+ .row {
+ display: flex;
+ padding: 30px 0;
+ margin-bottom: 70px;
+
+ @include sm-to-md {
+ margin: 0 20px;
+ }
+
+ @include sm {
+ flex-direction: column;
+ }
+ }
+
+ a {
+ color: $tc-dark-blue-110;
+ cursor: pointer;
+ }
+
+ .left {
+ padding-right: 33px;
+ border-right: 6px solid #f6f6f6;
+ max-width: 310px;
+
+ @include md-to-lg {
+ padding-right: 20px;
+ }
+
+ @include sm {
+ border-right: none;
+ max-width: 100%;
+ }
+
+ p,
+ a {
+ font-size: 13px;
+ color: $tc-gray-60;
+ font-style: italic;
+ max-width: 270px;
+ width: 270px;
+ line-height: 20px;
+ margin-bottom: 12px;
+
+ @media (min-width: 800px) and (max-width: 1000px) {
+ max-width: 220px;
+ width: 220px;
+ }
+
+ @media (min-width: 768px) and (max-width: 800px) {
+ max-width: 200px;
+ width: 200px;
+ }
+
+ @include sm {
+ max-width: 100%;
+ width: 100%;
+ }
+
+ @include sm-to-md {
+ margin-bottom: 25px;
+ }
+ }
+
+ h2 {
+ font-size: 15px;
+ line-height: 25px;
+ margin-bottom: 4px;
+ }
+ }
+
+ .right {
+ width: 100%;
+ margin-left: 60px;
+
+ @media (min-width: 768px) and (max-width: 1280px) {
+ margin-left: 30px;
+ }
+
+ @include sm {
+ margin-left: 0;
+ }
+
+ & > p {
+ font-size: 15px;
+ color: #37373c;
+ margin-bottom: 10px;
+ }
+
+ button {
+ height: 30px;
+ font-size: 13px;
+ line-height: 0;
+ }
+ }
+
+ .file-picker-container {
+ display: flex;
+ margin-bottom: 40px;
+
+ @include xs-to-md {
+ margin-bottom: 25px;
+ }
+
+ & > div {
+ margin-right: 16px;
+ }
+ }
+
+ .agree {
+ background-color: #fcfcfc;
+ border-top: 1px solid #f0f0f0;
+ border-bottom: 1px solid #f0f0f0;
+ margin-bottom: 30px;
+ padding: 30px 60px;
+ flex-direction: column;
+ align-items: center;
+
+ @include xs-to-lg {
+ margin: 0;
+ }
+
+ @include xs-to-md {
+ padding: 30px;
+ }
+
+ p {
+ font-size: 13px;
+ color: #37373c;
+ line-height: 24px;
+ margin-bottom: 50px;
+ }
+
+ a {
+ color: $tc-dark-blue-110;
+ }
+
+ .tc-checkbox-label {
+ line-height: 15px;
+ margin-left: 21px;
+ user-select: none;
+ cursor: pointer;
+ width: 195px;
+ font-size: 15px;
+ color: #3d3d3d;
+ }
+
+ .tc-checkbox {
+ height: 15px;
+ width: 210px;
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+ position: relative;
+ display: inline-block;
+ margin-bottom: 30px;
+
+ input[type=checkbox] {
+ visibility: visible;
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+ }
+
+ label {
+ cursor: pointer;
+ position: absolute;
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+ top: 0;
+ left: 0;
+ border-radius: $corner-radius;
+ box-shadow: none;
+ border: 1px solid $tc-gray-50;
+ background: $tc-gray-neutral-light;
+ transition: all 0.15s ease-in-out;
+
+ &::after {
+ opacity: 0;
+ content: '';
+ position: absolute;
+ width: 9px;
+ height: 5px;
+ background: transparent;
+ top: 3px;
+ left: 2px;
+ border: 3px solid $tc-dark-blue;
+ border-top: none;
+ border-right: none;
+ transform: rotate(-45deg);
+ transition: all 0.15s ease-in-out;
+ }
+
+ &:hover::after {
+ opacity: 0.3;
+ }
+ }
+
+ input[type=checkbox]:checked ~ label {
+ background: $tc-dark-blue;
+ border-color: $tc-dark-blue;
+ }
+
+ input[type=checkbox]:focus ~ label {
+ outline: 2px auto $tc-dark-blue;
+ }
+
+ input[type=checkbox]:checked + label::after {
+ opacity: 1;
+ border-color: $tc-white;
+ }
+ }
+ }
+}
diff --git a/src/containers/Submission/Submit/Uploading/index.jsx b/src/containers/Submission/Submit/Uploading/index.jsx
new file mode 100644
index 0000000..8c712dd
--- /dev/null
+++ b/src/containers/Submission/Submit/Uploading/index.jsx
@@ -0,0 +1,125 @@
+import React from "react";
+import PT from "prop-types";
+import { Link } from "@reach/router";
+import { PrimaryButton, DefaultButton as Button } from "components/Buttons";
+import { COMPETITION_TRACKS, CHALLENGES_URL } from "../../../../constants";
+import RobotHappy from "assets/icons/robot-happy.svg";
+import RobotSad from "assets/icons/robot-embarassed.svg";
+
+import "./styles.scss";
+
+const Uploading = ({
+ challengeId,
+ challengeName,
+ error,
+ isSubmitting,
+ submitDone,
+ reset,
+ retry,
+ track,
+ uploadProgress,
+ back,
+}) => {
+ return (
+
+
+ {isSubmitting &&
UPLOADING SUBMISSION FOR
}
+ {submitDone &&
SUBMISSION COMPLETED FOR
}
+ {error &&
ERROR SUBMITTING FOR
}
+
+ {isSubmitting &&
“{challengeName}”
}
+ {(submitDone || error) && (
+
+ {challengeName}
+
+ )}
+
+ {(isSubmitting || submitDone) &&
}
+ {error &&
}
+
+ {isSubmitting && (
+
+ Hey, your work is AWESOME! Please don't close this window while
+ I'm working, you'll lose all files!
+
+ )}
+ {isSubmitting && !submitDone && (
+
+ )}
+
+ {isSubmitting && !submitDone && (
+
+ Uploaded: {(100 * uploadProgress).toFixed()}%
+
+ )}
+ {error && (
+
+ Oh, that’s embarrassing! The file couldn’t be uploaded, I’m so
+ sorry.
+
+ )}
+
+ {error &&
{error}
}
+ {error && (
+
+
+
retry()}>Try Again
+
+ )}
+ {submitDone && !error && (
+
+ Thanks for participating! We’ve received your submission and will
+ send you an email shortly to confirm and explain what happens next.
+
+ )}
+ {submitDone && !error && (
+
+ {track === COMPETITION_TRACKS.DES ? (
+
+
+ back()}
+ >
+ View My Submissions
+
+
+ ) : (
+
+
+ back()}
+ >
+ Back to Challenge
+
+
+ )}
+
+ )}
+
+
+ );
+};
+
+Uploading.defaultProps = {};
+
+Uploading.propTypes = {
+ challengeId: PT.string,
+ challengeName: PT.string,
+ error: PT.string,
+ isSubmitting: PT.bool,
+ submitDone: PT.string,
+ reset: PT.func,
+ retry: PT.func,
+ track: PT.string,
+ uploadProgress: PT.number,
+ back: PT.func,
+};
+
+export default Uploading;
diff --git a/src/containers/Submission/Submit/Uploading/styles.scss b/src/containers/Submission/Submit/Uploading/styles.scss
new file mode 100644
index 0000000..54bfb41
--- /dev/null
+++ b/src/containers/Submission/Submit/Uploading/styles.scss
@@ -0,0 +1,90 @@
+@import "~styles/variables";
+@import "~styles/mixins";
+
+.container {
+ @include roboto-regular;
+
+ align-items: center;
+ width: 100%;
+ background-color: white;
+ left: 0;
+ z-index: 100000;
+ display: flex;
+ justify-content: center;
+ position: relative;
+ padding: 30px;
+}
+
+.link,
+.link:visited {
+ font-size: 20px;
+ line-height: 30px;
+ color: #1a85ff;
+}
+
+.uploading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+
+ .error-msg {
+ width: 500px;
+ margin-bottom: 45px;
+ padding: 5px 13px;
+ background: #fff4f4;
+ border: 1px solid #ffd4d1;
+ color: #f22f24;
+ font-size: 13px;
+ border-radius: 2px;
+ font-style: italic;
+ text-align: center;
+ }
+
+ .progress-container {
+ width: 650px;
+ height: 10px;
+ background-color: $tc-gray-10;
+ border-radius: 12px;
+
+ .progress-bar {
+ width: 0%;
+ height: 10px;
+ background-color: $tc-dark-blue;
+ border-radius: 12px;
+ }
+ }
+
+ h3 {
+ font-size: 20px;
+ line-height: 30px;
+ color: #3d3d3d;
+ }
+
+ svg {
+ margin-top: 50px;
+ }
+
+ p {
+ font-size: 15px;
+ color: #a3a3ad;
+ width: 80%;
+ text-align: center;
+ line-height: 18px;
+ margin-bottom: 30px;
+ margin-top: 30px;
+ }
+
+ .submitting {
+ margin-top: 20px;
+ font-size: 13px;
+ color: $tc-black;
+ }
+
+ .button-container {
+ align-items: center;
+ flex-wrap: wrap;
+ display: flex;
+ justify-content: center;
+ }
+}
diff --git a/src/containers/Submission/Submit/index.jsx b/src/containers/Submission/Submit/index.jsx
new file mode 100644
index 0000000..9f9e2a0
--- /dev/null
+++ b/src/containers/Submission/Submit/index.jsx
@@ -0,0 +1,122 @@
+import React from "react";
+import PT from "prop-types";
+import * as util from "../../../utils/submission";
+import Header from "./Header";
+import SubmitForm from "./SubmitForm";
+
+import "./styles.scss";
+
+const Submit = ({
+ challengeId,
+ challengeName,
+ phases,
+ status,
+ winners,
+ groups,
+ handle,
+ communityList,
+ isCommunityListLoaded,
+
+ track,
+ agreed,
+ filePickers,
+ submissionFilestackData,
+ userId,
+
+ errorMsg,
+ isSubmitting,
+ submitDone,
+ uploadProgress,
+
+ resetForm,
+ setAgreed,
+ setFilePickerError,
+ setFilePickerFileName,
+ setFilePickerUploadProgress,
+ setFilePickerDragged,
+ setSubmissionFilestackData,
+ submit,
+}) => {
+ const submissionEnded = util.isSubmissionEnded({ status, phases });
+ const canSubmitFinalFixes = util.canSubmitFinalFixes(
+ { winners, phases },
+ handle
+ );
+
+ const submissionPermitted = !submissionEnded || canSubmitFinalFixes;
+
+ return (
+
+
+
+ {submissionPermitted ? (
+
+ ) : (
+
+
Submissions are not permitted at this time.
+
+ )}
+
+
+ );
+};
+
+Submit.defaultProps = {};
+
+Submit.propTypes = {
+ challengeId: PT.string,
+ challengeName: PT.string,
+ phases: PT.arrayOf(PT.shape()),
+ status: PT.string,
+ winners: PT.arrayOf(PT.shape()),
+ groups: PT.arrayOf(PT.shape()),
+ handle: PT.string,
+ communityList: PT.arrayOf(PT.shape()),
+ isCommunityListLoaded: PT.bool,
+
+ track: PT.string,
+ agreed: PT.bool,
+ filePickers: PT.arrayOf(PT.shape()),
+ submissionFilestackData: PT.shape(),
+ userId: PT.number,
+
+ errorMsg: PT.string,
+ isSubmitting: PT.bool,
+ submitDone: PT.bool,
+ uploadProgress: PT.number,
+
+ resetForm: PT.func,
+ setAgreed: PT.func,
+ setFilePickerError: PT.func,
+ setFilePickerFileName: PT.func,
+ setFilePickerUploadProgress: PT.func,
+ setFilePickerDragged: PT.func,
+ setSubmissionFilestackData: PT.func,
+ submit: PT.func,
+};
+
+export default Submit;
diff --git a/src/containers/Submission/Submit/styles.scss b/src/containers/Submission/Submit/styles.scss
new file mode 100644
index 0000000..697bbba
--- /dev/null
+++ b/src/containers/Submission/Submit/styles.scss
@@ -0,0 +1,37 @@
+@import "~styles/mixins";
+
+.container {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ padding: 80px 0 40px;
+ background: #fff;
+ min-height: calc(100vh - var(--navbarHeight, 60px));
+
+ @include md-to-lg {
+ padding-bottom: 0;
+ }
+
+ @include xs-to-lg {
+ padding: 10px;
+ }
+
+ .content {
+ display: flex;
+ flex-direction: column;
+
+ @include md-to-lg {
+ width: 100%;
+ }
+
+ @include xl {
+ width: 1242px;
+ }
+
+ .not-permitted {
+ background-color: white;
+ text-align: center;
+ padding: 40px;
+ }
+ }
+}
diff --git a/src/containers/Submission/index.jsx b/src/containers/Submission/index.jsx
new file mode 100644
index 0000000..9c1e1ea
--- /dev/null
+++ b/src/containers/Submission/index.jsx
@@ -0,0 +1,244 @@
+import React, { useEffect, useLayoutEffect, useRef } from "react";
+import PT from "prop-types";
+import { connect } from "react-redux";
+import { navigate } from "@reach/router";
+import { PrimaryButton } from "components/Buttons";
+import AccessDenied from "components/AccessDenied";
+import LoadingIndicator from "components/LoadingIndicator";
+import { ACCESS_DENIED_REASON, CHALLENGES_URL } from "../../constants";
+import Submit from "./Submit";
+import actions from "../../actions";
+import { isLegacyId, isUuid } from "../../utils/challenge";
+
+const Submission = ({
+ id,
+ challengeId,
+ challengeName,
+ isRegistered,
+ phases,
+ status,
+ winners,
+ groups,
+ isAuthInitialized,
+ communityList,
+ isCommunityListLoaded,
+ getCommunityList,
+ isLoadingChallenge,
+ isChallengeLoaded,
+
+ track,
+ agreed,
+ filePickers,
+ submissionFilestackData,
+ userId,
+ handle,
+
+ errorMsg,
+ isSubmitting,
+ submitDone,
+ uploadProgress,
+
+ getChallenge,
+ submit,
+ resetForm,
+ setAgreed,
+ setFilePickerError,
+ setFilePickerFileName,
+ setFilePickerUploadProgress,
+ setFilePickerDragged,
+ setSubmissionFilestackData,
+ setAuth,
+}) => {
+ const propsRef = useRef();
+ propsRef.current = {
+ id,
+ challengeId,
+ getCommunityList,
+ setAuth,
+ getChallenge,
+ };
+
+ useLayoutEffect(() => {
+ if (isLegacyId(propsRef.current.id) || isUuid(propsRef.current.id)) {
+ propsRef.current.getCommunityList();
+ propsRef.current.setAuth();
+ propsRef.current.getChallenge(propsRef.current.id);
+ } else {
+ navigate(CHALLENGES_URL);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (isChallengeLoaded && isLegacyId(propsRef.current.id)) {
+ navigate(`${CHALLENGES_URL}/${propsRef.current.challengeId}/submit`);
+ }
+ }, [isChallengeLoaded]);
+
+ if (isLoadingChallenge || !isAuthInitialized) {
+ return ;
+ }
+
+ if (!isChallengeLoaded) {
+ return null;
+ }
+
+ if (!isRegistered) {
+ return (
+
+
+ Go to Challenge Details
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+Submission.defaultProps = {};
+
+Submission.propTypes = {
+ id: PT.string,
+ challengeId: PT.string,
+ challengeName: PT.string,
+ isRegistered: PT.bool,
+ phases: PT.arrayOf(PT.shape()),
+ status: PT.string,
+ winners: PT.arrayOf(PT.shape()),
+ groups: PT.arrayOf(PT.shape()),
+ isAuthInitialized: PT.bool,
+ communityList: PT.arrayOf(PT.shape()),
+ isCommunityListLoaded: PT.bool,
+ getCommunityList: PT.func,
+ isLoadingChallenge: PT.bool,
+ isChallengeLoaded: PT.bool,
+
+ track: PT.string,
+ agreed: PT.bool,
+ filePickers: PT.arrayOf(PT.shape()),
+ submissionFilestackData: PT.shape(),
+ userId: PT.number,
+ handle: PT.string,
+
+ errorMsg: PT.string,
+ isSubmitting: PT.bool,
+ submitDone: PT.bool,
+ uploadProgress: PT.number,
+
+ getChallenge: PT.func,
+ submit: PT.func,
+ resetForm: PT.func,
+ setAgreed: PT.func,
+ setFilePickerError: PT.func,
+ setFilePickerFileName: PT.func,
+ setFilePickerUploadProgress: PT.func,
+ setFilePickerDragged: PT.func,
+ setSubmissionFilestackData: PT.func,
+ setAuth: PT.func,
+};
+
+const mapStateToProps = (state, ownProps) => {
+ const challenge = state?.challenge?.challenge;
+
+ return {
+ id: ownProps.challengeId,
+ challengeId: challenge?.id,
+ challengeName: challenge?.name,
+ isRegistered: challenge?.isRegistered,
+ phases: challenge?.phases,
+ status: challenge?.status,
+ winners: challenge?.winners,
+ groups: challenge?.groups,
+ handle: state.auth.user ? state.auth.user.handle : "",
+ isAuthInitialized: state.auth.isAuthInitialized,
+ communityList: state.lookup.subCommunities,
+ isCommunityListLoaded: state.lookup.isSubCommunitiesLoaded,
+ isLoadingChallenge: state.challenge?.isLoadingChallenge,
+ isChallengeLoaded: state.challenge?.isChallengeLoaded,
+
+ track: challenge?.track,
+ agreed: state.submission.agreed,
+ filePickers: state.submission.filePickers,
+ submissionFilestackData: state.submission.submissionFilestackData,
+ userId: state.auth.user ? state.auth.user.userId : null,
+
+ errorMsg: state.submission.submitErrorMsg,
+ isSubmitting: state.submission.isSubmitting,
+ submitDone: state.submission.submitDone,
+ uploadProgress: state.submission.uploadProgress,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ const onProgress = (event) =>
+ dispatch(actions.submission.submit.uploadProgress(event));
+
+ return {
+ getCommunityList: () => {
+ dispatch(actions.lookup.getCommunityListDone());
+ },
+ setAuth: () => {
+ dispatch(actions.auth.setAuthDone());
+ },
+ getChallenge: (challengeId) => {
+ dispatch(actions.challenge.getChallengeInit(challengeId));
+ dispatch(actions.challenge.getChallengeDone(challengeId));
+ },
+ submit: (data) => {
+ dispatch(actions.submission.submit.submitInit());
+ dispatch(actions.submission.submit.submitDone(data, onProgress));
+ },
+ setAgreed: (agreed) => {
+ dispatch(actions.submission.submit.setAgreed(agreed));
+ },
+ setFilePickerError: (id, error) => {
+ dispatch(actions.submission.submit.setFilePickerError(id, error));
+ },
+ setFilePickerFileName: (id, fileName) => {
+ dispatch(actions.submission.submit.setFilePickerFileName(id, fileName));
+ },
+ setFilePickerDragged: (id, dragged) => {
+ dispatch(actions.submission.submit.setFilePickerDragged(id, dragged));
+ },
+ setFilePickerUploadProgress: (id, p) => {
+ dispatch(actions.submission.submit.setFilePickerUploadProgress(id, p));
+ },
+ setSubmissionFilestackData: (id, data) => {
+ dispatch(actions.submission.submit.setSubmissionFilestackData(id, data));
+ },
+ resetForm: () => {
+ dispatch(actions.submission.submit.submitReset());
+ },
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Submission);
diff --git a/src/containers/SubmissionManagement/MySubmissions/ScreeningDetails/index.jsx b/src/containers/SubmissionManagement/MySubmissions/ScreeningDetails/index.jsx
new file mode 100644
index 0000000..bbe700c
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/ScreeningDetails/index.jsx
@@ -0,0 +1,104 @@
+import React from "react";
+import PT from "prop-types";
+
+import "./styles.scss";
+
+const ScreeningDetails = ({ screening, helpPageUrl }) => {
+ const hasWarnings = screening.warnings;
+ const hasStatus = screening.status;
+ const hasStatusPassed = hasStatus === "passed";
+ const hasStatusFailed = hasStatus === "failed";
+ const hasPending = screening.status === "pending";
+ const warnLength = screening.warnings && hasWarnings.length;
+
+ let statusInfo;
+ if (hasPending) {
+ statusInfo = {
+ title: "Pending",
+ classname: "pending",
+ message:
+ "Your submission has been received, and will be screened after the end of the phase",
+ };
+ } else if (hasStatusPassed && !hasWarnings) {
+ statusInfo = {
+ title: "Passed Screening",
+ classname: "passed",
+ message: "You have passed screening.",
+ };
+ } else if (hasStatusFailed && !hasWarnings) {
+ statusInfo = {
+ title: "Failed Screening",
+ classname: "failed",
+ message: "You have failed screening",
+ };
+ } else if (hasStatusPassed && hasWarnings) {
+ statusInfo = {
+ title: "Passed Screening with Warnings",
+ classname: "passed",
+ message: `You have passed screening, but the screener has given you ${warnLength} warnings that you must fix in round 2.`,
+ };
+ } else if (hasStatusFailed && hasWarnings) {
+ statusInfo = {
+ title: "Failed Screening with Warnings",
+ classname: "failed",
+ message:
+ "You have failed screening and the screener has given you the following warning.",
+ };
+ } else {
+ statusInfo = {
+ title: "",
+ classname: "",
+ message:
+ "Your submission has been received, and will be evaluated during Review phase.",
+ };
+ }
+
+ const warnings = (screening.warnings || []).map((warning, i) => (
+
+
+ Warning {`${1 + i} : ${warning.brief}`}
+
+
{warning.details}
+
+ ));
+
+ return (
+
+
+
+ {statusInfo.title}
+
+
+
{statusInfo.message}
+
+ {warnings}
+ {(hasStatusFailed || (hasStatusPassed && hasWarnings)) && (
+
+ Need more info on how to pass screening? Go to help to read Rules &
+ Policies.
+
+ )}
+
+
+
+ );
+};
+
+ScreeningDetails.defaultProps = {
+ screening: {},
+};
+
+ScreeningDetails.propTypes = {
+ screening: PT.shape({}),
+ helpPageUrl: PT.string,
+};
+
+export default ScreeningDetails;
diff --git a/src/containers/SubmissionManagement/MySubmissions/ScreeningDetails/styles.scss b/src/containers/SubmissionManagement/MySubmissions/ScreeningDetails/styles.scss
new file mode 100644
index 0000000..8f0b815
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/ScreeningDetails/styles.scss
@@ -0,0 +1,90 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$status-space-10: $base-unit * 2;
+$status-space-15: $base-unit * 3;
+$status-space-20: $base-unit * 4;
+$status-space-25: $base-unit * 5;
+$gray-color: $tc-gray-80;
+$green-color: $tc-green;
+$red-color: $tc-red;
+
+.online-review-link {
+ background: none;
+}
+
+.screening-details {
+ background: $tc-gray-neutral-light;
+ font-weight: 400;
+ font-size: 15px;
+ color: $gray-color;
+ letter-spacing: 0;
+ line-height: $status-space-25;
+
+ .screening-details-head {
+ display: flex;
+ margin-bottom: $base-unit;
+
+ .status-title {
+ font-weight: 700;
+ color: $tc-orange;
+ letter-spacing: 0;
+ line-height: $status-space-20;
+
+ .passed {
+ color: $green-color;
+ }
+
+ &.failed {
+ color: $red-color;
+ }
+
+ &.pending {
+ color: $gray-color;
+ }
+ }
+
+ .online-review-link {
+ display: block;
+ margin-left: auto;
+ line-height: $status-space-20;
+ font-weight: 400;
+ font-size: 13px;
+ color: $tc-dark-blue-110;
+ text-decoration: underline;
+ padding: 0;
+ border: none;
+ text-transform: capitalize;
+
+ &:hover {
+ opacity: 0.7;
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+
+ .screening-warning {
+ margin-top: $status-space-15;
+ }
+
+ .more-info {
+ margin-top: $status-space-15;
+ margin-bottom: $base-unit;
+ }
+
+ .warning-bold {
+ font-weight: 700;
+ line-height: $status-space-20;
+ }
+
+ .help-btn {
+ text-align: right;
+ }
+
+ .help-link {
+ padding: 1px 6px;
+ }
+}
diff --git a/src/containers/SubmissionManagement/MySubmissions/ScreeningStatus/index.jsx b/src/containers/SubmissionManagement/MySubmissions/ScreeningStatus/index.jsx
new file mode 100644
index 0000000..04deef1
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/ScreeningStatus/index.jsx
@@ -0,0 +1,66 @@
+import React from "react";
+import PT from "prop-types";
+
+import "./styles.scss";
+
+const ScreeningStatus = ({ screening, onShowDetails, submissionId }) => {
+ const hasWarnings = screening.warnings;
+ const hasStatus = screening.status;
+ const hasStatusPassed = hasStatus === "passed";
+ const hasStatusFailed = hasStatus === "failed";
+ const hasPending = screening.status === "pending";
+ const warnLength = screening.warnings && hasWarnings.length;
+
+ let className;
+ if (hasPending) {
+ className = "pending";
+ } else if (hasStatusPassed && !hasWarnings) {
+ className = "pass-with-no-warn";
+ } else if (hasStatusFailed && !hasWarnings) {
+ className = "fail-with-no-warn";
+ } else {
+ className = "has-warn";
+ }
+
+ let statusClassName;
+ if (hasStatusPassed && hasWarnings) {
+ statusClassName = "passed";
+ } else if (hasStatusFailed && hasWarnings) {
+ statusClassName = "failed";
+ } else {
+ statusClassName = "";
+ }
+
+ return (
+
+ );
+};
+
+ScreeningStatus.defaultProps = {};
+
+ScreeningStatus.propTypes = {
+ screening: PT.shape(),
+ onShowDetails: PT.func,
+ submissionId: PT.string,
+};
+
+export default ScreeningStatus;
diff --git a/src/containers/SubmissionManagement/MySubmissions/ScreeningStatus/styles.scss b/src/containers/SubmissionManagement/MySubmissions/ScreeningStatus/styles.scss
new file mode 100644
index 0000000..86e5937
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/ScreeningStatus/styles.scss
@@ -0,0 +1,70 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$status-space-10: $base-unit * 2;
+$status-space-20: $base-unit * 4;
+$status-space-40: $base-unit * 8;
+$gray-color: #a3a3ad;
+$green-color: #60c700;
+$red-color: #f22f24;
+
+.status {
+ font-weight: 700;
+ display: inline-block;
+ padding: $base-unit $status-space-10;
+ border-radius: $status-space-40 0 0 $status-space-40;
+ text-transform: capitalize;
+
+ &.passed {
+ background: $tc-orange;
+ }
+
+ &.failed {
+ background: $red-color;
+ }
+}
+
+.screening-status {
+ background: $tc-gray-50;
+ border-radius: $status-space-40;
+ font-weight: 400;
+ font-size: 13px;
+ line-height: $status-space-20;
+ color: $tc-white;
+ padding: $base-unit $status-space-10;
+ display: inline-block;
+ text-transform: initial;
+ cursor: pointer;
+
+ &.has-warn {
+ padding: 0 $status-space-10 0 0;
+ }
+
+ &.pass-with-no-warn {
+ background: $green-color;
+
+ .status {
+ padding: 0 $base-unit;
+ }
+ }
+
+ &.fail-with-no-warn {
+ background: $red-color;
+
+ .status {
+ padding: 0 $base-unit;
+ }
+ }
+
+ &.pending {
+ background: transparent;
+ font-size: 15px;
+ color: $gray-color;
+ font-style: italic;
+ }
+}
+
+.warning {
+ padding-left: $status-space-10;
+ display: inline-block;
+}
diff --git a/src/containers/SubmissionManagement/MySubmissions/SubmissionRow/index.jsx b/src/containers/SubmissionManagement/MySubmissions/SubmissionRow/index.jsx
new file mode 100644
index 0000000..e37c43d
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/SubmissionRow/index.jsx
@@ -0,0 +1,86 @@
+import React from "react";
+import PT from "prop-types";
+import moment from "moment";
+import ScreeningStatus from "../ScreeningStatus";
+import DeleteIcon from "assets/icons/IconTrashSimple.svg";
+import DownloadIcon from "assets/icons/IconSquareDownload.svg";
+import ExpandIcon from "assets/icons/IconMinimalDown.svg";
+import { COMPETITION_TRACKS, CHALLENGE_STATUS } from "../../../../constants";
+
+import "./styles.scss";
+
+const SubmissionRow = ({
+ submission,
+ showScreeningDetails,
+ track,
+ onDownload,
+ onDelete,
+ onShowDetails,
+ status,
+ allowDelete,
+}) => {
+ const formatDate = (date) =>
+ moment(+new Date(date)).format("MMM DD, YYYY hh:mm A");
+
+ return (
+
+
+ {submission.id}
+ {submission.legacySubmissionId}
+ |
+ {submission.type} |
+ {formatDate(submission.created)} |
+ {track === COMPETITION_TRACKS.DES && (
+
+ {submission.screening && (
+
+ )}
+ |
+ )}
+
+
+
+ {status !== CHALLENGE_STATUS.COMPLETED &&
+ track !== COMPETITION_TRACKS.DES && (
+
+ )}
+
+
+ |
+
+ );
+};
+
+SubmissionRow.defaultProps = {};
+
+SubmissionRow.propTypes = {
+ submission: PT.shape({}),
+ showScreeningDetails: PT.bool,
+ track: PT.string,
+ onDownload: PT.func,
+ onDelete: PT.func,
+ onShowDetails: PT.func,
+ status: PT.string,
+ allowDelete: PT.bool,
+};
+
+export default SubmissionRow;
diff --git a/src/containers/SubmissionManagement/MySubmissions/SubmissionRow/styles.scss b/src/containers/SubmissionManagement/MySubmissions/SubmissionRow/styles.scss
new file mode 100644
index 0000000..223b44d
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/SubmissionRow/styles.scss
@@ -0,0 +1,166 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$submission-space-10: $base-unit * 2;
+$submission-space-20: $base-unit * 4;
+$submission-space-25: $base-unit * 5;
+$submission-space-50: $base-unit * 10;
+
+.submission-row {
+ width: 100%;
+ font-size: 15px;
+ color: $tc-black;
+ font-weight: 400;
+
+ @include xs-to-sm {
+ display: block;
+ position: relative;
+ padding: 10px 0;
+ }
+
+ td {
+ vertical-align: middle;
+ padding: $submission-space-20;
+ background: $tc-white;
+ border-top: 1px solid $tc-gray-10;
+
+ @include xs-to-lg {
+ padding: $submission-space-10;
+ }
+
+ @include xs-to-sm {
+ display: block;
+ border: none;
+ }
+
+ &.no-submission {
+ line-height: $submission-space-20;
+ padding: $submission-space-50 $submission-space-20;
+ text-align: center;
+ }
+
+ &.dev-details {
+ padding-right: 60px;
+ }
+ }
+
+ .preview-col {
+ @include xs-to-sm {
+ float: left;
+ }
+
+ .design-img {
+ width: 90px;
+ height: 90px;
+
+ @include xs-to-sm {
+ width: 80px;
+ height: 80px;
+ }
+ }
+
+ .dev-img {
+ width: 40px;
+ height: 40px;
+ }
+ }
+
+ .id-col {
+ font-weight: 700;
+ }
+
+ .date-col {
+ color: $tc-gray-50;
+ font-weight: 400;
+ line-height: $submission-space-20;
+
+ @include xs-to-sm {
+ padding: 0 10px;
+ }
+ }
+
+ .legacy-id {
+ color: $tc-gray-50;
+ }
+
+ .status-col {
+ text-align: center;
+
+ button {
+ background: none;
+ border: none;
+ padding: 0;
+
+ .pending {
+ text-transform: initial;
+ font-size: 15px;
+ color: $tc-gray-40;
+ line-height: $submission-space-20;
+ }
+ }
+ }
+
+ .action-col {
+ text-align: center;
+ min-width: 120px;
+
+ @include xs-to-sm {
+ position: absolute;
+ right: 0;
+ top: 10px;
+ padding: 10px 0;
+ min-width: 100px;
+
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+ }
+
+ path {
+ fill: $tc-gray-80;
+ }
+
+ .delete-icon {
+ margin: 0 0 0 24px;
+
+ @include xs-to-sm {
+ margin-left: 15px;
+ }
+ }
+
+ button {
+ background: none;
+ border: 0;
+ font-size: 0;
+ padding: 0;
+ line-height: 0;
+ display: inline-block;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ }
+ }
+
+ .expand-icon {
+ transition: all 0ms;
+ margin-left: 24px;
+
+ @include xs-to-sm {
+ margin-left: 15px;
+ }
+
+ &.expanded {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ .status-col button:focus {
+ outline: none;
+ }
+}
diff --git a/src/containers/SubmissionManagement/MySubmissions/SubmissionRowExpanded/index.jsx b/src/containers/SubmissionManagement/MySubmissions/SubmissionRowExpanded/index.jsx
new file mode 100644
index 0000000..bbd3d8c
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/SubmissionRowExpanded/index.jsx
@@ -0,0 +1,34 @@
+import React from "react";
+import PT from "prop-types";
+import ScreeningDetails from "../ScreeningDetails";
+
+import "./styles.scss";
+
+const SubmissionRowExpanded = ({
+ submission,
+ showScreeningDetails,
+ helpPageUrl,
+}) => {
+ return (
+
+ {showScreeningDetails && (
+
+
+ |
+ )}
+
+ );
+};
+
+SubmissionRowExpanded.defaultProps = {};
+
+SubmissionRowExpanded.propTypes = {
+ submission: PT.shape({}),
+ showScreeningDetails: PT.bool,
+ helpPageUrl: PT.string,
+};
+
+export default SubmissionRowExpanded;
diff --git a/src/containers/SubmissionManagement/MySubmissions/SubmissionRowExpanded/styles.scss b/src/containers/SubmissionManagement/MySubmissions/SubmissionRowExpanded/styles.scss
new file mode 100644
index 0000000..e65de41
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/SubmissionRowExpanded/styles.scss
@@ -0,0 +1,27 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$submission-space-10: $base-unit * 2;
+$submission-space-20: $base-unit * 4;
+$submission-space-25: $base-unit * 5;
+$submission-space-50: $base-unit * 10;
+
+.submission-row {
+ width: 100%;
+ font-size: 15px;
+ color: $tc-black;
+ font-weight: 400;
+
+ td {
+ vertical-align: middle;
+ padding: $submission-space-20;
+ background: $tc-white;
+ border-top: 1px solid $tc-gray-10;
+ line-height: 12px;
+
+ &.dev-details {
+ background: $tc-gray-neutral-light;
+ padding-right: 60px;
+ }
+ }
+}
diff --git a/src/containers/SubmissionManagement/MySubmissions/SubmissionTable/index.jsx b/src/containers/SubmissionManagement/MySubmissions/SubmissionTable/index.jsx
new file mode 100644
index 0000000..34c2dd8
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/SubmissionTable/index.jsx
@@ -0,0 +1,94 @@
+import React from "react";
+import PT from "prop-types";
+import moment from "moment";
+import { COMPETITION_TRACKS } from "../../../../constants";
+import SubmissionRow from "../SubmissionRow";
+import SubmissionRowExpanded from "../SubmissionRowExpanded";
+
+import "./styles.scss";
+
+const SubmissionTable = ({
+ submissions,
+ showDetails,
+ track,
+ onDelete,
+ helpPageUrl,
+ onDownload,
+ onShowDetails,
+ status,
+ submissionPhaseStartDate,
+}) => {
+ const headerRow = (
+
+ ID |
+ Type |
+ Submission Date |
+ {track === COMPETITION_TRACKS.DES && (
+ Screening Status |
+ )}
+ Actions |
+
+ );
+
+ const emptyRow = (
+
+
+ You have no submission uploaded so far.
+ |
+
+ );
+
+ return (
+
+
+ {headerRow}
+
+ {!submissions || submissions.length === 0
+ ? emptyRow
+ : submissions.map((submission) => [
+ ,
+ ,
+ ])}
+
+
+
+ );
+};
+
+SubmissionTable.defaultProps = {};
+
+SubmissionTable.propTypes = {
+ submissions: PT.arrayOf(PT.shape()),
+ showDetails: PT.shape({}),
+ track: PT.string,
+ onDelete: PT.func,
+ helpPageUrl: PT.string,
+ onDownload: PT.func,
+ onShowDetails: PT.func,
+ status: PT.string,
+ submissionPhaseStartDate: PT.string,
+};
+
+export default SubmissionTable;
diff --git a/src/containers/SubmissionManagement/MySubmissions/SubmissionTable/styles.scss b/src/containers/SubmissionManagement/MySubmissions/SubmissionTable/styles.scss
new file mode 100644
index 0000000..7dd8e26
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/SubmissionTable/styles.scss
@@ -0,0 +1,151 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$status-space-10: $base-unit * 2;
+$status-space-20: $base-unit * 4;
+$status-space-25: $base-unit * 5;
+$submission-space-10: $base-unit * 2;
+$submission-space-20: $base-unit * 4;
+$submission-space-25: $base-unit * 5;
+$submission-space-50: $base-unit * 10;
+
+.submissions-table {
+ border: 1px solid $tc-gray-10;
+ overflow: hidden;
+ border-radius: 4px 4px 0 0;
+
+ @include xs-to-sm {
+ border: none;
+ border-radius: 0;
+ border-top: 1px solid $tc-gray-10;
+ border-bottom: 1px solid $tc-gray-10;
+ }
+
+ table {
+ width: 100%;
+
+ thead {
+ tr {
+ background: $tc-gray-neutral-light;
+ }
+ }
+
+ th {
+ font-size: 13px;
+ color: $tc-gray-50;
+ font-weight: 500;
+ line-height: $status-space-20;
+ text-align: left;
+ padding: $status-space-25 $status-space-20;
+
+ &.status,
+ &.actions {
+ text-align: center;
+ }
+
+ @include xs-to-sm {
+ display: none;
+ }
+ }
+
+ .no-submission {
+ line-height: $submission-space-20;
+ padding: $submission-space-50 $submission-space-20;
+ text-align: center;
+ }
+ }
+
+ .status-col {
+ text-align: center;
+ }
+
+ .action-col {
+ text-align: center;
+ }
+}
+
+.submission-row {
+ width: 100%;
+ font-size: 15px;
+ color: $tc-black;
+ font-weight: 400;
+
+ td {
+ vertical-align: middle;
+ padding: $submission-space-20;
+ background: $tc-white;
+ border-top: 1px solid $tc-gray-10;
+ line-height: 12px;
+
+ &.no-submission {
+ line-height: $submission-space-20;
+ padding: $submission-space-50 $submission-space-20;
+ text-align: center;
+ }
+
+ &.dev-details {
+ background: $tc-gray-neutral-light;
+ padding-right: 60px;
+ }
+ }
+
+ .id-col {
+ font-weight: 700;
+ }
+
+ .date-col {
+ color: $tc-gray-50;
+ font-weight: 400;
+ line-height: $submission-space-20;
+ }
+
+ .status-col {
+ text-align: center;
+
+ button {
+ background: none;
+ border: none;
+ padding: 0;
+
+ .pending {
+ text-transform: initial;
+ font-size: 15px;
+ color: $tc-gray-40;
+ line-height: $submission-space-20;
+ }
+ }
+ }
+
+ .action-col {
+ text-align: center;
+
+ .delete-icon {
+ margin: 0 $submission-space-25;
+ }
+
+ button {
+ background: none;
+ border: 0;
+ font-size: 0;
+ padding: 0;
+ line-height: 0;
+ display: inline-block;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ .expand-icon {
+ transition: all 1.5s;
+
+ &.expanded {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ .status-col button:focus {
+ outline: none;
+ }
+}
diff --git a/src/containers/SubmissionManagement/MySubmissions/index.jsx b/src/containers/SubmissionManagement/MySubmissions/index.jsx
new file mode 100644
index 0000000..7b21c7f
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/index.jsx
@@ -0,0 +1,159 @@
+import React from "react";
+import PT from "prop-types";
+import moment from "moment";
+import { PrimaryButton } from "components/Buttons";
+import SubmissionTable from "./SubmissionTable";
+import LoadingIndicator from "components/LoadingIndicator";
+import * as util from "../../../utils/challenge";
+import config from "../../../../config";
+
+import styles from "./styles.scss";
+
+const MySubmissions = ({
+ challengeId,
+ challengeTrack,
+ challengeName,
+ challengeStatus,
+ challengePhases,
+ submissions,
+ loadingSubmissions,
+ showDetails,
+ submissionPhaseStartDate,
+ onShowDetails,
+ onDelete,
+ onDownload,
+ helpPageUrl,
+ isDeletingSubmission,
+}) => {
+ const challengeType = challengeTrack.toLowerCase();
+ const isDesign = challengeType === "design";
+ const isDevelop = challengeType === "development";
+ const currentPhase = util.currentPhase(challengePhases);
+ const submissionPhase = util.submissionPhase(challengePhases);
+ const submissionEndDate =
+ submissionPhase && util.phaseEndDate(submissionPhase);
+
+ const now = moment();
+ const end = moment(currentPhase && currentPhase.scheduledEndDate);
+ const diff = end.isAfter(now) ? end.diff(now) : 0;
+ const timeLeft = moment.duration(diff);
+
+ const [days, hours, minutes] = [
+ timeLeft.get("days"),
+ timeLeft.get("hours"),
+ timeLeft.get("minutes"),
+ ];
+
+ const isLoadingOrDeleting = loadingSubmissions || isDeletingSubmission;
+
+ return (
+
+ {/* Header */}
+
+
+
+ {currentPhase &&
{currentPhase.name}
}
+ {challengeStatus !== "Completed" ? (
+
+
+ {days > 0 && `${days}D`} {hours}H {minutes}M
+
+
left
+
+ ) : (
+
The challenge has ended
+ )}
+
+
+ {/* Table */}
+
+
+
Manage your submissions
+ {isDesign && currentPhase && (
+
+ {currentPhase.name} Ends:{" "}
+ {end.format("dddd MM/DD/YY hh:mm A")}
+
+ )}
+
+ {isDesign && (
+
+ We always recommend to download your submission to check you
+ uploaded the correct zip files and also verify the photos and fonts
+ declarations. If you don’t want to see a submission, simply delete.
+ If you have a new submission, use the Upload Submission button to
+ add one at the top of the list.
+
+ )}
+ {isDevelop && (
+
+ We always recommend to download your submission to check you
+ uploaded the correct zip file. If you don’t want to see the
+ submission, simply delete. If you have a new submission, use the
+ Upload Submission button to overwrite the current one.
+
+ )}
+ {isLoadingOrDeleting &&
}
+ {!isLoadingOrDeleting && (
+
+ onDownload(challengeType, submissionId)
+ }
+ onShowDetails={onShowDetails}
+ />
+ )}
+
+ {/* Footer */}
+ {now.isBefore(submissionEndDate) && (
+
+
+ {!isDevelop || !submissions || submissions.length === 0
+ ? "Add Submission"
+ : "Update Submission"}
+
+
+ )}
+
+ );
+};
+
+MySubmissions.defaultProps = {};
+
+MySubmissions.propTypes = {
+ challengeId: PT.string,
+ challengeTrack: PT.string,
+ challengeName: PT.string,
+ challengeStatus: PT.string,
+ challengePhases: PT.arrayOf(PT.shape()),
+ submissions: PT.arrayOf(PT.shape()),
+ loadingSubmissions: PT.bool,
+ showDetails: PT.shape({}),
+ submissionPhaseStartDate: PT.string,
+ onShowDetails: PT.func,
+ onDelete: PT.func,
+ onDownload: PT.func,
+ helpPageUrl: PT.string,
+ isDeletingSubmission: PT.bool,
+};
+
+export default MySubmissions;
diff --git a/src/containers/SubmissionManagement/MySubmissions/styles.scss b/src/containers/SubmissionManagement/MySubmissions/styles.scss
new file mode 100644
index 0000000..7fd5c7c
--- /dev/null
+++ b/src/containers/SubmissionManagement/MySubmissions/styles.scss
@@ -0,0 +1,184 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$submang-space-140: $base-unit * 28;
+$submang-space-50: $base-unit * 10;
+$submang-space-35: $base-unit * 7;
+$submang-space-30: $base-unit * 6;
+$submang-space-25: $base-unit * 5;
+$submang-space-20: $base-unit * 4;
+$gray-color: $tc-gray-40;
+$light-gray-color: $tc-gray-neutral-light;
+
+.btn-wrap {
+ text-align: center;
+ color: white;
+}
+
+.add-sub-btn {
+ margin: 10px auto;
+}
+
+.submission-management {
+ padding-bottom: 40px;
+}
+
+.submission-management-content {
+ padding: $submang-space-20 $submang-space-140;
+
+ @include md {
+ padding: $submang-space-20 $submang-space-35;
+ }
+
+ @include xs-to-sm {
+ padding: 0;
+ }
+
+ .content-head {
+ display: flex;
+ justify-content: space-between;
+ font-weight: 400;
+ font-size: 15px;
+ color: $tc-gray-50;
+ line-height: $submang-space-25;
+ margin-top: $submang-space-50;
+ margin-bottom: $base-unit;
+
+ @include xs-to-sm {
+ flex-direction: column;
+ margin-top: $submang-space-20;
+ padding: 0 15px;
+ }
+
+ .title {
+ font-size: 20px;
+ color: $tc-gray-80;
+ line-height: $submang-space-30;
+ }
+
+ .round-ends {
+ font-size: 15px;
+ color: $tc-black;
+ line-height: $submang-space-30;
+
+ @include xs-to-sm {
+ margin-top: 10px;
+ margin-bottom: 15px;
+ }
+
+ span.ends-label {
+ color: $tc-gray-50;
+
+ @include xs-to-sm {
+ display: block;
+ margin-bottom: -10px;
+ }
+ }
+ }
+ }
+
+ .recommend-info {
+ font-weight: 400;
+ color: $tc-gray-50;
+ line-height: $submang-space-25;
+ margin-bottom: $submang-space-30;
+
+ @include xs-to-sm {
+ padding: 0 15px;
+ }
+ }
+}
+
+.submission-management-header {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ font-weight: 400;
+ padding: $submang-space-25 $submang-space-50;
+ background: $light-gray-color;
+ border-bottom: 1px solid $tc-gray-neutral-dark;
+
+ @include md {
+ padding: $submang-space-25 $submang-space-35;
+ }
+
+ @include xs-to-sm {
+ flex-direction: column;
+ padding: $submang-space-20 15px;
+ }
+
+ .left-col {
+ padding-right: 200px;
+
+ @include xs-to-sm {
+ padding-right: 0;
+ }
+
+ .name {
+ font-size: 28px;
+ color: $tc-black;
+ line-height: $submang-space-35;
+ }
+
+ .back-btn {
+ background: none;
+ color: $gray-color;
+ font-size: 20px;
+ line-height: $submang-space-25;
+ margin-top: 5px;
+ display: inline-block;
+ padding: 0;
+ border: none;
+ font-weight: 400;
+ text-transform: initial;
+
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+
+ .right-col {
+ @include roboto-regular;
+
+ min-width: 100px;
+
+ @include xs-to-sm {
+ margin-top: 10px;
+
+ p,
+ div {
+ display: inline-block;
+ }
+ }
+
+ .round {
+ font-size: 15px;
+ color: $tc-gray-80;
+ line-height: $submang-space-25;
+ }
+
+ .time-left {
+ font-size: 20px;
+ color: $tc-gray-80;
+ line-height: $submang-space-30;
+
+ @include xs-to-sm {
+ font-size: 15px;
+ font-weight: bold;
+ line-height: $submang-space-25;
+ margin-left: 5px;
+ }
+ }
+
+ .left-label {
+ font-size: 13px;
+ color: $tc-gray-50;
+ line-height: $submang-space-20;
+
+ @include xs-to-sm {
+ margin-left: 5px;
+ }
+ }
+ }
+}
diff --git a/src/containers/SubmissionManagement/index.jsx b/src/containers/SubmissionManagement/index.jsx
new file mode 100644
index 0000000..a582958
--- /dev/null
+++ b/src/containers/SubmissionManagement/index.jsx
@@ -0,0 +1,288 @@
+import React, { useEffect, useLayoutEffect, useRef } from "react";
+import PT from "prop-types";
+import _ from "lodash";
+import { navigate } from "@reach/router";
+import { connect } from "react-redux";
+import Button from "components/Buttons";
+import Modal from "components/Modal";
+import LoadingIndicator from "components/LoadingIndicator";
+import MySubmissions from "./MySubmissions";
+import AccessDenied from "components/AccessDenied";
+import { ACCESS_DENIED_REASON, CHALLENGES_URL } from "../../constants";
+import actions from "../../actions";
+import { isLegacyId, isUuid } from "../../utils/challenge";
+
+import "./styles.scss";
+
+const SubmissionManagement = ({
+ id,
+ challengeId,
+ challengeLegacyId,
+ challengeTrack,
+ challengeName,
+ challengeStatus,
+ challengePhases,
+
+ isDeletingSubmission,
+ isLoadingChallenge,
+ isChallengeLoaded,
+ isLoadingMySubmissions,
+ isRegistered,
+
+ mySubmissions,
+ submissionPhaseStartDate,
+ showDetails,
+ showModal,
+ toBeDeletedId,
+
+ onShowDetails,
+ onSubmissionDelete,
+ onCancelSubmissionDelete,
+ onSubmissionDeleteConfirmed,
+ onDownloadSubmission,
+ getChallenge,
+ getMySubmissions,
+}) => {
+ const propsRef = useRef();
+ propsRef.current = {
+ id,
+ challengeId,
+ challengeLegacyId,
+ getChallenge,
+ getMySubmissions,
+ };
+
+ useLayoutEffect(() => {
+ const didChallengeLoaded =
+ propsRef.current.challengeId &&
+ `${propsRef.current.challengeId}` === `${propsRef.current.id}`;
+ if (didChallengeLoaded) {
+ propsRef.current.getMySubmissions(propsRef.current.id);
+ return;
+ }
+
+ if (isLegacyId(propsRef.current.id)) {
+ propsRef.current.getChallenge(propsRef.current.id);
+ } else if (isUuid(propsRef.current.id)) {
+ propsRef.current.getChallenge(propsRef.current.id);
+ propsRef.current.getMySubmissions(propsRef.current.id);
+ } else {
+ navigate(CHALLENGES_URL);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (isChallengeLoaded && isLegacyId(propsRef.current.id)) {
+ navigate(
+ `${CHALLENGES_URL}/${propsRef.current.challengeId}/my-submissions`
+ );
+ propsRef.current.getMySubmissions(propsRef.current.challengeId);
+ }
+ }, [isChallengeLoaded]);
+
+ if (isLoadingChallenge) {
+ return ;
+ }
+
+ if (!isChallengeLoaded) {
+ return null;
+ }
+
+ if (!isRegistered) {
+ return (
+
+ );
+ }
+
+ const isEmpty = _.isEmpty(challengeName);
+ const modal = (
+
+
+
+ Are you sure you want to delete submission{" "}
+ {toBeDeletedId}?
+
+
+ This will permanently remove all files from our servers and can’t be
+ undone. You’ll have to upload all the files again in order to restore
+ it.
+
+
+
+
+
+
+
+
+
+
+ );
+
+ return (
+
+
+ {!isEmpty && (
+
+ )}
+ {showModal && modal}
+
+
+ );
+};
+
+SubmissionManagement.defaultProps = {};
+
+SubmissionManagement.propTypes = {
+ id: PT.string,
+ challengeId: PT.string,
+ challengeLegacyId: PT.number,
+ challengeTrack: PT.string,
+ challengeName: PT.string,
+ challengeStatus: PT.string,
+ challengePhases: PT.arrayOf(PT.shape()),
+
+ isDeletingSubmission: PT.bool,
+ isLoadingChallenge: PT.bool,
+ isChallengeLoaded: PT.bool,
+ isLoadingMySubmissions: PT.bool,
+ isRegistered: PT.bool,
+
+ mySubmissions: PT.arrayOf(PT.shape()),
+ submissionPhaseStartDate: PT.string,
+ showDetails: PT.shape({}),
+ showModal: PT.bool,
+ toBeDeletedId: PT.string,
+
+ onShowDetails: PT.func,
+ onSubmissionDelete: PT.func,
+ onCancelSubmissionDelete: PT.func,
+ onSubmissionDeleteConfirmed: PT.func,
+ onDownloadSubmission: PT.func,
+ getChallenge: PT.func,
+ getMySubmissions: PT.func,
+};
+
+const mapStateToProps = (state, ownProps) => {
+ const challenge = (state.challenge && state.challenge.challenge) || {};
+ const allPhases = challenge.phases || [];
+ const submissionPhase =
+ allPhases.find(
+ (phase) =>
+ ["Submission", "Checkpoint Submission"].includes(phase.name) &&
+ phase.isOpen
+ ) || {};
+
+ return {
+ id: ownProps.challengeId,
+ challengeId: challenge.id,
+ challengeLegacyId: challenge.legacyId,
+ challengeTrack: challenge.track,
+ challengeName: challenge.name,
+ challengeStatus: challenge.status,
+ challengePhases: challenge.phases,
+
+ isDeletingSubmission: state.submissionManagement.deletingSubmission,
+ isLoadingChallenge: state.challenge.isLoadingChallenge,
+ isChallengeLoaded: state.challenge.isChallengeLoaded,
+ isLoadingMySubmissions: state.submissionManagement.isLoadingMySubmissions,
+ isRegistered: challenge.isRegistered,
+
+ mySubmissions: state.submissionManagement.mySubmissions,
+ submissionPhaseStartDate:
+ submissionPhase.actualStartDate ||
+ submissionPhase.scheduledStartDate ||
+ "",
+ showDetails: state.submissionManagement.showDetails,
+ showModal: state.submissionManagement.showModal,
+ toBeDeletedId: state.submissionManagement.toBeDeletedId,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onShowDetails: (submissionId) => {
+ dispatch(
+ actions.submissionManagement.mySubmissions.showDetails(submissionId)
+ );
+ },
+ onSubmissionDelete: (submissionId) => {
+ dispatch(
+ actions.submissionManagement.mySubmissions.confirmDelete(submissionId)
+ );
+ },
+ onCancelSubmissionDelete: () => {
+ dispatch(actions.submissionManagement.mySubmissions.cancelDelete());
+ },
+ onSubmissionDeleteConfirmed: (submissionId) => {
+ dispatch(
+ actions.submissionManagement.mySubmissions.deleteSubmissionInit()
+ );
+ dispatch(
+ actions.submissionManagement.mySubmissions.deleteSubmissionDone(
+ submissionId
+ )
+ );
+ },
+ onDownloadSubmission: (challengeType, submissionId) => {
+ dispatch(
+ actions.submissionManagement.mySubmissions.downloadSubmissionDone(
+ challengeType,
+ submissionId
+ )
+ );
+ },
+ getChallenge: (challengeId) => {
+ dispatch(actions.challenge.getChallengeInit());
+ dispatch(actions.challenge.getChallengeDone(challengeId));
+ },
+ getMySubmissions: (challengeId) => {
+ dispatch(
+ actions.submissionManagement.mySubmissions.getMySubmissionsInit()
+ );
+ dispatch(
+ actions.submissionManagement.mySubmissions.getMySubmissionsDone(
+ challengeId
+ )
+ );
+ },
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(SubmissionManagement);
diff --git a/src/containers/SubmissionManagement/styles.scss b/src/containers/SubmissionManagement/styles.scss
new file mode 100644
index 0000000..2a0b498
--- /dev/null
+++ b/src/containers/SubmissionManagement/styles.scss
@@ -0,0 +1,76 @@
+@import '~styles/variables';
+@import '~styles/mixins';
+
+$sm-space-10: $base-unit * 2;
+$sm-space-15: $base-unit * 3;
+$sm-space-25: $base-unit * 5;
+$sm-space-40: $base-unit * 8;
+
+.outer-container {
+ background: $tc-gray-10;
+ padding: 24px;
+ max-width: 2056px;
+ margin: 0 auto;
+
+ @include xs-to-sm {
+ padding: 15px;
+ }
+}
+
+.deletingIndicator {
+ margin: -17px 0;
+
+ &:global.hidden {
+ display: none;
+ }
+}
+
+.submission-management-container {
+ @include roboto-regular;
+
+ background: #fff;
+ margin: auto;
+ min-height: calc(100vh - var(--navbarHeight, 60px) - 24px * 2);
+
+}
+
+.modal-content {
+ @include roboto-regular;
+
+ text-align: center;
+ padding-top: $sm-space-15;
+ padding-bottom: $sm-space-15;
+
+ .are-you-sure {
+ font-weight: 400;
+ font-size: 15px;
+ color: $tc-gray-80;
+ line-height: $sm-space-25;
+ margin-bottom: $sm-space-10;
+ padding: 0 $sm-space-15;
+
+ .id {
+ color: #000;
+ font-weight: 500;
+ }
+ }
+
+ .remove-warn {
+ font-weight: 400;
+ font-size: 13px;
+ color: $tc-gray-60;
+ line-height: $sm-space-25;
+ padding: 0 $sm-space-15;
+ margin-bottom: $sm-space-40;
+ }
+
+ .action-btns {
+ button {
+ margin: 0 5px;
+ }
+
+ &:global.hidden {
+ display: none;
+ }
+ }
+}
diff --git a/src/containers/challenge-detail/index.jsx b/src/containers/challenge-detail/index.jsx
index 7773c9d..b2a17b1 100644
--- a/src/containers/challenge-detail/index.jsx
+++ b/src/containers/challenge-detail/index.jsx
@@ -227,7 +227,7 @@ class ChallengeDetailPageContainer extends React.Component {
challenge.isLegacyChallenge &&
!history.location.pathname.includes(challenge.id)
) {
- history.location.pathname = `/challenges/${challenge.id}`; // eslint-disable-line no-param-reassign
+ history.location.pathname = `/earn/find/challenges/${challenge.id}`; // eslint-disable-line no-param-reassign
history.push(history.location.pathname, history.state);
}
diff --git a/src/reducers/auth.js b/src/reducers/auth.js
index 6e89996..5b36096 100644
--- a/src/reducers/auth.js
+++ b/src/reducers/auth.js
@@ -30,6 +30,10 @@ function onProfileLoaded(state, action) {
};
}
+function onSetAuthDone(state, { payload }) {
+ return { ...state, user: payload, isAuthInitialized: true };
+}
+
/**
* Creates a new Auth reducer with the specified initial state.
* @param {Object} initialState Optional. Initial state.
@@ -58,6 +62,7 @@ function create(initialState) {
}),
},
}),
+ [actions.auth.setAuthDone]: onSetAuthDone,
},
_.defaults(initialState, {
authenticating: true,
@@ -65,6 +70,7 @@ function create(initialState) {
tokenV2: "",
tokenV3: "",
user: null,
+ isAuthInitialized: false,
})
);
}
diff --git a/src/reducers/challenge.js b/src/reducers/challenge.js
index c2cb8c9..346db3e 100644
--- a/src/reducers/challenge.js
+++ b/src/reducers/challenge.js
@@ -446,6 +446,32 @@ function onGetSubmissionInformationDone(state, action) {
};
}
+function onGetChallengeInit(state) {
+ return {
+ ...state,
+ isLoadingChallenge: true,
+ isChallengeLoaded: false,
+ };
+}
+
+function onGetChallengeDone(state, { error, payload }) {
+ if (error) {
+ logger.error("Failed to get challenge details!", payload);
+ fireErrorMessage(
+ "ERROR: Failed to load the challenge",
+ "Please, try again a bit later"
+ );
+ return { ...state, isLoadingChallenge: false, isChallengeLoaded: false };
+ }
+
+ return {
+ ...state,
+ challenge: { ...payload },
+ isLoadingChallenge: false,
+ isChallengeLoaded: true,
+ };
+}
+
/**
* Creates a new Challenge reducer with the specified initial state.
* @param {Object} initialState Optional. Initial state.
@@ -492,6 +518,8 @@ function create(initialState) {
[a.getActiveChallengesCountDone]: onGetActiveChallengesCountDone,
[a.getSubmissionInformationInit]: onGetSubmissionInformationInit,
[a.getSubmissionInformationDone]: onGetSubmissionInformationDone,
+ [a.getChallengeInit]: onGetChallengeInit,
+ [a.getChallengeDone]: onGetChallengeDone,
},
_.defaults(initialState, {
details: null,
@@ -510,6 +538,7 @@ function create(initialState) {
updatingChallengeUuid: "",
mmSubmissions: [],
submissionInformation: null,
+ isLoadingChallenge: false,
})
);
}
diff --git a/src/reducers/index.js b/src/reducers/index.js
index d096045..e3bb5a5 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -9,6 +9,8 @@ import page from "./page";
import terms from "./terms";
import auth from "./auth";
import errors from "./errors";
+import submission from "./submission";
+import submissionManagement from "./submissionManagement";
export default combineReducers({
challenges,
@@ -20,5 +22,7 @@ export default combineReducers({
page,
terms,
auth,
+ submission,
+ submissionManagement,
errors,
});
diff --git a/src/reducers/lookup.js b/src/reducers/lookup.js
index e384553..56d9523 100644
--- a/src/reducers/lookup.js
+++ b/src/reducers/lookup.js
@@ -15,8 +15,18 @@ function onGetTagsDone(state, { payload }) {
return { ...state, tags: payload };
}
-function onGetCommunityListDone(state, { payload }) {
- return { ...state, subCommunities: payload };
+function onGetCommunityListInit(state) {
+ return {
+ ...state,
+ isSubCommunitiesLoaded: false,
+ };
+}
+
+function onGetCommunityListDone(state, { error, payload }) {
+ if (error) {
+ return { ...state, subCommunities: [] };
+ }
+ return { ...state, subCommunities: payload, isSubCommunitiesLoaded: true };
}
/**
@@ -259,6 +269,7 @@ function create(initialState = {}) {
return handleActions(
{
[a.getTagsDone]: onGetTagsDone,
+ [a.getCommunityListInit]: onGetCommunityListInit,
[a.getCommunityListDone]: onGetCommunityListDone,
[a.getTypesInit]: (state) => state,
[a.getTypesDone]: onGetTypesDone,
@@ -295,6 +306,7 @@ function create(initialState = {}) {
tags: [],
subCommunities: [],
isLoggedIn: null,
+ isSubCommunitiesLoaded: false,
})
);
}
diff --git a/src/reducers/submission.js b/src/reducers/submission.js
new file mode 100644
index 0000000..9496a95
--- /dev/null
+++ b/src/reducers/submission.js
@@ -0,0 +1,170 @@
+import { handleActions } from "redux-actions";
+import { logger, fireErrorMessage } from "../utils/logger";
+
+const defaultState = {
+ isSubmitting: false,
+ submitDone: false,
+ submitErrorMsg: "",
+ agreed: false,
+ uploadProgress: 0,
+ filePickers: [],
+ submissionFilestackData: {
+ challengeId: "",
+ fileUrl: "",
+ filename: "",
+ mimetype: "",
+ size: 0,
+ key: "",
+ container: "",
+ },
+};
+
+function onSubmitDone(state, { error, payload }) {
+ if (error) {
+ logger.error("Failed to submit for the challenge");
+ fireErrorMessage(
+ "ERROR: Failed to submit!",
+ "Please, try to submit from https://software.topcoder.com or email you submission to support@topcoder.com"
+ );
+
+ return {
+ ...state,
+ submitErrorMsg: "Failed to submit",
+ isSubmitting: false,
+ submitDone: false,
+ };
+ }
+
+ if (payload.message) {
+ /* payload message is present when upload of file fails due to any reason so
+ * handle this special case for error */
+ logger.error(`Failed to submit for the challenge - ${payload.message}`);
+ return {
+ ...state,
+ submitErrorMsg: payload.message || "Failed to submit",
+ isSubmitting: false,
+ submitDone: false,
+ };
+ }
+
+ /* TODO: I am not sure, whether this code is just wrong, or does it handle
+ * only specific errors, returned from API for design submissions? I am
+ * adding a more generic failure handling code just above. */
+ if (payload.result && !payload.result.success) {
+ return {
+ ...state,
+ submitErrorMsg: payload.result.content.message || "Failed to submit",
+ isSubmitting: false,
+ submitDone: false,
+ };
+ }
+
+ return {
+ ...state,
+ ...payload,
+ isSubmitting: false,
+ submitDone: true,
+ };
+}
+
+function onSubmitInit(state) {
+ return {
+ ...state,
+ isSubmitting: true,
+ submitDone: false,
+ submitErrorMsg: "",
+ uploadProgress: 0,
+ };
+}
+
+function onSubmitReset(state) {
+ return {
+ ...state,
+ isSubmitting: false,
+ submitDone: false,
+ submitErrorMsg: "",
+ uploadProgress: 0,
+ };
+}
+
+function onUploadProgress(state, { payload }) {
+ return {
+ ...state,
+ uploadProgress: payload,
+ };
+}
+
+/**
+ * Returns a new state with the filePicker updated according to map, or added if not existing
+ * @param {Object} state Current state
+ * @param {String} id ID of the
+ * @param {Object} map Key value pairs for the new FilePicker state
+ * @return New state
+ */
+function fpSet(state, id, map) {
+ let found = false;
+
+ const newFilePickers = state.filePickers.map((fp) => {
+ if (fp.id === id) {
+ found = true;
+ return {
+ ...fp,
+ ...map,
+ };
+ }
+ return fp;
+ });
+
+ if (found) {
+ return { ...state, filePickers: newFilePickers };
+ }
+
+ return {
+ ...state,
+ filePickers: [...newFilePickers, { id, ...map }],
+ };
+}
+
+function onSetAgreed(state, { payload }) {
+ return { ...state, agreed: payload };
+}
+
+function onSetFilePickerError(state, { payload }) {
+ return fpSet(state, payload.id, { error: payload.error });
+}
+
+function onSetFilePickerFileName(state, { payload }) {
+ return fpSet(state, payload.id, { fileName: payload.fileName });
+}
+
+function onSetFilePickerUploadProgress(state, { payload }) {
+ return fpSet(state, payload.id, { uploadProgress: payload.progress });
+}
+
+function onSetFilePickerDragged(state, { payload }) {
+ return fpSet(state, payload.id, { dragged: payload.dragged });
+}
+
+function onSetSubmissionFilestackData(state, { payload }) {
+ return { ...state, submissionFilestackData: payload };
+}
+
+const reducer = handleActions(
+ {
+ SUBMIT: {
+ SUBMIT_DONE: onSubmitDone,
+ SUBMIT_INIT: onSubmitInit,
+ SUBMIT_RESET: onSubmitReset,
+ UPLOAD_PROGRESS: onUploadProgress,
+ SET_AGREED: onSetAgreed,
+ SET_FILE_PICKER_ERROR: onSetFilePickerError,
+ SET_FILE_PICKER_FILE_NAME: onSetFilePickerFileName,
+ SET_FILE_PICKER_UPLOAD_PROGRESS: onSetFilePickerUploadProgress,
+ SET_FILE_PICKER_DRAGGED: onSetFilePickerDragged,
+ SET_SUBMISSION_FILESTACK_DATA: onSetSubmissionFilestackData,
+ },
+ },
+ defaultState
+);
+
+export default reducer;
diff --git a/src/reducers/submissionManagement.js b/src/reducers/submissionManagement.js
new file mode 100644
index 0000000..29393dd
--- /dev/null
+++ b/src/reducers/submissionManagement.js
@@ -0,0 +1,104 @@
+import { handleActions } from "redux-actions";
+import _ from "lodash";
+import { logger } from "../utils/logger";
+
+const defaultState = {
+ showDetails: {},
+ showModal: false,
+ toBeDeletedId: "",
+ deletingSubmission: false,
+ mySubmissions: [],
+ isLoadingMySubmissions: false,
+};
+
+function onShowDetails(state, { payload: id }) {
+ const showDetails = _.clone(state.showDetails);
+ if (showDetails[id]) delete showDetails[id];
+ else showDetails[id] = true;
+ return { ...state, showDetails };
+}
+
+function onConfirmDelete(state, { payload }) {
+ return {
+ ...state,
+ showModal: true,
+ toBeDeletedId: payload,
+ };
+}
+
+function onCancelDelete(state) {
+ return {
+ ...state,
+ showModal: false,
+ toBeDeletedId: "",
+ };
+}
+
+function onDeleteSubmissionInit(state) {
+ return {
+ ...state,
+ showModal: false,
+ deletingSubmission: true,
+ };
+}
+
+function onDeleteSubmissionDone(state, { error, payload }) {
+ if (error) {
+ return {
+ ...state,
+ deletingSubmission: false,
+ };
+ }
+
+ const deletedSubmissionId = payload;
+ return {
+ ...state,
+ deletingSubmission: false,
+ showModal: false,
+ toBeDeletedId: "",
+ mySubmissions: state.mySubmissions.filter(
+ (submission) => submission.id !== deletedSubmissionId
+ ),
+ };
+}
+
+function onGetMySubmissionsInit(state) {
+ return {
+ ...state,
+ isLoadingMySubmissions: true,
+ };
+}
+
+function onGetMySubmissionsDone(state, { error, payload }) {
+ if (error) {
+ logger.error("Failed to get user's submissions for the challenge", payload);
+ return {
+ ...state,
+ mySubmissions: [],
+ isLoadingMySubmissions: false,
+ };
+ }
+
+ return {
+ ...state,
+ mySubmissions: [...payload],
+ isLoadingMySubmissions: false,
+ };
+}
+
+const reducer = handleActions(
+ {
+ MY_SUBMISSIONS: {
+ SHOW_DETAILS: onShowDetails,
+ CONFIRM_DELETE: onConfirmDelete,
+ CANCEL_DELETE: onCancelDelete,
+ DELETE_SUBMISSION_INIT: onDeleteSubmissionInit,
+ DELETE_SUBMISSION_DONE: onDeleteSubmissionDone,
+ GET_MY_SUBMISSIONS_INIT: onGetMySubmissionsInit,
+ GET_MY_SUBMISSIONS_DONE: onGetMySubmissionsDone,
+ },
+ },
+ defaultState
+);
+
+export default reducer;
diff --git a/src/routers/submissions/index.jsx b/src/routers/submissions/index.jsx
new file mode 100644
index 0000000..e68f4c8
--- /dev/null
+++ b/src/routers/submissions/index.jsx
@@ -0,0 +1,38 @@
+/**
+ * Main App component
+ */
+import React from "react";
+import { Router } from "@reach/router";
+import _ from "lodash";
+import Submission from "../../containers/Submission";
+import SubmissionManagement from "../../containers/SubmissionManagement";
+import ErrorMessage from "components/ErrorMessage";
+import { useSelector } from "react-redux";
+import { clearErrorMesssage } from "../../utils/logger";
+import { CHALLENGES_URL } from "../../constants";
+
+import "react-date-range/dist/theme/default.css";
+import "react-date-range/dist/styles.css";
+import "rc-tooltip/assets/bootstrap.css";
+
+const App = () => {
+ const alert = useSelector((state) => state.errors.alerts[0]);
+ return (
+ <>
+
+
+
+
+
+ {alert && (
+ clearErrorMesssage()}
+ />
+ )}
+ >
+ );
+};
+
+export default App;
diff --git a/src/services/api.js b/src/services/api.js
index 76d05de..dc85cbd 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1,6 +1,6 @@
/* global process */
import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
-import { keys } from "lodash";
+import _, { keys } from "lodash";
import * as utils from "../utils";
async function doFetch(endpoint, options = {}, v3, baseUrl) {
@@ -25,6 +25,16 @@ async function doFetch(endpoint, options = {}, v3, baseUrl) {
});
}
+async function download(endpoint, baseUrl, cancellationSignal) {
+ const options = {
+ headers: { ["Content-Type"]: "application/json" },
+ signal: cancellationSignal,
+ };
+ const response = await doFetch(endpoint, options, undefined, baseUrl);
+
+ return response;
+}
+
async function get(endpoint, baseUrl, cancellationSignal) {
const options = {
headers: { ["Content-Type"]: "application/json" },
@@ -69,10 +79,44 @@ async function patch(endpoint, body) {
return response.json();
}
+/**
+ * Upload with progress
+ * @param {String} endpoint
+ * @param {Object} body and headers
+ * @param {Function} onProgress handler for update progress only works for client side for now
+ * @return {Promise}
+ */
+async function upload(endpoint, options, onProgress) {
+ const base = process.env.API.V5;
+ const { tokenV3 } = await getAuthUserTokens();
+ const headers = options.headers ? _.clone(options.headers) : {};
+ if (tokenV3) headers.Authorization = `Bearer ${tokenV3}`;
+
+ return new Promise((res, rej) => {
+ const xhr = new XMLHttpRequest(); //eslint-disable-line
+ xhr.open(options.method, `${base}${endpoint}`);
+ Object.keys(headers).forEach((key) => {
+ if (headers[key] != null) {
+ xhr.setRequestHeader(key, headers[key]);
+ }
+ });
+ xhr.onload = (e) => res(e.target.responseText);
+ xhr.onerror = rej;
+ if (xhr.upload && onProgress) {
+ xhr.upload.onprogress = (evt) => {
+ if (evt.lengthComputable) onProgress(evt.loaded / evt.total);
+ };
+ }
+ xhr.send(options.body);
+ });
+}
+
export default {
doFetch,
get,
post,
put,
patch,
+ upload,
+ download,
};
diff --git a/src/services/challenge.js b/src/services/challenge.js
new file mode 100644
index 0000000..38e4c6b
--- /dev/null
+++ b/src/services/challenge.js
@@ -0,0 +1,116 @@
+import api from "./api";
+import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
+import { decodeToken } from "tc-auth-lib";
+import qs from "qs";
+import _ from "lodash";
+import * as util from "../utils/api";
+
+/**
+ * @internal
+ */
+async function getChallengeDetails(endpoint, legacyInfo) {
+ let query = "";
+ if (legacyInfo) {
+ query = `legacyId=${legacyInfo.legacyId}`;
+ }
+
+ const url = `${endpoint}?${query}`;
+ const result = await api.get(url).then(util.tryThrowError);
+
+ return {
+ challenge: legacyInfo ? result[0] : result,
+ };
+}
+
+/**
+ * Gets challenge registrants from Topcoder API.
+ * @param {Number|String} challengeId
+ * @return {Promise} Resolves to the challenge registrants array.
+ * @internal
+ */
+async function getChallengeRegistrants(challengeId) {
+ /* If no token provided, resource will return Submitter role only */
+ const roleId = (await isLoggedIn())
+ ? await getRoleId("Submitter")
+ : undefined;
+ const params = {
+ challengeId,
+ roleId,
+ };
+
+ let registrants = await api
+ .get(`/resources?${qs.stringify(params)}`)
+ .then(util.tryThrowError);
+
+ /* API will return all roles to currentUser, so need to filter in FE */
+ if (roleId) {
+ registrants = _.filter(registrants, (r) => r.roleId === roleId);
+ }
+
+ return registrants || [];
+}
+
+/**
+ * @internal
+ */
+async function isLoggedIn() {
+ const { tokenV3 } = await getAuthUserTokens();
+ return !!tokenV3;
+}
+
+/**
+ * Get the Resource Role ID from provided Role Name
+ * @param {String} roleName
+ * @return {Promise}
+ * @internal
+ */
+async function getRoleId(roleName) {
+ const params = {
+ name: roleName,
+ isActive: true,
+ };
+ const roles = await api
+ .get(`/resource-roles?${qs.stringify(params)}`)
+ .then(util.tryThrowError);
+
+ if (_.isEmpty(roles)) {
+ throw new Error("Resource Role not found!");
+ }
+
+ return roles[0].id;
+}
+
+async function getChallenge(challengeId) {
+ let challenge = {};
+ let isLegacyChallenge = false;
+ let isRegistered = false;
+ let registrants = [];
+ const { tokenV3 } = await getAuthUserTokens();
+ const memberId = tokenV3 ? decodeToken(tokenV3).userId : null;
+
+ if (/^[\d]{5,8}$/.test(challengeId)) {
+ isLegacyChallenge = true;
+ challenge = await getChallengeDetails("/challenges/", {
+ legacyId: challengeId,
+ }).then((res) => res.challenge);
+ } else {
+ challenge = await getChallengeDetails(`/challenges/${challengeId}`).then(
+ (res) => res.challenge
+ );
+ }
+
+ if (challenge) {
+ registrants = await getChallengeRegistrants(challenge.id);
+ isRegistered =
+ memberId && _.some(registrants, (r) => `${r.memberId}` === `${memberId}`);
+ }
+
+ return {
+ ...challenge,
+ isRegistered,
+ };
+}
+
+export default {
+ getChallenge,
+};
diff --git a/src/services/submission.js b/src/services/submission.js
new file mode 100644
index 0000000..a67dd64
--- /dev/null
+++ b/src/services/submission.js
@@ -0,0 +1,57 @@
+import qs from "qs";
+import _ from "lodash";
+import api from "./api";
+
+import * as util from "../utils/api";
+
+async function submit(data, onProgress) {
+ const url = "/submissions/";
+ return api
+ .upload(
+ url,
+ {
+ body: data,
+ method: "POST",
+ },
+ onProgress
+ )
+ .then(
+ (res) => {
+ const jres = JSON.parse(res);
+ return jres;
+ },
+ (err) => {
+ throw err;
+ }
+ );
+}
+
+function deleteSubmission(submissionId) {
+ return api
+ .delete(`/submissions/${submissionId}`)
+ .then(util.tryThrowError)
+ .then(() => submissionId);
+}
+
+function downloadSubmission(track, submissionId) {
+ return api
+ .download(`/submissions/${submissionId}/download`)
+ .then(util.tryThrowError)
+ .then((res) => {
+ return res.blob();
+ });
+}
+
+function getSubmissions(filter) {
+ return api
+ .get(`/submissions?${qs.stringify(filter, { encode: false })}`)
+ .then(util.tryThrowError)
+ .then((res) => res);
+}
+
+export default {
+ submit,
+ deleteSubmission,
+ downloadSubmission,
+ getSubmissions,
+};
diff --git a/src/styles/_legacy-buttons.scss b/src/styles/_legacy-buttons.scss
new file mode 100644
index 0000000..4862526
--- /dev/null
+++ b/src/styles/_legacy-buttons.scss
@@ -0,0 +1,221 @@
+/* LEGACY CODE BELOW */
+
+// Buttons
+
+// Table of Contents
+//
+// Links
+// Buttons
+
+:global {
+ button {
+ cursor: pointer;
+ }
+
+ // Links
+ a.tc-link,
+ .tc-link,
+ a.tc-link:active,
+ .tc-link:active,
+ a.tc-link:visited,
+ .tc-link:visited {
+ cursor: pointer;
+ }
+
+ a.tc-link:hover,
+ .tc-link:hover {
+ color: $tc-dark-blue;
+ text-decoration: none;
+ }
+
+ .tc-btn {
+ @include roboto-regular;
+
+ border-radius: $corner-radius;
+ cursor: pointer;
+ user-select: none;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ // Button States
+
+ a.tc-btn-pressed,
+ .tc-btn-pressed {
+ box-shadow: 0 1px 3px 0 $tc-gray-80;
+ }
+
+ // Button Sizes
+
+ a.tc-btn-lg,
+ .tc-btn-lg {
+ height: 50px;
+ padding: 10px 30px;
+
+ @include tc-heading-md;
+ }
+
+ a.tc-btn-md,
+ .tc-btn-md {
+ height: 40px;
+ padding: 10px 25px;
+
+ @include tc-label-lg;
+ }
+
+ a.tc-btn-sm,
+ .tc-btn-sm {
+ height: 30px;
+ padding: 5px 15px;
+
+ @include tc-label-md;
+ }
+
+ a.tc-btn-xs,
+ .tc-btn-xs {
+ height: 20px;
+ padding: 3px 10px;
+
+ @include tc-label-sm;
+ }
+
+ // Button Types
+
+ .tc-btn-primary {
+ background-color: $tc-dark-blue;
+
+ @include background-gradient($tc-dark-blue, $tc-dark-blue);
+
+ color: $tc-gray-neutral-light;
+ border: 1px solid $tc-dark-blue;
+
+ @include button-transition;
+
+ &:hover {
+ color: $tc-gray-neutral-light;
+
+ @include background-gradient($tc-dark-blue, $tc-dark-blue);
+ }
+
+ &:active {
+ color: $tc-gray-neutral-light;
+
+ @include background-gradient($tc-dark-blue, $tc-dark-blue);
+ }
+ }
+
+ .tc-btn-secondary {
+ background-color: $tc-gray-40;
+ color: $tc-gray-neutral-light;
+ border: 1px solid $tc-gray-50;
+
+ @include button-transition;
+
+ &:hover {
+ color: $tc-gray-neutral-light;
+
+ @include background-gradient($tc-gray-40, $tc-gray-50);
+ }
+
+ &:active {
+ color: $tc-gray-neutral-light;
+
+ @include background-gradient($tc-gray-50, $tc-gray-40);
+ }
+ }
+
+ a.tc-btn-default,
+ .tc-btn-default {
+ background-color: white;
+ color: $tc-gray-70;
+ border: 1px solid $tc-gray-50;
+ }
+
+ a.tc-btn-warning,
+ .tc-btn-warning {
+ background-color: $tc-red-70;
+ color: $tc-gray-neutral-light;
+ border: 1px solid $tc-red;
+ }
+
+ button[disabled],
+ a.tc-btn[disabled],
+ button[disabled]:hover,
+ a.tc-btn[disabled]:hover,
+ button[disabled]:active,
+ a.tc-btn[disabled]:active {
+ background-color: $tc-gray-10;
+ border: none;
+ cursor: default;
+ color: $tc-white;
+ }
+
+ .tc-outline-btn {
+ border: 1px solid $tc-gray-30;
+ background: $tc-white;
+ cursor: pointer;
+ display: inline-block;
+ padding: $base-unit - 1 $base-unit * 2;
+ font-weight: 400;
+ font-size: 12px;
+ color: $tc-black;
+ line-height: $base-unit * 4;
+ border-radius: $corner-radius;
+ margin-left: $base-unit * 3;
+ }
+
+ .tc-blue-btn {
+ cursor: pointer;
+ display: inline-block;
+ padding: $base-unit - 1 $base-unit * 2;
+ font-weight: 400;
+ font-size: 12px;
+ border-radius: $corner-radius;
+ margin-left: $base-unit * 3;
+ background: $tc-dark-blue;
+ color: $tc-white;
+ border: none;
+ line-height: $base-unit * 4 + 2;
+ }
+
+ .tc-btn.tc-btn-wide {
+ padding: 0 30px;
+ }
+
+ .tc-btn.tc-btn-s {
+ height: 30px;
+ padding: 0 10px;
+ line-height: 28px;
+ font-size: 12px;
+ font-weight: 500;
+
+ &:active {
+ line-height: 29px;
+ }
+ }
+
+ .tc-btn.tc-btn-ghost {
+ color: #0096ff;
+ background-color: $tc-white;
+
+ &:hover {
+ color: $tc-white;
+ border-color: #0096ff;
+ background-color: #0096ff;
+ }
+
+ &:active {
+ color: $tc-white;
+ border-color: #097dce;
+ background-color: #097dce;
+ box-shadow: inset 0 1px 1px 0 rgba(0, 0, 0, 0.3);
+ }
+
+ &:disabled {
+ border-color: #b7b7b7;
+ color: #b7b7b7;
+ }
+ }
+}
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
index c511d50..0adaf13 100644
--- a/src/styles/_mixins.scss
+++ b/src/styles/_mixins.scss
@@ -10,6 +10,13 @@ $member-blue: #4c50d9;
$member-yellow: #f2c900;
$member-red: #ea1900;
+// Replace the bourbon background with simple linear gradient - we only use this for the buttons;
+@mixin background-gradient($start, $stop) {
+ background: $start;
+ background: linear-gradient(to top, $start 0%, $stop 100%);
+ filter: progid:dximagetransform.microsoft.gradient(startColorstr='$start', endColorstr='$stop', GradientType=0); /* IE6-9 */
+}
+
// Placeholder
@mixin placeholder {
&::-webkit-input-placeholder {
@@ -28,3 +35,7 @@ $member-red: #ea1900;
@content;
}
}
+
+@mixin button-transition {
+ transition: background 0.5s;
+}
diff --git a/src/styles/_typography.scss b/src/styles/_typography.scss
new file mode 100644
index 0000000..21c3fde
--- /dev/null
+++ b/src/styles/_typography.scss
@@ -0,0 +1,143 @@
+// Headings
+
+h1 { @include tc-heading-xl; }
+h2 { @include tc-heading-lg; }
+h3 { @include tc-heading-md; }
+h4 { @include tc-heading-sm; }
+h5 { @include tc-heading-xs; }
+h6 { @include tc-heading-xs; }
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 { color: inherit; }
+
+/* Label text styles. */
+
+@mixin tc-label-xl {
+ @include roboto-medium;
+
+ color: $tc-black;
+ font-size: 20px;
+ line-height: 25px;
+}
+
+@mixin tc-label-lg {
+ @include roboto-medium;
+
+ color: $tc-black;
+ font-size: 15px;
+ line-height: 20px;
+}
+
+@mixin tc-label-md {
+ @include roboto-medium;
+
+ color: $tc-black;
+ font-size: 13px;
+ line-height: 20px;
+}
+
+@mixin tc-label-sm {
+ @include roboto-regular;
+
+ color: $tc-black;
+ font-size: 12px;
+ line-height: 15px;
+}
+
+@mixin tc-label-xs {
+ @include roboto-regular;
+
+ color: $tc-black;
+ font-size: 11px;
+ line-height: 15px;
+}
+
+/* Body text styles. */
+
+@mixin tc-body-lg {
+ @include roboto-regular;
+
+ color: $tc-gray-80;
+ font-size: 20px;
+ line-height: 25px;
+}
+
+@mixin tc-body-md {
+ @include roboto-regular;
+
+ color: $tc-gray-80;
+ font-size: 15px;
+ line-height: 25px;
+}
+
+@mixin tc-body-sm {
+ @include roboto-regular;
+
+ color: $tc-gray-80;
+ font-size: 13px;
+ line-height: 25px;
+}
+
+@mixin tc-body-xs {
+ @include roboto-regular;
+
+ color: $tc-gray-80;
+ font-size: 11px;
+ line-height: 20px;
+}
+
+/* Heading text styles. */
+
+@mixin tc-heading-xl {
+ @include roboto-light;
+
+ color: $tc-black;
+ font-size: 36px;
+ line-height: 45px;
+}
+
+@mixin tc-heading-lg {
+ @include roboto-regular;
+
+ color: $tc-black;
+ font-size: 28px;
+ line-height: 35px;
+}
+
+@mixin tc-heading-md {
+ @include roboto-regular;
+
+ color: $tc-black;
+ font-size: 20px;
+ line-height: 30px;
+}
+
+@mixin tc-heading-sm {
+ @include roboto-bold;
+
+ color: $tc-black;
+ font-size: 15px;
+ line-height: 25px;
+}
+
+@mixin tc-heading-xs {
+ @include roboto-bold;
+
+ color: $tc-black;
+ font-size: 13px;
+ line-height: 25px;
+}
+
+/* Titles text styles. */
+
+@mixin tc-title {
+ @include roboto-light;
+
+ color: $tc-black;
+ font-size: 42px;
+ line-height: 50px;
+}
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 6a8f540..5ca32ec 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -4,6 +4,10 @@
@import "utils";
:global {
+ :not(.challenge-listing-container) {
+ @import "legacy-buttons";
+ }
+
html,
body {
text-rendering: geometricPrecision;
@@ -23,4 +27,13 @@
body {
flex: 1;
}
+}
+
+#tooltips-container-id {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 0;
+ z-index: 1001;
} // end :global
diff --git a/src/styles/mixins/_resetChallengeListing.scss b/src/styles/mixins/_resetChallengeListing.scss
new file mode 100644
index 0000000..5006894
--- /dev/null
+++ b/src/styles/mixins/_resetChallengeListing.scss
@@ -0,0 +1,22 @@
+@mixin reset() {
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ }
+
+ h1 { @include heading-xl; }
+ h2 { @include heading-lg; }
+ h3 { @include heading-md; }
+ h4 { @include heading-sm; }
+ h5 { @include heading-xs; }
+ h6 { @include heading-xs; }
+
+ h1,h2,h3,h4,h5,h6 {
+ line-height: 1;
+ }
+
+ a {
+ cursor: pointer;
+ }
+}
diff --git a/src/utils/api.js b/src/utils/api.js
new file mode 100644
index 0000000..7b2510e
--- /dev/null
+++ b/src/utils/api.js
@@ -0,0 +1,10 @@
+export const tryThrowError = async (res) => {
+ if (!res.ok) {
+ // network failure
+ if (res.statusText) {
+ throw new Error(res.statusText);
+ }
+ }
+
+ return res;
+};
diff --git a/src/utils/challenge.js b/src/utils/challenge.js
index 3532748..5aca8a4 100644
--- a/src/utils/challenge.js
+++ b/src/utils/challenge.js
@@ -484,3 +484,18 @@ export function updateChallengeType(challenges, challengeTypeMap) {
});
}
}
+
+export const currentPhase = (phases) => {
+ return phases
+ .filter((p) => p.name !== "Registration" && p.isOpen)
+ .sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0];
+};
+
+export const submissionPhase = (phases) => {
+ return phases.filter((p) => p.name === "Submission")[0];
+};
+
+export const isLegacyId = (id) => /^[\d]{5,8}$/.test(id);
+
+export const isUuid = (id) =>
+ /^[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}|\d{5,8}$/.test(id);
diff --git a/src/utils/index.js b/src/utils/index.js
index f8cbb73..b3fc4e1 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -169,3 +169,13 @@ export function parseTotalPrizes(s) {
}
if (valid) return n;
}
+
+export function triggerDownload(fileName,blob) {
+ const url = window.URL.createObjectURL(new Blob([blob]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', fileName);
+ document.body.appendChild(link);
+ link.click();
+ link.parentNode.removeChild(link);
+}
diff --git a/src/utils/logger.js b/src/utils/logger.js
index 56f89c7..0f7a6c5 100644
--- a/src/utils/logger.js
+++ b/src/utils/logger.js
@@ -21,6 +21,8 @@
import _ from "lodash";
import config from "../../config";
+import store from "../store";
+import { createAction } from "redux-actions";
const isDev = process.env.APPMODE === "development";
const logger = {};
@@ -77,4 +79,25 @@ if (leLogger) {
extend("warn", "warning");
}
+/**
+ * The function behaves similarly to javascript alert()
+ * it will show a modal error diaglog with styling until the user clicks OK.
+ */
+export const fireErrorMessage = (title, details) => {
+ setImmediate(() => {
+ const newError = createAction("NEW_ERROR", (paramTitle, paramDetails) => ({
+ title: paramTitle,
+ details: paramDetails,
+ }));
+ store.dispatch(newError(title, details));
+ });
+};
+
+export const clearErrorMesssage = () => {
+ setImmediate(() => {
+ const clearError = createAction("CLEAR_ERROR");
+ store.dispatch(clearError());
+ });
+};
+
export default logger;
diff --git a/src/utils/submission.js b/src/utils/submission.js
index ddef534..106c858 100644
--- a/src/utils/submission.js
+++ b/src/utils/submission.js
@@ -180,4 +180,83 @@ export function processMMSubmissions(submissions) {
return finalSubmissions;
}
+export const isSubmissionEnded = (challenge) => {
+ const { status, phases } = challenge;
+
+ return (
+ status === "COMPLETED" ||
+ (!_.some(phases, { name: "Submission", isOpen: true }) &&
+ !_.some(phases, { name: "Checkpoint Submission", isOpen: true }))
+ );
+};
+
+export const canSubmitFinalFixes = (challenge, handle) => {
+ const { winners, phases } = challenge;
+ const hasFirstPlacement =
+ !_.isEmpty(winners) && _.some(winners, { placement: 1, handle });
+
+ let canSubmit = false;
+ if (hasFirstPlacement && !_.isEmpty(phases)) {
+ canSubmit = _.some(phases, { phaseType: "Final Fix", isOpen: true });
+ }
+
+ return canSubmit;
+};
+
+export const isChallengeBelongToTopgearGroup = (challenge, communityList) => {
+ const { groups } = challenge;
+
+ // check if challenge belong to any group
+ if (!_.isEmpty(groups)) {
+ return false;
+ }
+
+ const topGearCommunity = _.find(communityList, { mainSubdomain: "topgear" });
+ if (!topGearCommunity) {
+ return false;
+ }
+
+ // check the group info match with group list
+ for (let i = 0; i < groups.length; i += 1) {
+ if (groups[i] && _.includes(topGearCommunity.groupIds, groups[i])) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+export const getSubmissionDetail = (challenge) => {
+ const { phases } = challenge;
+
+ const checkpoint = _.find(phases, {
+ name: "Checkpoint Submission",
+ });
+ const submission = _.find(phases, {
+ name: "Submission",
+ });
+ const finalFix = _.find(phases, {
+ name: "Final Fix",
+ });
+ let subType;
+
+ // Submission type logic
+ if (checkpoint && checkpoint.isOpen) {
+ subType = "Checkpoint Submission";
+ } else if (
+ checkpoint &&
+ !checkpoint.isOpen &&
+ submission &&
+ submission.isOpen
+ ) {
+ subType = "Contest Submission";
+ } else if (finalFix && finalFix.isOpen) {
+ subType = "Studio Final Fix Submission";
+ } else {
+ subType = "Contest Submission";
+ }
+
+ return subType;
+};
+
export default undefined;