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 @@ + + + + 5D558F9B-43EB-41DF-905D-8862EFEE8959 + Created with sketchtool. + + + + + + + + + + + + + + \ 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 @@ + + + icon checkpoint + + + + + + 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 : '-'} + +
+
+ )} +
+ + + {r.memberHandle} + + +
+
+
+ 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.