diff --git a/src/assets/images/arrow-down.svg b/src/assets/images/arrow-down.svg
new file mode 100644
index 00000000..d53eedd1
--- /dev/null
+++ b/src/assets/images/arrow-down.svg
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/check-mark.svg b/src/assets/images/check-mark.svg
new file mode 100644
index 00000000..36586472
--- /dev/null
+++ b/src/assets/images/check-mark.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/ChallengeViewTabs.module.scss b/src/components/ChallengeEditor/ChallengeViewTabs/ChallengeViewTabs.module.scss
new file mode 100644
index 00000000..27533d89
--- /dev/null
+++ b/src/components/ChallengeEditor/ChallengeViewTabs/ChallengeViewTabs.module.scss
@@ -0,0 +1,25 @@
+@import '../../../styles/includes';
+
+.list {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+
+.tabsContainer {
+ ul {
+ margin: 0;
+ }
+
+ :global {
+ .react-tabs__tab--selected {
+ background: $light-bg;
+
+ &:focus::after {
+ background: $light-bg;
+ }
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js
new file mode 100644
index 00000000..f59eec20
--- /dev/null
+++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js
@@ -0,0 +1,109 @@
+/**
+ * Component to render tabs in challenge view page
+ */
+import React, { useState, useMemo } from 'react'
+import PropTypes from 'prop-types'
+import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
+
+import ChallengeViewComponent from '../ChallengeView'
+import Registrants from '../Registrants'
+import { getResourceRoleByName } from '../../../util/tc'
+import 'react-tabs/style/react-tabs.css'
+import styles from './ChallengeViewTabs.module.scss'
+
+const ChallengeViewTabs = ({
+ projectDetail,
+ challenge,
+ attachments,
+ isBillingAccountExpired,
+ metadata,
+ challengeResources,
+ token,
+ isLoading,
+ challengeId,
+ assignedMemberDetails,
+ enableEdit,
+ onLaunchChallenge,
+ onCloseTask
+}) => {
+ const [selectedTab, setSelectedTab] = useState(0)
+
+ const registrants = useMemo(() => {
+ const { resourceRoles } = metadata
+ const role = getResourceRoleByName(resourceRoles, 'Submitter')
+ if (role && challengeResources) {
+ return challengeResources.filter(resource => resource.roleId === role.id)
+ } else {
+ return []
+ }
+ }, [metadata, challengeResources])
+
+ return (
+
+ {
+ setSelectedTab(index)
+ }}
+ >
+
+ DETAILS
+ REGISTRANTS({registrants.length})
+
+
+
+
+
+ {selectedTab === 0 && (
+
+ )}
+ {selectedTab === 1 && (
+
+ )}
+
+ )
+}
+
+ChallengeViewTabs.defaultProps = {
+ projectDetail: {},
+ challenge: {},
+ metadata: {},
+ challengeResources: {},
+ token: ''
+}
+
+ChallengeViewTabs.propTypes = {
+ projectDetail: PropTypes.object,
+ challenge: PropTypes.object,
+ isBillingAccountExpired: PropTypes.bool,
+ attachments: PropTypes.array,
+ metadata: PropTypes.object,
+ token: PropTypes.string,
+ isLoading: PropTypes.bool.isRequired,
+ challengeId: PropTypes.string.isRequired,
+ challengeResources: PropTypes.arrayOf(PropTypes.object),
+ assignedMemberDetails: PropTypes.shape(),
+ enableEdit: PropTypes.bool,
+ onLaunchChallenge: PropTypes.func,
+ onCloseTask: PropTypes.func
+}
+
+export default ChallengeViewTabs
diff --git a/src/components/ChallengeEditor/Registrants/Registrants.module.scss b/src/components/ChallengeEditor/Registrants/Registrants.module.scss
new file mode 100644
index 00000000..5f1063eb
--- /dev/null
+++ b/src/components/ChallengeEditor/Registrants/Registrants.module.scss
@@ -0,0 +1,315 @@
+@import "../../../styles/includes";
+
+$tc-gold: #ffd84d;
+$tc-gold-130: #9a6d00;
+$tc-silver: #d1d0cf;
+$tc-silver-130: #554e48;
+$tc-bronze: #d98f64;
+$tc-bronze-130: #733d17;
+$tc-gray-10: #d5d5d5;
+$member-gray: #555;
+$member-green: #258205;
+$member-blue: #4c50d9;
+$member-yellow: #f2c900;
+$member-red: #ea1900;
+
+.container {
+ @include roboto;
+
+ padding-top: 66px;
+ padding-bottom: 197px;
+ max-width: 966px;
+ margin: 0 auto;
+
+ @include xs-to-sm {
+ padding: 0 30px 30px;
+ }
+}
+
+.head {
+ font-weight: 500;
+ font-size: 13px;
+ line-height: 15px;
+ padding-bottom: 14px;
+ color: $tc-gray-50;
+ display: flex;
+ border-bottom: 1px solid $tc-gray-10;
+
+ @include xs-to-sm {
+ display: none;
+ }
+}
+
+.body {
+ @include xs-to-sm {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+.row {
+ display: flex;
+ line-height: 50px;
+ color: $tc-gray-70;
+ font-size: 15px;
+ border-bottom: 1px solid $tc-gray-10;
+
+ @include xs-to-sm {
+ flex-direction: column;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ width: 100%;
+ }
+}
+
+.sm-only {
+ display: none;
+
+ @include xs-to-sm {
+ display: block;
+ }
+}
+
+.title {
+ color: $tc-gray-50;
+ font-size: 13px;
+ line-height: 15px;
+ font-weight: 500;
+}
+
+.col-1,
+.col-2,
+.col-3,
+.col-4,
+.col-5,
+.col-6 {
+ display: flex;
+ align-items: center;
+ border: none;
+ outline: none;
+ font: inherit;
+ color: inherit;
+ padding: 10;
+
+ @include xs-to-sm {
+ padding-left: 0;
+ line-height: 20px;
+ width: auto;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+.col-arrow {
+ display: flex;
+ padding-left: 5px;
+
+ :global {
+ svg {
+ margin-bottom: -1px;
+ }
+ }
+}
+
+.col-arrow-is-sorting {
+ :global {
+ g {
+ fill: #006ad7;
+ }
+ }
+}
+
+.col-arrow-sort-asc {
+ -moz-transform: scale(1, -1);
+ -o-transform: scale(1, -1);
+ -webkit-transform: scale(1, -1);
+ transform: scale(1, -1);
+}
+
+.col-1 {
+ width: 150px;
+ padding-left: 20px;
+ color: $tc-gray-50;
+
+ > span {
+ line-height: 15px;
+
+ :global {
+ div {
+ display: flex;
+ justify-items: center;
+ height: 100%;
+ }
+ }
+ }
+
+ @include xs-to-sm {
+ padding-left: 0;
+ margin-bottom: 10px;
+ line-height: 20px;
+ width: auto;
+ }
+}
+
+.col-2 {
+ width: 150px;
+ margin-left: 20px;
+
+ @include xs-to-sm {
+ padding-left: 0;
+ margin-bottom: 10px;
+ line-height: 20px;
+ width: auto;
+ }
+}
+
+.col-3 {
+ width: 298px;
+
+ @include xs-to-sm {
+ font-size: 15px;
+ line-height: 25px;
+ margin-bottom: 10px;
+ width: auto;
+ }
+
+ a,
+ a:hover,
+ a:visited {
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
+
+.col-4 {
+ width: 317px;
+
+ @include xs-to-sm {
+ margin-bottom: 15px;
+ line-height: 20px;
+ width: auto;
+ }
+}
+
+.col-5 {
+ display: flex;
+ align-items: center;
+
+ @include xs-to-sm {
+ margin-bottom: 15px;
+ line-height: 20px;
+ flex-direction: column;
+ align-items: flex-start;
+ width: auto;
+ }
+}
+
+.col-6 {
+ display: flex;
+ align-items: center;
+ width: 351px;
+
+ @include xs-to-sm {
+ line-height: 20px;
+ flex-direction: column;
+ align-items: flex-start;
+ width: auto;
+ }
+}
+
+.table-header {
+ cursor: pointer;
+ background: transparent;
+
+ &:hover {
+ color: #006ad7;
+
+ :global {
+ g {
+ fill: #006ad7;
+ }
+ }
+ }
+}
+
+.passed {
+ display: inline-block;
+ height: 22px;
+ margin-bottom: 3px;
+ margin-left: 9px;
+ vertical-align: middle;
+ width: 22px;
+}
+
+.placement {
+ background-color: #cee6ff;
+ color: #006ad7;
+ display: inline-block;
+ width: 22px;
+ height: 22px;
+ border-radius: 100%;
+ line-height: 22px;
+ text-align: center;
+ margin-left: 6px;
+}
+
+.placement-1 {
+ background-color: $tc-gold;
+ color: $tc-gold-130;
+}
+
+.placement-2 {
+ background-color: $tc-silver;
+ color: $tc-silver-130;
+}
+
+.placement-3 {
+ background-color: $tc-bronze;
+ color: $tc-bronze-130;
+}
+
+.design {
+ .col-3 {
+ width: 212px;
+ }
+
+ .col-4 {
+ width: 231px;
+ }
+
+ .col-5 {
+ width: 265px;
+ }
+
+ .col-6 {
+ width: 258px;
+ }
+}
+
+.tooltip {
+ font-size: 14px;
+ margin: 10px 10px 0 10px;
+ padding-top: 5px;
+}
+
+.level-1 {
+ color: $member-gray !important;
+}
+
+.level-2 {
+ color: $member-green !important;
+}
+
+.level-3 {
+ color: $member-blue !important;
+}
+
+.level-4 {
+ color: $member-yellow !important;
+}
+
+.level-5 {
+ color: $member-red !important;
+}
diff --git a/src/components/ChallengeEditor/Registrants/index.js b/src/components/ChallengeEditor/Registrants/index.js
new file mode 100644
index 00000000..69e00782
--- /dev/null
+++ b/src/components/ChallengeEditor/Registrants/index.js
@@ -0,0 +1,545 @@
+/* eslint jsx-a11y/no-static-element-interactions:0 */
+/**
+ * Registrants tab component.
+ */
+
+import React from 'react'
+import PT from 'prop-types'
+import moment from 'moment'
+import _ from 'lodash'
+import cn from 'classnames'
+import ReactSVG from 'react-svg'
+import { getRatingLevel, sortList } from '../../../util/tc'
+import styles from './Registrants.module.scss'
+
+const assets = require.context('../../../assets/images', false, /svg/)
+const CheckMark = './check-mark.svg'
+const ArrowDown = './arrow-down.svg'
+
+function formatDate (date) {
+ if (!date) return '-'
+ return moment(date)
+ .local()
+ .format('MMM DD, YYYY HH:mm')
+}
+
+function getDate (arr, handle) {
+ const results = arr
+ .filter(
+ a => _.toString(a.createdBy || a.memberHandle) === _.toString(handle)
+ )
+ .sort(
+ (a, b) =>
+ new Date(b.submissionTime || b.submissionDate).getTime() -
+ new Date(a.submissionTime || a.submissionDate).getTime()
+ )
+ return results[0]
+ ? results[0].submissionTime || results[0].submissionDate
+ : ''
+}
+
+function passedCheckpoint (checkpoints, handle, results) {
+ const mine = checkpoints.filter(
+ c => _.toString(c.createdBy) === _.toString(handle)
+ )
+ return _.some(mine, m =>
+ _.find(results, r => r.submissionId === m.submissionId)
+ )
+}
+
+function getPlace (results, handle, places) {
+ const found = _.find(
+ results,
+ w =>
+ _.toString(w.memberHandle) === _.toString(handle) &&
+ w.placement <= places &&
+ w.submissionStatus !== 'Failed Review'
+ )
+
+ if (found) {
+ return found.placement
+ }
+ return -1
+}
+
+export default class Registrants extends React.Component {
+ constructor (props, context) {
+ super(props, context)
+
+ this.state = {
+ sortedRegistrants: [],
+ registrantsSort: {
+ field: '',
+ sort: ''
+ }
+ }
+
+ this.getCheckPoint = this.getCheckPoint.bind(this)
+ this.getCheckPointDate = this.getCheckPointDate.bind(this)
+ this.getFlagFirstTry = this.getFlagFirstTry.bind(this)
+ this.sortRegistrants = this.sortRegistrants.bind(this)
+ this.getRegistrantsSortParam = this.getRegistrantsSortParam.bind(this)
+ this.updateSortedRegistrants = this.updateSortedRegistrants.bind(this)
+ this.onSortChange = this.onSortChange.bind(this)
+ }
+
+ componentDidMount () {
+ this.updateSortedRegistrants()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { registrants, registrantsSort } = this.props
+ if (
+ !_.isEqual(prevProps.registrants, registrants) ||
+ !_.isEqual(prevProps.registrantsSort, registrantsSort)
+ ) {
+ this.updateSortedRegistrants()
+ }
+ }
+ onSortChange (sort) {
+ this.setState({
+ registrantsSort: sort
+ })
+ this.updateSortedRegistrants()
+ }
+ /**
+ * Get checkpoint date of registrant
+ */
+ getCheckPointDate () {
+ const { challenge } = this.props
+ const checkpointPhase = (challenge.phases || []).find(
+ x => x.name === 'Checkpoint Submission'
+ )
+ return moment(
+ checkpointPhase
+ ? checkpointPhase.actualEndDate || checkpointPhase.scheduledEndDate
+ : 0
+ )
+ }
+
+ /**
+ * Get checkpoint of registrant
+ * @param {Object} registrant registrant info
+ */
+ getCheckPoint (registrant) {
+ const { challenge } = this.props
+ const checkpoints = challenge.checkpoints || []
+ const checkpointDate = this.getCheckPointDate()
+
+ const twoRounds =
+ challenge.round1Introduction && challenge.round2Introduction
+
+ let checkpoint
+ if (twoRounds) {
+ checkpoint = getDate(checkpoints, registrant.memberHandle)
+ if (
+ !checkpoint &&
+ moment(registrant.submissionDate).isBefore(checkpointDate)
+ ) {
+ checkpoint = registrant.submissionDate
+ }
+ }
+
+ return checkpoint
+ }
+
+ /**
+ * Get final of registrant
+ * @param {Object} registrant get checkpoint of registrant
+ */
+ getFinal (registrant) {
+ let final
+ if (moment(registrant.submissionDate).isAfter(this.getCheckPointDate())) {
+ final = registrant.submissionDate
+ }
+ return final
+ }
+
+ /**
+ * Check if it have flag for first try
+ * @param {Object} registrant registrant info
+ */
+ getFlagFirstTry (registrant) {
+ const { notFoundCountryFlagUrl } = this.props
+ if (
+ !registrant.countryInfo ||
+ notFoundCountryFlagUrl[registrant.countryInfo.countryCode]
+ ) {
+ return null
+ }
+
+ return registrant.countryInfo.countryFlag
+ }
+
+ /**
+ * Get registrans sort parameter
+ */
+ getRegistrantsSortParam () {
+ const { registrantsSort } = this.state
+ let { field, sort } = registrantsSort
+ if (!field) {
+ field = 'Registration Date' // default field for registrans sorting
+ }
+ if (!sort) {
+ sort = 'asc' // default order for registrans sorting
+ }
+
+ return {
+ field,
+ sort
+ }
+ }
+
+ /**
+ * Update sorted registrant array
+ */
+ updateSortedRegistrants () {
+ const { registrants } = this.props
+ const sortedRegistrants = _.cloneDeep(registrants)
+ this.sortRegistrants(sortedRegistrants)
+ this.setState({ sortedRegistrants })
+ }
+
+ /**
+ * Sort array of registrant
+ * @param {Array} registrants array of registrant
+ */
+ sortRegistrants (registrants) {
+ const { field, sort } = this.getRegistrantsSortParam()
+ return sortList(registrants, field, sort, (a, b) => {
+ let valueA = 0
+ let valueB = 0
+ let valueIsString = false
+ switch (field) {
+ case 'Country': {
+ valueA = a.countryCode
+ valueB = b.countryCode
+ valueIsString = true
+ break
+ }
+ case 'Rating': {
+ valueA = a.rating
+ valueB = b.rating
+ break
+ }
+ case 'Username': {
+ valueA = `${a.memberHandle}`.toLowerCase()
+ valueB = `${b.memberHandle}`.toLowerCase()
+ valueIsString = true
+ break
+ }
+ case 'Registration Date': {
+ valueA = new Date(a.created)
+ valueB = new Date(b.created)
+ break
+ }
+ case 'Round 1 Submitted Date': {
+ const checkpointA = this.getCheckPoint(a)
+ const checkpointB = this.getCheckPoint(b)
+ if (checkpointA) {
+ valueA = new Date(checkpointA)
+ }
+ if (checkpointB) {
+ valueB = new Date(checkpointB)
+ }
+ break
+ }
+ case 'Submitted Date': {
+ const checkpointA = this.getFinal(a)
+ const checkpointB = this.getFinal(b)
+ if (checkpointA) {
+ valueA = new Date(checkpointA)
+ }
+ if (checkpointB) {
+ valueB = new Date(checkpointB)
+ }
+ break
+ }
+ default:
+ }
+
+ return {
+ valueA,
+ valueB,
+ valueIsString
+ }
+ })
+ }
+
+ render () {
+ const { challenge, checkpointResults, results } = this.props
+ const { prizeSets, track } = challenge
+
+ const { sortedRegistrants } = this.state
+ const { field, sort } = this.getRegistrantsSortParam()
+ const revertSort = sort === 'desc' ? 'asc' : 'desc'
+ const isDesign = track.toLowerCase() === 'design'
+
+ const placementPrizes = _.find(prizeSets, { type: 'placement' })
+ const { prizes } = placementPrizes || []
+
+ const checkpoints = challenge.checkpoints || []
+
+ const twoRounds =
+ challenge.round1Introduction && challenge.round2Introduction
+ const places = (prizes && prizes.length) || 0
+ console.log(styles)
+ return (
+
+
+ {!isDesign && (
+
+ )}
+
+
+ {twoRounds && (
+
+ )}
+
+
+
+ {sortedRegistrants.map(r => {
+ const placement = getPlace(results, r.memberHandle, places)
+ let checkpoint = this.getCheckPoint(r)
+ if (checkpoint) {
+ checkpoint = formatDate(checkpoint)
+ }
+ const final = this.getFinal(r)
+
+ return (
+
+ {!isDesign && (
+
+
+ Rating
+
+
+
+ {!_.isNil(r.rating) && r.rating !== 0 ? r.rating : '-'}
+
+
+
+ )}
+
+
+
+ Registration Date
+
+
{formatDate(r.created)}
+
+ {twoRounds && (
+
+
+ Round 1 Submitted Date
+
+
+ {checkpoint}
+ {passedCheckpoint(
+ checkpoints,
+ r.memberHandle,
+ checkpointResults
+ ) && (
+
+ )}
+
+
+ )}
+
+
+ {twoRounds ? 'Round 2 ' : ''}
+ Submitted Date
+
+
+ {formatDate(final)}
+ {placement > 0 && (
+
+ {placement}
+
+ )}
+
+
+
+ )
+ })}
+
+
+ )
+ }
+}
+
+Registrants.defaultProps = {
+ results: [],
+ checkpointResults: {},
+ registrantsSort: {},
+ notFoundCountryFlagUrl: {},
+ onGetFlagImageFail: {},
+ registrants: []
+}
+
+Registrants.propTypes = {
+ challenge: PT.shape({
+ phases: PT.arrayOf(
+ PT.shape({
+ actualEndDate: PT.string,
+ name: PT.string.isRequired,
+ scheduledEndDate: PT.string
+ })
+ ).isRequired,
+ checkpoints: PT.arrayOf(PT.shape()),
+ subTrack: PT.any,
+ prizeSets: PT.arrayOf(PT.shape()).isRequired,
+ registrants: PT.arrayOf(PT.shape()).isRequired,
+ round1Introduction: PT.string,
+ round2Introduction: PT.string,
+ type: PT.string,
+ track: PT.string
+ }).isRequired,
+ results: PT.arrayOf(PT.shape()),
+ checkpointResults: PT.shape(),
+ registrants: PT.arrayOf(PT.shape()),
+ registrantsSort: PT.shape({
+ field: PT.string,
+ sort: PT.string
+ }),
+ notFoundCountryFlagUrl: PT.objectOf(PT.bool).isRequired
+}
diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js
index 3be5fd34..ace3ae45 100644
--- a/src/containers/ChallengeEditor/index.js
+++ b/src/containers/ChallengeEditor/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import { withRouter, Route } from 'react-router-dom'
import moment from 'moment'
import ChallengeEditorComponent from '../../components/ChallengeEditor'
-import ChallengeViewComponent from '../../components/ChallengeEditor/ChallengeView'
+import ChallengeViewTabs from '../../components/ChallengeEditor/ChallengeViewTabs'
import Loader from '../../components/Loader'
import { checkAdmin } from '../../util/tc'
import styles from './ChallengeEditor.module.scss'
@@ -377,7 +377,7 @@ class ChallengeEditor extends Component {
exact
path={`${this.props.match.path}/view`}
render={({ match }) => ((
- {
+ if (a > b) {
+ return 1
+ }
+
+ if (a === b) {
+ return 0
+ }
+
+ return -1
+ }
+
+ list.sort((a, b) => {
+ let valueForAB = {}
+ valueForAB = getValue(a, b)
+ let { valueA, valueB } = valueForAB
+ const { valueIsString } = valueForAB
+ if (valueIsString) {
+ if (_.isNil(valueA)) {
+ valueA = ''
+ }
+ if (_.isNil(valueB)) {
+ valueB = ''
+ }
+ } else {
+ if (_.isNil(valueA)) {
+ valueA = 0
+ }
+ if (_.isNil(valueB)) {
+ valueB = 0
+ }
+ }
+ if (sort === 'desc') {
+ return compare(valueB, valueA)
+ }
+
+ return compare(valueA, valueB)
+ })
+}
/**
* Given a rating value, returns corresponding color.
* @param {Number} rating Rating.