diff --git a/.circleci/config.yml b/.circleci/config.yml
index a456a88e..d348291b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -160,7 +160,7 @@ workflows:
context: org-global
filters: &filters-dev
branches:
- only: ["develop", "pm-2917", "points", "pm-3270", "engagements"]
+ only: ["develop", "pm-2917", "points", "pm-3270", "projects-api-v6"]
# Production builds are exectuted only on tagged commits to the
# master branch.
diff --git a/config/constants/development.js b/config/constants/development.js
index af3244d6..67b69f47 100644
--- a/config/constants/development.js
+++ b/config/constants/development.js
@@ -30,7 +30,7 @@ module.exports = {
CHALLENGE_PHASES_URL: `${DEV_API_HOSTNAME}/v6/challenge-phases`,
CHALLENGE_TIMELINES_URL: `${DEV_API_HOSTNAME}/v6/challenge-timelines`,
COPILOTS_URL: 'https://copilots.topcoder-dev.com',
- PROJECT_API_URL: `${DEV_API_HOSTNAME}/v5/projects`,
+ PROJECT_API_URL: `${DEV_API_HOSTNAME}/v6/projects`,
GROUPS_API_URL: `${DEV_API_HOSTNAME}/v6/groups`,
TERMS_API_URL: `${DEV_API_HOSTNAME}/v5/terms`,
RESOURCES_API_URL: `${DEV_API_HOSTNAME}/v6/resources`,
diff --git a/config/constants/local.js b/config/constants/local.js
index ad2ae1da..c551b163 100644
--- a/config/constants/local.js
+++ b/config/constants/local.js
@@ -12,6 +12,7 @@ const LOCAL_MEMBER_API = 'http://localhost:3003/v6'
const LOCAL_RESOURCE_API = 'http://localhost:3004/v6'
const LOCAL_REVIEW_API = 'http://localhost:3005/v6'
const LOCAL_SKILLS_API_V5 = 'http://localhost:3006/v5/standardized-skills'
+const LOCAL_PROJECTS_API = 'http://localhost:3008/v6/projects'
// Lookups API available on 3007 if needed in future
// const LOCAL_LOOKUPS_API = 'http://localhost:3007/v6'
@@ -46,8 +47,8 @@ module.exports = {
// Copilots and other apps remain on dev
COPILOTS_URL: 'https://copilots.topcoder-dev.com',
- // Projects API: keep dev unless you run projects locally
- PROJECT_API_URL: `${DEV_API_HOSTNAME}/v5/projects`,
+ // Projects API v6: keep dev default (switch to LOCAL_PROJECTS_API when needed)
+ PROJECT_API_URL: `${DEV_API_HOSTNAME}/v6/projects`,
// Local groups/resources/review services
GROUPS_API_URL: `${LOCAL_GROUPS_API}/groups`,
diff --git a/config/constants/production.js b/config/constants/production.js
index c4f69daf..94c3e95e 100644
--- a/config/constants/production.js
+++ b/config/constants/production.js
@@ -29,7 +29,7 @@ module.exports = {
CHALLENGE_PHASES_URL: `${PROD_API_HOSTNAME}/v6/challenge-phases`,
CHALLENGE_TIMELINES_URL: `${PROD_API_HOSTNAME}/v6/challenge-timelines`,
COPILOTS_URL: `https://copilots.${DOMAIN}`,
- PROJECT_API_URL: `${PROD_API_HOSTNAME}/v5/projects`,
+ PROJECT_API_URL: `${PROD_API_HOSTNAME}/v6/projects`,
GROUPS_API_URL: `${PROD_API_HOSTNAME}/v6/groups`,
TERMS_API_URL: `${PROD_API_HOSTNAME}/v5/terms`,
MEMBERS_API_URL: `${PROD_API_HOSTNAME}/v5/members`,
diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
index 0de3fa0b..d22fd537 100644
--- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
+++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
@@ -61,6 +61,45 @@ const normalizeTrackForScorecards = (challenge, metadata) => {
return null
}
+const normalizePhaseToken = (value) => (value || '')
+ .toString()
+ .toLowerCase()
+ .trim()
+ .replace(/\bphase\b$/, '')
+ .replace(/[-_\s]/g, '')
+
+const normalizeIdValue = (value) => (
+ value === undefined || value === null
+ ? ''
+ : value.toString()
+)
+
+const getScorecardsForPhase = (scorecards = [], phases = [], phaseId) => {
+ const normalizedPhaseId = normalizeIdValue(phaseId)
+ if (!normalizedPhaseId) {
+ return []
+ }
+
+ const selectedPhase = phases.find(phase => (
+ normalizeIdValue(phase.phaseId) === normalizedPhaseId ||
+ normalizeIdValue(phase.id) === normalizedPhaseId
+ ))
+
+ if (!selectedPhase || !selectedPhase.name) {
+ return []
+ }
+
+ const normalizedPhaseName = normalizePhaseToken(selectedPhase.name)
+ if (!normalizedPhaseName) {
+ return []
+ }
+
+ return scorecards.filter(scorecard => (
+ scorecard &&
+ normalizePhaseToken(scorecard.type) === normalizedPhaseName
+ ))
+}
+
class ChallengeReviewerField extends Component {
constructor (props) {
super(props)
@@ -602,6 +641,31 @@ class ChallengeReviewerField extends Component {
baseCoefficient: defaultReviewer.baseCoefficient,
incrementalCoefficient: defaultReviewer.incrementalCoefficient
})
+
+ if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
+ const { metadata = {} } = this.props
+ const scorecardsForPhase = getScorecardsForPhase(
+ metadata.scorecards || [],
+ challenge.phases || [],
+ value
+ )
+ const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
+ const hasCurrentScorecard = scorecardsForPhase.some(scorecard => (
+ normalizeIdValue(scorecard.id) === currentScorecardId
+ ))
+
+ if (!hasCurrentScorecard) {
+ const defaultScorecardId = normalizeIdValue(defaultReviewer && defaultReviewer.scorecardId)
+ const hasDefaultScorecard = defaultScorecardId && scorecardsForPhase.some(scorecard => (
+ normalizeIdValue(scorecard.id) === defaultScorecardId
+ ))
+ const fallbackScorecardId = hasDefaultScorecard
+ ? defaultScorecardId
+ : normalizeIdValue(scorecardsForPhase[0] && scorecardsForPhase[0].id)
+
+ fieldUpdate.scorecardId = fallbackScorecardId || ''
+ }
+ }
}
if (field === 'memberReviewerCount') {
@@ -661,29 +725,12 @@ class ChallengeReviewerField extends Component {
const { challenge, metadata = {}, readOnly = false } = this.props
const { scorecards = [], workflows = [] } = metadata
const validationErrors = challenge.submitTriggered ? this.validateReviewer(reviewer) : {}
- const selectedPhase = challenge.phases.find(p => p.phaseId === reviewer.phaseId)
+ const filteredScorecards = getScorecardsForPhase(
+ scorecards,
+ challenge.phases || [],
+ reviewer.phaseId
+ )
const isDesignChallenge = challenge && challenge.trackId === DES_TRACK_ID
- const normalize = (value) => (value || '')
- .toString()
- .toLowerCase()
- .trim()
- .replace(/\bphase\b$/, '')
- .replace(/[-_\s]/g, '')
-
- const filteredScorecards = scorecards.filter(item => {
- if (!selectedPhase || !selectedPhase.name || !item || !item.type) {
- return false
- }
-
- const normalizedType = normalize(item.type)
- const normalizedPhaseName = normalize(selectedPhase.name)
-
- if (!normalizedType || !normalizedPhaseName) {
- return false
- }
-
- return normalizedType === normalizedPhaseName
- })
return (
diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js
index ee500aaa..8637bd6a 100644
--- a/src/components/ChallengeEditor/ChallengeView/index.js
+++ b/src/components/ChallengeEditor/ChallengeView/index.js
@@ -31,6 +31,7 @@ import {
import PhaseInput from '../../PhaseInput'
import CheckpointPrizesField from '../CheckpointPrizes-Field'
import { isBetaMode } from '../../../util/localstorage'
+import WiproAllowedField from '../WiproAllowedField'
const ChallengeView = ({
projectDetail,
@@ -95,6 +96,7 @@ const ChallengeView = ({
if (isLoading || _.isEmpty(metadata.challengePhases) || challenge.id !== challengeId) return
const showTimeline = false // disables the timeline for time being https://github.com/topcoder-platform/challenge-engine-ui/issues/706
const isTask = _.get(challenge, 'task.isTask', false)
+ const isFunChallenge = challenge.funChallenge === true
const phases = _.get(challenge, 'phases', [])
const showCheckpointPrizes = _.get(challenge, 'timelineTemplateId') === MULTI_ROUND_CHALLENGE_TEMPLATE_ID
const useDashboardData = _.find(challenge.metadata, { name: 'show_data_dashboard' })
@@ -195,6 +197,7 @@ const ChallengeView = ({
<>
{dashboardToggle}
+
{}} readOnly />
Groups: {groups}
@@ -262,14 +265,24 @@ const ChallengeView = ({
token={token}
readOnly
/>}
-
- {
- showCheckpointPrizes && (
-
- )
- }
-
-
+ {isFunChallenge ? (
+
+
+ Fun Challenge: True
+
+
+ ) : (
+ <>
+
+ {
+ showCheckpointPrizes && (
+
+ )
+ }
+
+
+ >
+ )}
diff --git a/src/components/ChallengeEditor/FunChallengeField/FunChallengeField.module.scss b/src/components/ChallengeEditor/FunChallengeField/FunChallengeField.module.scss
new file mode 100644
index 00000000..422b7433
--- /dev/null
+++ b/src/components/ChallengeEditor/FunChallengeField/FunChallengeField.module.scss
@@ -0,0 +1,76 @@
+@use '../../../styles/includes' as *;
+
+.row {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ margin: 30px 30px 0 30px;
+ align-content: space-between;
+ justify-content: flex-start;
+
+ .tcCheckbox {
+ @include tc-checkbox;
+
+ height: 18px;
+ width: 210px;
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+ position: relative;
+ display: inline-block;
+
+ input[type='checkbox'] {
+ display: none;
+ }
+
+ label {
+ @include roboto-light();
+
+ line-height: 17px;
+ font-weight: 300;
+ cursor: pointer;
+ position: absolute;
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ top: 0;
+ left: 0;
+ border: none;
+ box-shadow: none;
+ background: $tc-gray-30;
+ transition: all 0.15s ease-in-out;
+
+ &::after {
+ opacity: 0;
+ content: '';
+ position: absolute;
+ width: 9px;
+ height: 5px;
+ background: transparent;
+ top: 2px;
+ left: 2px;
+ border-top: none;
+ border-right: none;
+ transform: rotate(-45deg);
+ transition: all 0.15s ease-in-out;
+ }
+
+ &:hover::after {
+ opacity: 0.3;
+ }
+
+ div {
+ margin-left: 24px;
+ width: 300px;
+ }
+ }
+
+ input[type='checkbox']:checked ~ label {
+ background: $tc-blue-20;
+ }
+
+ input[type='checkbox']:checked + label::after {
+ border-color: $white;
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/FunChallengeField/index.js b/src/components/ChallengeEditor/FunChallengeField/index.js
new file mode 100644
index 00000000..d826b916
--- /dev/null
+++ b/src/components/ChallengeEditor/FunChallengeField/index.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import styles from './FunChallengeField.module.scss'
+
+/**
+ * Renders a checkbox to toggle the `funChallenge` flag for Marathon Match challenges.
+ *
+ * @param {Object} props component props
+ * @param {Object} props.challenge challenge data object that may include `funChallenge`
+ * @param {Function} props.onUpdateOthers callback used to update top-level challenge fields
+ * @param {boolean} props.readOnly when true, renders the control as read-only
+ * @returns {import('react').ReactNode} rendered fun challenge checkbox field
+ */
+const FunChallengeField = ({ challenge, onUpdateOthers, readOnly }) => {
+ const isFunChallenge = challenge.funChallenge === true
+
+ return (
+
+
+
onUpdateOthers({ field: 'funChallenge', value: !isFunChallenge })}
+ />
+
+
+
+ )
+}
+
+FunChallengeField.defaultProps = {
+ readOnly: false
+}
+
+FunChallengeField.propTypes = {
+ challenge: PropTypes.shape().isRequired,
+ onUpdateOthers: PropTypes.func.isRequired,
+ readOnly: PropTypes.bool
+}
+
+export default FunChallengeField
diff --git a/src/components/ChallengeEditor/WiproAllowedField/WiproAllowedField.module.scss b/src/components/ChallengeEditor/WiproAllowedField/WiproAllowedField.module.scss
new file mode 100644
index 00000000..422b7433
--- /dev/null
+++ b/src/components/ChallengeEditor/WiproAllowedField/WiproAllowedField.module.scss
@@ -0,0 +1,76 @@
+@use '../../../styles/includes' as *;
+
+.row {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ margin: 30px 30px 0 30px;
+ align-content: space-between;
+ justify-content: flex-start;
+
+ .tcCheckbox {
+ @include tc-checkbox;
+
+ height: 18px;
+ width: 210px;
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+ position: relative;
+ display: inline-block;
+
+ input[type='checkbox'] {
+ display: none;
+ }
+
+ label {
+ @include roboto-light();
+
+ line-height: 17px;
+ font-weight: 300;
+ cursor: pointer;
+ position: absolute;
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ top: 0;
+ left: 0;
+ border: none;
+ box-shadow: none;
+ background: $tc-gray-30;
+ transition: all 0.15s ease-in-out;
+
+ &::after {
+ opacity: 0;
+ content: '';
+ position: absolute;
+ width: 9px;
+ height: 5px;
+ background: transparent;
+ top: 2px;
+ left: 2px;
+ border-top: none;
+ border-right: none;
+ transform: rotate(-45deg);
+ transition: all 0.15s ease-in-out;
+ }
+
+ &:hover::after {
+ opacity: 0.3;
+ }
+
+ div {
+ margin-left: 24px;
+ width: 300px;
+ }
+ }
+
+ input[type='checkbox']:checked ~ label {
+ background: $tc-blue-20;
+ }
+
+ input[type='checkbox']:checked + label::after {
+ border-color: $white;
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/WiproAllowedField/index.js b/src/components/ChallengeEditor/WiproAllowedField/index.js
new file mode 100644
index 00000000..303dab55
--- /dev/null
+++ b/src/components/ChallengeEditor/WiproAllowedField/index.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import styles from './WiproAllowedField.module.scss'
+
+/**
+ * Renders a checkbox to toggle the `wiproAllowed` flag for challenges.
+ *
+ * @param {Object} props component props
+ * @param {Object} props.challenge challenge data object that may include `wiproAllowed`
+ * @param {Function} props.onUpdateOthers callback used to update top-level challenge fields
+ * @param {boolean} props.readOnly when true, renders the control as read-only
+ * @returns {import('react').ReactNode} rendered wipro allowed checkbox field
+ */
+const WiproAllowedField = ({ challenge, onUpdateOthers, readOnly }) => {
+ const isWiproAllowed = challenge.wiproAllowed === true
+
+ return (
+
+
+
onUpdateOthers({ field: 'wiproAllowed', value: !isWiproAllowed })}
+ />
+
+
+
+ )
+}
+
+WiproAllowedField.defaultProps = {
+ readOnly: false
+}
+
+WiproAllowedField.propTypes = {
+ challenge: PropTypes.shape().isRequired,
+ onUpdateOthers: PropTypes.func.isRequired,
+ readOnly: PropTypes.bool
+}
+
+export default WiproAllowedField
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js
index 88297520..de2c2cce 100644
--- a/src/components/ChallengeEditor/index.js
+++ b/src/components/ChallengeEditor/index.js
@@ -67,6 +67,8 @@ import AssignedMemberField from './AssignedMember-Field'
import Tooltip from '../Tooltip'
import CancelDropDown from './Cancel-Dropdown'
import UseSchedulingAPIField from './UseSchedulingAPIField'
+import FunChallengeField from './FunChallengeField'
+import WiproAllowedField from './WiproAllowedField'
import MilestoneField from './Milestone-Field'
import DiscussionField from './Discussion-Field'
@@ -453,6 +455,9 @@ class ChallengeEditor extends Component {
* @param isSub The option from sub field
* @param field The challenge field
* @param index The index of array
+ *
+ * Side effect: when `field` is `typeId` and the selected type is not Marathon Match,
+ * resets `funChallenge` to `false`.
*/
onUpdateSelect (option, isSub = false, field = '', index = -1) {
if (option) {
@@ -460,6 +465,9 @@ class ChallengeEditor extends Component {
const newChallenge = { ...oldChallenge }
if (!isSub) {
newChallenge[field] = option
+ if (field === 'typeId' && option !== MARATHON_TYPE_ID) {
+ newChallenge.funChallenge = false
+ }
} else {
if (index < 0) {
newChallenge[field][option.key] = option.name
@@ -786,8 +794,13 @@ class ChallengeEditor extends Component {
}
checkValidCopilot () {
- const copilotFee = _.find(this.state.challenge.prizeSets, p => p.type === PRIZE_SETS_TYPE.COPILOT_PAYMENT, [])
- if (copilotFee && parseInt(copilotFee.prizes[0].value) > 0 && !this.state.challenge.copilot) {
+ const { challenge } = this.state
+ if (challenge.funChallenge === true) {
+ return true
+ }
+
+ const copilotFee = _.find(challenge.prizeSets, p => p.type === PRIZE_SETS_TYPE.COPILOT_PAYMENT, [])
+ if (copilotFee && parseInt(copilotFee.prizes[0].value) > 0 && !challenge.copilot) {
return false
}
return true
@@ -874,16 +887,17 @@ class ChallengeEditor extends Component {
isValidChallenge () {
const { challenge } = this.state
+ const isFunChallenge = challenge.funChallenge === true
if (this.props.isNew) {
const { name, trackId, typeId } = challenge
return !!name && !!trackId && !!typeId
}
- if (!this.isValidChallengePrizes()) {
+ if (!isFunChallenge && !this.isValidChallengePrizes()) {
return false
}
- if (!this.checkValidCopilot()) {
+ if (!isFunChallenge && !this.checkValidCopilot()) {
return false
}
@@ -899,9 +913,11 @@ class ChallengeEditor extends Component {
'trackId',
'typeId',
'name',
- 'description',
- 'prizeSets'
+ 'description'
]
+ if (!isFunChallenge) {
+ requiredFields.push('prizeSets')
+ }
if (isSkillsRequired) {
requiredFields.push('skills')
}
@@ -1076,6 +1092,15 @@ class ChallengeEditor extends Component {
}, 500)
}
+ /**
+ * Collects challenge payload for create/update API calls.
+ *
+ * Includes `funChallenge` in picked challenge fields and, when `funChallenge`
+ * is enabled, omits `prizeSets` from the payload.
+ *
+ * @param {string} status challenge status to set in payload
+ * @returns {Object} cloned challenge payload ready for API submission
+ */
collectChallengeData (status) {
const { isPhaseChange } = this.state
const { attachments, metadata } = this.props
@@ -1098,7 +1123,9 @@ class ChallengeEditor extends Component {
'discussions',
'task',
'skills',
- 'reviewers'
+ 'reviewers',
+ 'funChallenge',
+ 'wiproAllowed'
], this.state.challenge)
const isTask = _.find(metadata.challengeTypes, { id: challenge.typeId, isTask: true })
challenge.legacy = _.assign(this.state.challenge.legacy, {
@@ -1106,10 +1133,13 @@ class ChallengeEditor extends Component {
})
challenge.timelineTemplateId = _.get(this.getCurrentTemplate(), 'id')
challenge.projectId = this.props.projectId
- challenge.prizeSets = challenge.prizeSets.map(p => {
+ challenge.prizeSets = (challenge.prizeSets || []).map(p => {
const prizes = p.prizes.map(s => ({ ...s, value: convertDollarToInteger(s.value, '$') }))
return { ...p, prizes }
})
+ if (challenge.funChallenge === true) {
+ delete challenge.prizeSets
+ }
challenge.status = status
if (status === CHALLENGE_STATUS.ACTIVE && isTask) {
challenge.startDate = moment().format()
@@ -1810,6 +1840,8 @@ class ChallengeEditor extends Component {
const copilotResources = metadata.members || challengeResources
const isDesignChallenge = challenge.trackId === DES_TRACK_ID
const isChallengeType = challenge.typeId === CHALLENGE_TYPE_ID
+ const isMarathonMatch = challenge.typeId === MARATHON_TYPE_ID
+ const isFunChallenge = challenge.funChallenge === true
const showRoundType = isDesignChallenge && isChallengeType
const showCheckpointPrizes = challenge.timelineTemplateId === MULTI_ROUND_CHALLENGE_TEMPLATE_ID
const useDashboardData = _.find(challenge.metadata, { name: 'show_data_dashboard' })
@@ -1932,6 +1964,7 @@ class ChallengeEditor extends Component {
{/* remove terms field and use default term */}
{false && ()}
+
@@ -2025,14 +2058,21 @@ class ChallengeEditor extends Component {
token={token}
removeAttachment={removeAttachment}
/>}
-
- {
- showCheckpointPrizes && (
-
- )
- }
-
-
+ {isMarathonMatch && (
+
+ )}
+ {!isFunChallenge && (
+ <>
+
+ {
+ showCheckpointPrizes && (
+
+ )
+ }
+
+
+ >
+ )}
{errorContainer}
{actionButtons}
diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js
index 2147fdc9..5cb3a294 100644
--- a/src/containers/ChallengeEditor/index.js
+++ b/src/containers/ChallengeEditor/index.js
@@ -129,14 +129,28 @@ class ChallengeEditor extends Component {
componentWillReceiveProps (nextProps) {
const { match } = this.props
- const { match: newMatch, loadChallengeDetails, loadResources, loadSubmissions, projectDetail, loggedInUser } = nextProps
+ const {
+ match: newMatch,
+ loadChallengeDetails,
+ loadResources,
+ loadSubmissions,
+ projectDetail,
+ loggedInUser,
+ submissionsPerPage
+ } = nextProps
const projectId = _.get(newMatch.params, 'projectId', null)
const challengeId = _.get(newMatch.params, 'challengeId', null)
if (
_.get(match.params, 'projectId', null) !== projectId ||
_.get(match.params, 'challengeId', null) !== challengeId
) {
- this.fetchChallengeDetails(newMatch, loadChallengeDetails, loadResources, loadSubmissions)
+ this.fetchChallengeDetails(
+ newMatch,
+ loadChallengeDetails,
+ loadResources,
+ loadSubmissions,
+ submissionsPerPage
+ )
} else {
this.setState({ challengeDetails: nextProps.challengeDetails })
}
diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js
index f3edb55a..871f078b 100644
--- a/src/containers/Challenges/index.js
+++ b/src/containers/Challenges/index.js
@@ -150,14 +150,15 @@ class Challenges extends Component {
fetchNextProjects
} = this.props
const { challengeTypes = [] } = metadata
+ const isActiveProjectLoaded =
+ reduxProjectInfo && `${reduxProjectInfo.id}` === `${activeProjectId}`
+
return (
{(dashboard || activeProjectId !== -1 || selfService) && (
}
*/
-export async function fetchSubmissions (challengeId, pageObj) {
- const { page, perPage } = pageObj
+export async function fetchSubmissions (challengeId, pageObj = {}) {
+ const page = _.get(pageObj, 'page', 1)
+ const perPage = _.get(pageObj, 'perPage', 10)
const response = await axiosInstance.get(`${SUBMISSIONS_API_URL}?challengeId=${challengeId}&perPage=${perPage}&page=${page}`)
const responseData = _.get(response, 'data', {})
const meta = _.get(responseData, 'meta', {})