diff --git a/package-lock.json b/package-lock.json index da18caae..b524184a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14995,6 +14995,12 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -15873,6 +15879,14 @@ "faye-websocket": "^0.10.0", "uuid": "^3.4.0", "websocket-driver": "0.6.5" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "sockjs-client": { @@ -17104,10 +17118,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.1.1", @@ -17755,6 +17768,14 @@ "requires": { "ansi-colors": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "webpack-merge": { diff --git a/package.json b/package.json index 61cd6967..a95fce45 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,8 @@ "redux-logger": "^3.0.6", "redux-promise-middleware": "^6.1.2", "redux-thunk": "^2.3.0", - "tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.4" + "tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.4", + "uuid": "^8.3.2" }, "browserslist": [ "last 1 version", diff --git a/src/assets/images/customer-logos.svg b/src/assets/images/customer-logos.svg new file mode 100644 index 00000000..0719e61e --- /dev/null +++ b/src/assets/images/customer-logos.svg @@ -0,0 +1,139 @@ + + + Homepage-Logos + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/CustomerScroll/index.jsx b/src/components/CustomerScroll/index.jsx new file mode 100644 index 00000000..bc89cecf --- /dev/null +++ b/src/components/CustomerScroll/index.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import "./styles.module.scss"; + +function CustomerScroll() { + return ( +
+
Trusted By
+
+
+ ); +} + +export default CustomerScroll; diff --git a/src/components/CustomerScroll/styles.module.scss b/src/components/CustomerScroll/styles.module.scss new file mode 100644 index 00000000..60cfdf55 --- /dev/null +++ b/src/components/CustomerScroll/styles.module.scss @@ -0,0 +1,56 @@ +@import "styles/include"; + +.title { + @include font-barlow; + font-weight: 600; + font-size: 22px; + color: #7f7f7f; + text-align: center; + text-transform: uppercase; + margin-bottom: 30px; +} + +@keyframes scroll { + from {background-position: 0 0;} + to {background-position: -7701px 0;} +} + +.scrolling-logos { + background-image: url("../../assets/images/customer-logos.svg"); + background-size: cover; + height: 60px; + width: 100%; + animation: scroll 300s linear infinite; + position: relative; + + &:before { + background: linear-gradient(to right, #F4F5F6 0%, rgba(255, 255, 255, 0) 100%); + content: ''; + height: 60px; + left: 0; + position: absolute; + top: 0; + width: 60px; + z-index: 2; + } + &:after { + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #F4F5F6 100%); + content: ''; + height: 60px; + position: absolute; + right: 0; + top: 0; + width: 60px; + z-index: 2; + } +} + +@media only screen and (max-height: 859px) { + .scrolling-logos { + height: 30px; + } + .title { + font-size: 16px; + margin-bottom: 15px; + } +} diff --git a/src/constants/index.js b/src/constants/index.js index f1c5b67c..235a67e7 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -111,6 +111,7 @@ export const CANDIDATE_STATUS = { TOPCODER_REJECTED: "topcoder-rejected", JOB_CLOSED: "job-closed", OFFERED: "offered", + WITHDRAWN: "withdrawn", }; /** @@ -133,6 +134,7 @@ export const CANDIDATE_STATUS_FILTERS = [ title: "Candidates to Review", noCandidateMessage: "No Candidates To Review", statuses: [CANDIDATE_STATUS.OPEN], + urlParam: "to-review", }, { key: CANDIDATE_STATUS_FILTER_KEY.INTERESTED, @@ -140,6 +142,7 @@ export const CANDIDATE_STATUS_FILTERS = [ title: "Interviews", noCandidateMessage: "No Interviews", statuses: [CANDIDATE_STATUS.INTERVIEW], + urlParam: "interviews", }, { key: CANDIDATE_STATUS_FILTER_KEY.SELECTED, @@ -147,6 +150,7 @@ export const CANDIDATE_STATUS_FILTERS = [ title: "Selected", noCandidateMessage: "No Selected Candidates", statuses: [CANDIDATE_STATUS.SELECTED, CANDIDATE_STATUS.OFFERED], + urlParam: "selected", }, { key: CANDIDATE_STATUS_FILTER_KEY.NOT_INTERESTED, @@ -159,7 +163,9 @@ export const CANDIDATE_STATUS_FILTERS = [ CANDIDATE_STATUS.REJECTED_OTHER, CANDIDATE_STATUS.TOPCODER_REJECTED, CANDIDATE_STATUS.JOB_CLOSED, + CANDIDATE_STATUS.WITHDRAWN, ], + urlParam: "declined", }, ]; @@ -264,6 +270,7 @@ export const ACTION_TYPE = { */ ADD_MATCHING_ROLE: "ADD_MATCHING_ROLE", DELETE_MATCHING_ROLE: "DELETE_MATCHING_ROLE", + EDIT_MATCHING_ROLE: "EDIT_MATCHING_ROLE", }; /** @@ -366,3 +373,8 @@ export const MAX_ALLOWED_INTERVIEWS = 3; * Custom role names to remove from RoleList component */ export const CUSTOM_ROLE_NAMES = ["custom", "niche"]; + +/** + * Minimal Resource Booking duration (weeks) + */ +export const MIN_DURATION = 4; diff --git a/src/root.component.jsx b/src/root.component.jsx index bdd3b79b..2f5d06a9 100644 --- a/src/root.component.jsx +++ b/src/root.component.jsx @@ -10,6 +10,7 @@ import JobDetails from "./routes/JobDetails"; import JobForm from "./routes/JobForm"; import TeamAccess from "./routes/TeamAccess"; import CreateNewTeam from "./routes/CreateNewTeam"; +import CreateTeamLanding from "./routes/CreateNewTeam/pages/CreateTeamLanding"; import InputSkills from "./routes/CreateNewTeam/pages/InputSkills"; import InputJobDescription from "./routes/CreateNewTeam/pages/InputJobDescription"; import SelectRole from "./routes/CreateNewTeam/pages/SelectRole"; @@ -25,18 +26,20 @@ export default function Root() { - - + - - - + + + + + + {/* Global config for Toastr popups */} diff --git a/src/routes/CreateNewTeam/actions/index.js b/src/routes/CreateNewTeam/actions/index.js index 0a224017..62d6e04a 100644 --- a/src/routes/CreateNewTeam/actions/index.js +++ b/src/routes/CreateNewTeam/actions/index.js @@ -36,6 +36,11 @@ const deleteMatchingRole = () => ({ type: ACTION_TYPE.DELETE_MATCHING_ROLE, }); +const editMatchingRole = (role) => ({ + type: ACTION_TYPE.EDIT_MATCHING_ROLE, + payload: role, +}); + export const clearSearchedRoles = () => (dispatch, getState) => { dispatch(clearRoles()); updateLocalStorage(getState().searchedRoles); @@ -51,6 +56,11 @@ export const addRoleSearchId = (id) => (dispatch, getState) => { updateLocalStorage(getState().searchedRoles); }; +export const editRoleAction = (role) => (dispatch, getState) => { + dispatch(editMatchingRole(role)); + updateLocalStorage(getState().searchedRoles); +}; + export const deleteSearchedRole = (id) => (dispatch, getState) => { dispatch(deleteRole(id)); updateLocalStorage(getState().searchedRoles); diff --git a/src/routes/CreateNewTeam/components/EditRoleForm/index.jsx b/src/routes/CreateNewTeam/components/EditRoleForm/index.jsx new file mode 100644 index 00000000..e816f5aa --- /dev/null +++ b/src/routes/CreateNewTeam/components/EditRoleForm/index.jsx @@ -0,0 +1,160 @@ +/** + * Edit Role Form + * form for eidting details about current role + */ +import React, { useEffect, useState } from "react"; +import PT from "prop-types"; +import { Form, Field, useField } from "react-final-form"; +import FormField from "components/FormField"; +import BaseCreateModal from "../BaseCreateModal"; +import Button from "components/Button"; +import MonthPicker from "components/MonthPicker"; +import InformationTooltip from "components/InformationTooltip"; +import IconCrossLight from "../../../../assets/images/icon-cross-light.svg"; +import "./styles.module.scss"; +import NumberInput from "components/NumberInput"; +import { + validator, + validateExists, + validateMin, + composeValidators, +} from "./utils/validator"; +import { MIN_DURATION } from "constants"; + +const Error = ({ name }) => { + const { + meta: { dirty, error }, + } = useField(name, { subscription: { dirty: true, error: true } }); + return dirty && error ? {error} : null; +}; + +function EditRoleForm({ onChange, role }) { + const [startMonthVisible, setStartMonthVisible] = useState(false); + const onRoleChange = (state) => { + if (state.hasValidationErrors) { + onChange(false); + }else { + onChange(true, state.values); + } + }; + + return ( +
{}} + mutators={{ + clearField: ([fieldName], state, { changeValue }) => { + changeValue(state, fieldName, () => undefined); + }, + }} + validate={validator} + > + {({ + form: { + mutators: { clearField }, + getState, + }, + }) => { + return ( +
+ + + + + + + + + + + +
# of resourcesDuration (weeks)Start month
+ + {({ input, meta }) => ( + { + input.onChange(v); + onRoleChange(getState()); + }} + onBlur={input.onBlur} + onFocus={input.onFocus} + min="1" + error={meta.touched && meta.error} + /> + )} + + + + + {({ input, meta }) => ( + { + input.onChange(v); + onRoleChange(getState()); + }} + onBlur={input.onBlur} + onFocus={input.onFocus} + min="4" + error={meta.touched && meta.error} + /> + )} + + + + {startMonthVisible ? ( + <> + + {(props) => ( + { + props.input.onChange(v); + onRoleChange(getState()); + }} + onBlur={props.input.onBlur} + onFocus={props.input.onFocus} + /> + )} + + + + ) : ( +
+ + +
+ )} +
+
+ ); + }} +
+ ); +} + +EditRoleForm.propTypes = { + submitForm: PT.func, + role: PT.object, +}; + +export default EditRoleForm; diff --git a/src/routes/CreateNewTeam/components/EditRoleForm/styles.module.scss b/src/routes/CreateNewTeam/components/EditRoleForm/styles.module.scss new file mode 100644 index 00000000..d9f635f9 --- /dev/null +++ b/src/routes/CreateNewTeam/components/EditRoleForm/styles.module.scss @@ -0,0 +1,108 @@ +@import "styles/include"; + +.toggle-button { + @include font-roboto; + outline: none; + border: none; + background: none; + font-size: 12px; + font-weight: 500; + color: #137D60; + padding: 1px 6px 0 6px; + + &.toggle-description { + margin-top: 12px; + > span { + font-size: 18px; + vertical-align: middle; + } + } +} + +.table { + margin-top: 40px; + width: 100%; + th { + @include font-roboto; + font-size: 12px; + color: #555; + padding-bottom: 7px; + border-bottom: 1px solid #d4d4d4; + + &.bold { + font-weight: 700; + } + } + + .role-row { + td { + > div { + margin-left: auto; + margin-right: auto; + } + + padding: 18px 18px 18px 0; + width: 30%; + vertical-align: top; + @include font-barlow; + font-weight: 600; + font-size: 16px; + color: #2a2a2a; + border-bottom: 1px solid #e9e9e9; + + &:last-child { + padding-right: 0; + } + + input { + @include font-roboto; + font-size: 14px; + line-height: normal; + height: 34px; + &[type="number"] { + width: 98px; + } + } + } + } +} + +.flex-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + width: 118px; + margin-top: 13px; +} + +.error { + font-size: 14px; + font-weight: 400; + color: red; + display: block; +} + +.delete-role { + border: none; + background: none; + + margin-top: 13px; + + &:hover { + g { + stroke: red; + } + } +} + +.table-container { + > button { + margin: 0 auto; + } + padding: 0 30px; + width: 80%; + textarea { + height: 95px; + } +} diff --git a/src/routes/CreateNewTeam/components/EditRoleForm/utils/validator.js b/src/routes/CreateNewTeam/components/EditRoleForm/utils/validator.js new file mode 100644 index 00000000..704f1099 --- /dev/null +++ b/src/routes/CreateNewTeam/components/EditRoleForm/utils/validator.js @@ -0,0 +1,56 @@ +const composeValidators = (...validators) => (value) => + validators.reduce((error, validator) => error || validator(value), undefined); + +const validateMin = (min, message) => (value) => + isNaN(value) || value >= min ? undefined : message; + +const validateName = (name) => { + if (!name || name.trim().length === 0) { + return "Please enter a team name."; + } + return undefined; +}; + +const validateNumber = (number) => { + const converted = Number(number); + + if ( + Number.isNaN(converted) || + converted !== Math.floor(converted) || + converted < 1 + ) { + return "Please enter a positive integer"; + } + return undefined; +}; + +const validateMonth = (monthString) => { + const then = new Date(monthString); + const now = new Date(); + const thenYear = then.getFullYear(); + const nowYear = now.getFullYear(); + const thenMonth = then.getMonth(); + const nowMonth = now.getMonth(); + + if (thenYear < nowYear || (thenYear === nowYear && thenMonth < nowMonth)) { + return "Start month may not be before current month"; + } + return undefined; +}; + +const validator = (role) => { + const roleErrors = {}; + roleErrors.numberOfResources = validateNumber(role.numberOfResources); + roleErrors.durationWeeks = validateNumber(role.durationWeeks); + if (role.startMonth) { + roleErrors.startMonth = validateMonth(role.startMonth); + } + + return roleErrors; +}; + +const validateExists = (value) => { + return value === undefined ? "Please enter a positive integer" : undefined; +}; + +export { validator, validateExists, validateMin, composeValidators }; diff --git a/src/routes/CreateNewTeam/components/InputContainer/index.jsx b/src/routes/CreateNewTeam/components/InputContainer/index.jsx index cebc1551..f87a671c 100644 --- a/src/routes/CreateNewTeam/components/InputContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/InputContainer/index.jsx @@ -8,16 +8,16 @@ import React from "react"; import PT from "prop-types"; import AddedRolesAccordion from "../AddedRolesAccordion"; -import Completeness from "../Completeness"; +import Progress from "../Progress"; import "./styles.module.scss"; function InputContainer({ stages, - isCompletenessDisabled, + isProgressDisabled, toRender, search, onClick, - completenessStyle, + progressStyle, addedRoles, }) { return ( @@ -25,10 +25,10 @@ function InputContainer({ {toRender(search)}
-
-

No Matching Profiles

+

Additional Evaluation Needed

@@ -53,8 +53,11 @@ function NoMatchingProfilesResultCard({ role }) { : "Custom Role"}

- We will be looking internally for members matching your requirements - and be back at them in about 2 weeks. + We did not get a perfect match to your requirements on the first pass, + but we are confident they are out there. We'd like to dig a little + deeper into our community to find someone who can fit your needs. This + may take up to two weeks. Please continue to submit your request, and + a Topcoder representative will reach out to you soon with next steps.

{role.rates && role.name ? (
diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss index b39ab76b..d60db1e5 100644 --- a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss @@ -56,7 +56,7 @@ @include font-roboto; font-size: 14px; line-height: 22px; - width: 357px; + width: 80%; text-align: center; } .niche-rate-box { diff --git a/src/routes/CreateNewTeam/components/Completeness/index.jsx b/src/routes/CreateNewTeam/components/Progress/index.jsx similarity index 82% rename from src/routes/CreateNewTeam/components/Completeness/index.jsx rename to src/routes/CreateNewTeam/components/Progress/index.jsx index df29ba1d..06c1cebe 100644 --- a/src/routes/CreateNewTeam/components/Completeness/index.jsx +++ b/src/routes/CreateNewTeam/components/Progress/index.jsx @@ -1,6 +1,6 @@ /** - * Completeness Sidebar - * Shows level of completeness through skill + * Progress Sidebar + * Shows level of progress through skill * input process and contains a button for * searching for users or submitting the job. */ @@ -8,13 +8,13 @@ import Button from "components/Button"; import React from "react"; import cn from "classnames"; import PT from "prop-types"; -import CompleteProgress from "../CompleteProgress"; +import ProgressBar from "../ProgressBar"; import "./styles.module.scss"; import IconMultipleActionsCheck from "../../../../assets/images/icon-multiple-actions-check-2.svg"; import IconListQuill from "../../../../assets/images/icon-list-quill.svg"; import IconOfficeFileText from "../../../../assets/images/icon-office-file-text.svg"; -function Completeness({ +function Progress({ extraStyleName, isDisabled, onClick, @@ -23,7 +23,7 @@ function Completeness({ percentage, }) { - let backgroundIcon + let backgroundIcon if (extraStyleName === "input-skills") { backgroundIcon= } else if (extraStyleName === "input-job-description") { @@ -33,8 +33,8 @@ function Completeness({ } return ( -
- +
+
    {stages.map((stage) => (
  • 100) { @@ -17,7 +17,7 @@ function CompleteProgress({ percentDone }) { return (
    -

    Completeness

    +

    Progress

    {percentDone}%
    @@ -30,8 +30,8 @@ function CompleteProgress({ percentDone }) { ); } -CompleteProgress.propTypes = { +ProgressBar.propTypes = { percentDone: PT.number, }; -export default CompleteProgress; +export default ProgressBar; diff --git a/src/routes/CreateNewTeam/components/CompleteProgress/styles.module.scss b/src/routes/CreateNewTeam/components/ProgressBar/styles.module.scss similarity index 100% rename from src/routes/CreateNewTeam/components/CompleteProgress/styles.module.scss rename to src/routes/CreateNewTeam/components/ProgressBar/styles.module.scss diff --git a/src/routes/CreateNewTeam/components/ResultCard/index.jsx b/src/routes/CreateNewTeam/components/ResultCard/index.jsx index 07f21c13..8aa4fa42 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/ResultCard/index.jsx @@ -12,6 +12,7 @@ import IconEarthCheck from "../../../../assets/images/icon-earth-check.svg"; import IconMultipleUsers from "../../../../assets/images/icon-multiple-users.svg"; import IconMultipleActionsCheck from "../../../../assets/images/icon-multiple-actions-check-2.svg"; import IconTeamMeetingChat from "../../../../assets/images/icon-team-meeting-chat.svg"; +import EditRoleForm from "../EditRoleForm"; import Curve from "../../../../assets/images/curve.svg"; import CircularProgressBar from "../CircularProgressBar"; import Button from "components/Button"; @@ -26,7 +27,7 @@ function formatPercent(value) { return `${Math.round(value * 100)}%`; } -function ResultCard({ role }) { +function ResultCard({ role, currentRole, onSaveEditRole }) { const { numberOfMembersAvailable, isExternalMember, @@ -78,10 +79,10 @@ function ResultCard({ role }) {
    {showRates && !isExternalMember && ( -
    +
    {userHandle && (

    - Hi {userHandle}, we have special rates for you as a Xeno User! + Hi {userHandle}, we have special rates for you as a Wipro User!

    )}
    @@ -97,8 +98,15 @@ function ResultCard({ role }) {

    /Week

    -
    -

    In-Country Rate

    +
    +

    Global Niche Rate

    +
    +

    {formatRate(rates.niche)}

    +

    /Week

    +
    +
    +
    +

    Offshore Niche Rate

    {formatRate(rates.inCountry)}

    /Week

    @@ -124,8 +132,15 @@ function ResultCard({ role }) {

    /Week

    -
    -

    In-Country Rate

    +
    +

    Global Niche Rate

    +
    +

    {formatRate(rates.rate30Niche)}

    +

    /Week

    +
    +
    +
    +

    Offshore Niche Rate

    {formatRate(rates.rate30InCountry)}

    /Week

    @@ -151,8 +166,16 @@ function ResultCard({ role }) {

    /Week

    -
    -

    In-Country Rate

    + +
    +

    Global Niche Rate

    +
    +

    {formatRate(rates.rate20Niche)}

    +

    /Week

    +
    +
    +
    +

    Offshore Niche Rate

    {formatRate(rates.rate20InCountry)}

    /Week

    @@ -245,20 +268,9 @@ function ResultCard({ role }) {

    Members matched

    -
    -

    - 60% of members are available 20 hours / week (part - time) -

    -

    - 20% of members are available 30 hours / week (part - time) -

    -

    - 10% of members are available 40 hours / week (full - time) -

    -
    + {currentRole && ( + + )}
    )}
    @@ -267,6 +279,8 @@ function ResultCard({ role }) { ResultCard.propTypes = { role: PT.object, + currentRole: PT.object, + onSaveEditRole: PT.func, }; export default ResultCard; diff --git a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss index 6babd0a1..150884c0 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss @@ -138,7 +138,7 @@ padding-bottom: 50px; } -.xeno-rates { +.wipro-rates { display: flex; flex-direction: column; padding: 0 25px 50px 52px; @@ -180,7 +180,8 @@ } } .global, - .in-country, + .global-niche, + .offshore-niche, .offshore { display: flex; flex-direction: column; @@ -225,7 +226,12 @@ .global::before { background-color: #c99014; } - .in-country::before { + + .global-niche::before { + background-color: #0ab88a; + } + + .offshore-niche::before { background-color: #716d67; } .offshore::before { diff --git a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx index bfefd418..cd55604d 100644 --- a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx @@ -14,10 +14,13 @@ import InputContainer from "../InputContainer"; import SearchContainer from "../SearchContainer"; import SubmitContainer from "../SubmitContainer"; +const SEARCHINGTIME = 1600; + function SearchAndSubmit(props) { const { stages, setStages, searchObject, onClick, page } = props; const [searchState, setSearchState] = useState(null); + const [isNewRole, setIsNewRole] = useState(false); const { matchingRole } = useSelector((state) => state.searchedRoles); @@ -48,12 +51,21 @@ function SearchAndSubmit(props) { if (previousSearchId) { searchObjectCopy.previousRoleSearchRequestId = previousSearchId; } + const searchingBegin = Date.now(); searchRoles(searchObjectCopy) .then((res) => { const name = _.get(res, "data.name"); const searchId = _.get(res, "data.roleSearchRequestId"); if (name && !isCustomRole({ name })) { - dispatch(addSearchedRole({ searchId, name })); + dispatch( + addSearchedRole({ + searchId, + name, + numberOfResources: 1, + durationWeeks: 4, + }) + ); + setIsNewRole(true); } else if (searchId) { dispatch(addRoleSearchId(searchId)); } @@ -63,8 +75,13 @@ function SearchAndSubmit(props) { console.error(err); }) .finally(() => { - setCurrentStage(2, stages, setStages); - setSearchState("done"); + _.delay( + () => { + setCurrentStage(2, stages, setStages); + setSearchState("done"); + }, + Date.now() - searchingBegin > SEARCHINGTIME ? 0 : 1500 + ); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, previousSearchId, searchObject]); @@ -80,9 +97,11 @@ function SearchAndSubmit(props) { /> { setSearchState("state2"); - }, 800); - }, 800); + }, 500); + }, 500); return () => { clearTimeout(timer1); diff --git a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx index 33f1a580..9b177e0b 100644 --- a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx @@ -5,10 +5,13 @@ * search pages. Contains logic and supporting * components for searching for roles. */ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useState, useMemo, useEffect } from "react"; import PT from "prop-types"; +import _ from "lodash"; +import { useDispatch } from "react-redux"; +import { editRoleAction } from "../../actions"; import AddedRolesAccordion from "../AddedRolesAccordion"; -import Completeness from "../Completeness"; +import Progress from "../Progress"; import SearchCard from "../SearchCard"; import ResultCard from "../ResultCard"; import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; @@ -17,14 +20,39 @@ import AddAnotherModal from "../AddAnotherModal"; import "./styles.module.scss"; function SearchContainer({ + isNewRole, stages, - completenessStyle, + progressStyle, navigate, addedRoles, searchState, + previousSearchId, matchingRole, }) { const [addAnotherOpen, setAddAnotherOpen] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [buttonClickable, setButtonClickable] = useState(true); + + const dispatch = useDispatch(); + const currentRole = useMemo(() => { + return _.find(addedRoles, { searchId: previousSearchId }); + }, [addedRoles, previousSearchId]); + + useEffect(() => { + if (isNewRole) { + setShowEditModal(true); + } + }, [isNewRole]); + + const onSaveEditRole = useCallback( + (isValid, role) => { + setButtonClickable(isValid) + if (isValid) { + dispatch(editRoleAction({ ...role, searchId: previousSearchId })); + } + }, + [addedRoles, previousSearchId] + ); const onSubmit = useCallback(() => { setAddAnotherOpen(false); @@ -37,7 +65,14 @@ function SearchContainer({ const renderLeftSide = () => { if (searchState === "searching") return ; - if (!isCustomRole(matchingRole)) return ; + if (!isCustomRole(matchingRole)) + return ( + + ); return ; }; @@ -52,14 +87,15 @@ function SearchContainer({ {renderLeftSide()}
    - setAddAnotherOpen(true)} - extraStyleName={completenessStyle} - buttonLabel="Submit Request" + extraStyleName={progressStyle} + buttonLabel="Continue" stages={stages} percentage={getPercentage()} /> @@ -76,8 +112,10 @@ function SearchContainer({ } SearchContainer.propTypes = { + isNewRole: PT.bool, stages: PT.array, - completenessStyle: PT.string, + progressStyle: PT.string, + previousSearchId: PT.string, navigate: PT.func, addedRoles: PT.array, searchState: PT.string, diff --git a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx index b7bfdc40..5a523cfd 100644 --- a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx @@ -17,13 +17,13 @@ import { toastr } from "react-redux-toastr"; import { navigate } from "@reach/router"; import ResultCard from "../ResultCard"; import AddedRolesAccordion from "../AddedRolesAccordion"; -import Completeness from "../Completeness"; +import Progress from "../Progress"; import AddAnotherModal from "../AddAnotherModal"; import TeamDetailsModal from "../TeamDetailsModal"; import ConfirmationModal from "../ConfirmationModal"; import withAuthentication from "../../../../hoc/withAuthentication"; import "./styles.module.scss"; -import { isCustomRole, setCurrentStage } from "utils/helpers"; +import { isCustomRole, isUuid, setCurrentStage } from "utils/helpers"; import { clearSearchedRoles } from "../../actions"; import { postTeamRequest } from "services/teams"; import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; @@ -31,7 +31,7 @@ import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; function SubmitContainer({ stages, setStages, - completenessStyle, + progressStyle, matchingRole, addedRoles, }) { @@ -65,11 +65,15 @@ function SubmitContainer({ }; const assembleTeam = (formData) => { - const teamObject = _.pick(formData, ["teamName", "teamDescription"]); + const teamObject = _.pick(formData, [ + "teamName", + "teamDescription", + "refCode", + ]); const positions = []; for (let key of Object.keys(formData)) { - if (key === "teamName" || key === "teamDescription") { + if (!isUuid(key)) { continue; } const position = _.mapValues(formData[key], (val, key) => @@ -116,10 +120,10 @@ function SubmitContainer({ )}
    - setAddAnotherOpen(true)} - extraStyleName={completenessStyle} - buttonLabel="Submit Request" + extraStyleName={progressStyle} + buttonLabel="Continue" stages={stages} percentage="98" /> @@ -150,7 +154,7 @@ function SubmitContainer({ SubmitContainer.propTypes = { stages: PT.array, setStages: PT.func, - completenessStyle: PT.string, + progressStyle: PT.string, addedRoles: PT.array, matchingRole: PT.object, }; diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx index 30d89b08..fde71b11 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx @@ -11,6 +11,7 @@ import FormField from "components/FormField"; import BaseCreateModal from "../BaseCreateModal"; import { FORM_FIELD_TYPE } from "constants/"; import { formatPlural } from "utils/format"; +import { isUuid } from "utils/helpers"; import Button from "components/Button"; import MonthPicker from "components/MonthPicker"; import InformationTooltip from "components/InformationTooltip"; @@ -28,7 +29,7 @@ const Error = ({ name }) => { }; function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { - const [showDescription, setShowDescription] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); const [startMonthVisible, setStartMonthVisible] = useState({}); // Ensure role is removed from form state when it is removed from redux store @@ -37,7 +38,7 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { useEffect(() => { const values = getFormState().values; for (let fieldName of Object.keys(values)) { - if (fieldName === "teamName" || fieldName === "teamDescription") { + if (!isUuid(fieldName)) { continue; } if (addedRoles.findIndex((role) => role.searchId === fieldName) === -1) { @@ -49,8 +50,8 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { const dispatch = useDispatch(); - const toggleDescription = () => { - setShowDescription((prevState) => !prevState); + const toggleAdvanced = () => { + setShowAdvanced((prevState) => !prevState); }; return ( @@ -78,7 +79,7 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { open={open} onClose={onClose} title="Team Details" - subtitle="Please provide your team details before submitting a request." + subtitle="Please provide a name for your Team. This could be the name of the project they will work on, the name of the team they are joining, or whatever else will make this talent request meaningful for you." buttons={ @@ -133,100 +146,108 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { - {addedRoles.map(({ searchId: id, name }) => ( - - - + + + + - - - - - ))} + + + + ) + )}
    Start month
    {name} - - {({ input, meta }) => ( - + {addedRoles.map( + ({ + searchId: id, + name, + numberOfResources, + durationWeeks, + startMonth, + }) => ( +
    {name} + + {({ input, meta }) => ( + + )} + + + + + {({ input, meta }) => ( + + )} + + + + {startMonth || startMonthVisible[id] ? ( + <> + + {(props) => ( + + )} + + + + ) : ( +
    + + +
    )} - - -
    - - {({ input, meta }) => ( - - )} - - - - {startMonthVisible[id] ? ( - <> - - {(props) => ( - - )} - - - - ) : ( -
    - - -
    - )} -
    - -
    + +
    diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss index 96193ed6..75395759 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss @@ -10,7 +10,7 @@ color: #137D60; padding: 1px 6px 0 6px; - &.toggle-description { + &.toggle-advanced { margin-top: 12px; > span { font-size: 18px; diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js b/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js index 9e1a9caf..059605e8 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js @@ -1,3 +1,6 @@ +import { isUuid } from "utils/helpers"; +import { MIN_DURATION } from "constants"; + const validateName = (name) => { if (!name || name.trim().length === 0) { return "Please enter a team name."; @@ -18,6 +21,15 @@ const validateNumber = (number) => { return undefined; }; +const validateGreaterThan = (number, min) => { + const isInvalidNum = validateNumber(number); + if (isInvalidNum) return isInvalidNum; + + return number < min + ? `Talent as a Service engagements have a ${MIN_DURATION} week minimum commitment.` + : undefined; +}; + const validateMonth = (monthString) => { const then = new Date(monthString); const now = new Date(); @@ -35,7 +47,7 @@ const validateMonth = (monthString) => { const validateRole = (role) => { const roleErrors = {}; roleErrors.numberOfResources = validateNumber(role.numberOfResources); - roleErrors.durationWeeks = validateNumber(role.durationWeeks); + roleErrors.durationWeeks = validateGreaterThan(role.durationWeeks, MIN_DURATION); if (role.startMonth) { roleErrors.startMonth = validateMonth(role.startMonth); } @@ -49,7 +61,7 @@ const validator = (values) => { errors.teamName = validateName(values.teamName); for (const key of Object.keys(values)) { - if (key === "teamDescription" || key === "teamName") continue; + if (!isUuid(key)) continue; errors[key] = validateRole(values[key]); } diff --git a/src/routes/CreateNewTeam/components/progress/index.jsx b/src/routes/CreateNewTeam/components/progress/index.jsx new file mode 100644 index 00000000..06c1cebe --- /dev/null +++ b/src/routes/CreateNewTeam/components/progress/index.jsx @@ -0,0 +1,72 @@ +/** + * Progress Sidebar + * Shows level of progress through skill + * input process and contains a button for + * searching for users or submitting the job. + */ +import Button from "components/Button"; +import React from "react"; +import cn from "classnames"; +import PT from "prop-types"; +import ProgressBar from "../ProgressBar"; +import "./styles.module.scss"; +import IconMultipleActionsCheck from "../../../../assets/images/icon-multiple-actions-check-2.svg"; +import IconListQuill from "../../../../assets/images/icon-list-quill.svg"; +import IconOfficeFileText from "../../../../assets/images/icon-office-file-text.svg"; + +function Progress({ + extraStyleName, + isDisabled, + onClick, + buttonLabel, + stages, + percentage, +}) { + + let backgroundIcon + if (extraStyleName === "input-skills") { + backgroundIcon= + } else if (extraStyleName === "input-job-description") { + backgroundIcon= + } else { + backgroundIcon= + } + + return ( +
    + +
      + {stages.map((stage) => ( +
    • + {stage.name} +
    • + ))} +
    + + {backgroundIcon} +
    + ); +} + +Progress.propTypes = { + extraStyleName: PT.string, + isDisabled: PT.bool, + onClick: PT.func, + buttonLabel: PT.string, + currentStageIdx: PT.number, + stages: PT.arrayOf(PT.string), +}; + +export default Progress; diff --git a/src/routes/CreateNewTeam/components/progress/styles.module.scss b/src/routes/CreateNewTeam/components/progress/styles.module.scss new file mode 100644 index 00000000..529e15e6 --- /dev/null +++ b/src/routes/CreateNewTeam/components/progress/styles.module.scss @@ -0,0 +1,81 @@ +@import "styles/include"; + +.progress { + @include rounded-card; + overflow: hidden; + padding: 12px; + position: relative; + width: 250px; + color: #fff; + button { + border: none; + } +} + +.input-job-description { + background-image: linear-gradient(135deg, #2984BD 0%, #0AB88A 100%); +} + +.input-skills { + background-image: linear-gradient(221.5deg, #646CD0 0%, #9d41c9 100%); +} + +.role-selection { + background-image: linear-gradient(45deg, #8B41B0 0%, #EF476F 100%); +} + +.list { + margin-bottom: 55px; + & + button[disabled] { + background-color: #E9E9E9; + color: #FFF; + opacity: 1; + filter: none; + } +} + +.list-item { + margin-bottom: 14px; + font-size: 14px; + line-height: 16px; + font-weight: 400; + + &:before { + content: ""; + color: #fff; + border: 1px solid #ffffff; + border-radius: 100%; + width: 16px; + height: 16px; + margin-right: 10px; + display: block; + float: left; + } + + &.active { + font-weight: 500; + &:before { + background-color: #fff; + } + } + + &.done { + font-weight: 400; + color: rgba(255, 255, 255, 0.6); + &:before { + content: "✓"; + font-size: 9px; + line-height: 14px; + padding-left: 2px; + } + } +} + +.transparent-icon { + position: absolute; + right: -50px; + top: 85px; + opacity: 10%; + width: 144px; + height: 144px; +} diff --git a/src/routes/CreateNewTeam/index.jsx b/src/routes/CreateNewTeam/index.jsx index 7140a739..44c07e6c 100644 --- a/src/routes/CreateNewTeam/index.jsx +++ b/src/routes/CreateNewTeam/index.jsx @@ -1,59 +1,19 @@ /** * Create New Team * - * Landing page for creating new teams - * by selecting a role, inputting skills, - * or inputting a job description + * Container for Create New Team subroutes */ -import React, { useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { navigate } from "@reach/router"; -import _ from "lodash"; -import Page from "components/Page"; -import PageHeader from "components/PageHeader"; -import LandingBox from "./components/LandingBox"; -import { clearMatchingRole } from "./actions"; -import IconMultipleActionsCheck from "../../assets/images/icon-multiple-actions-check-2.svg"; -import IconListQuill from "../../assets/images/icon-list-quill.svg"; -import IconOfficeFileText from "../../assets/images/icon-office-file-text.svg"; +import React from "react"; +import CustomerScroll from "components/CustomerScroll"; import "./styles.module.scss"; -function CreateNewTeam() { - const dispatch = useDispatch(); - const goToRoute = (path) => { - dispatch(clearMatchingRole()); - navigate(path); - }; - - return ( - - Create New Team
    } /> -

    - Please select how you want to find members that match your requirements. -

    - } - backgroundImage="linear-gradient(101.95deg, #8B41B0 0%, #EF476F 100%)" - onClick={() => goToRoute("/taas/createnewteam/role")} - /> - } - backgroundImage="linear-gradient(221.5deg, #2C95D7 0%, #9D41C9 100%)" - onClick={() => goToRoute("/taas/createnewteam/skills")} - /> - } - backgroundImage="linear-gradient(135deg, #2984BD 0%, #0AB88A 100%)" - onClick={() => goToRoute("/taas/createnewteam/jd")} - /> - - ); -} +const CreateNewTeam = (props) => ( +
    + {props.children} +
    + +
    +
    +); export default CreateNewTeam; diff --git a/src/routes/CreateNewTeam/pages/CreateTeamLanding/index.jsx b/src/routes/CreateNewTeam/pages/CreateTeamLanding/index.jsx new file mode 100644 index 00000000..a00bf427 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/CreateTeamLanding/index.jsx @@ -0,0 +1,58 @@ +/** + * Create Team Landing + * + * Landing page for creating new teams + * by selecting a role, inputting skills, + * or inputting a job description + */ +import React from "react"; +import { useDispatch } from "react-redux"; +import { navigate } from "@reach/router"; +import Page from "components/Page"; +import PageHeader from "components/PageHeader"; +import LandingBox from "../../components/LandingBox"; +import { clearMatchingRole } from "../../actions"; +import IconMultipleActionsCheck from "../../../../assets/images/icon-multiple-actions-check-2.svg"; +import IconListQuill from "../../../../assets/images/icon-list-quill.svg"; +import IconOfficeFileText from "../../../../assets/images/icon-office-file-text.svg"; +import "./styles.module.scss"; + +function CreateNewTeam() { + const dispatch = useDispatch(); + const goToRoute = (path) => { + dispatch(clearMatchingRole()); + navigate(path); + }; + + return ( + + Create New Team
    } /> +

    + Please select how you want to find members that match your requirements. +

    + } + backgroundImage="linear-gradient(101.95deg, #8B41B0 0%, #EF476F 100%)" + onClick={() => goToRoute("/taas/createnewteam/role")} + /> + } + backgroundImage="linear-gradient(221.5deg, #2C95D7 0%, #9D41C9 100%)" + onClick={() => goToRoute("/taas/createnewteam/skills")} + /> + } + backgroundImage="linear-gradient(135deg, #2984BD 0%, #0AB88A 100%)" + onClick={() => goToRoute("/taas/createnewteam/jd")} + /> + + ); +} + +export default CreateNewTeam; diff --git a/src/routes/CreateNewTeam/pages/CreateTeamLanding/styles.module.scss b/src/routes/CreateNewTeam/pages/CreateTeamLanding/styles.module.scss new file mode 100644 index 00000000..e1903c90 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/CreateTeamLanding/styles.module.scss @@ -0,0 +1,3 @@ +.title { + font-weight: 500; +} \ No newline at end of file diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx index 6d095ca5..0597ba98 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx @@ -32,10 +32,10 @@ function SkillListPopup({ open, skills, isLoading, onClose, onContinueClick }) { open={open} onClose={onClose} headerIcon={} - title="Skills" + title="Identified Skills" subtitle={ skills.length - ? "These skills are found in your Job Description" + ? "Topcoder has identified the following skills referenced in your Job Description." : "No skills are found in your Job Description" } isLoading={isLoading} @@ -43,11 +43,11 @@ function SkillListPopup({ open, skills, isLoading, onClose, onContinueClick }) { maxWidth="460px" buttons={Buttons} > -
    +
      {_.map(skills, (s) => { - return
      {s.tag}
      ; + return
    • {s.tag}
    • ; })} -
    +
); } diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss index b0270326..ee0c59e2 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss @@ -1,48 +1,9 @@ @import "styles/include"; -.button-group { - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-end; - :first-child { - margin-right: 8px; - } -} - -.modal-body { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; +.list { + @include font-roboto; + font-size: 18px; + line-height: normal; + color: #2a2a2a; text-align: center; - margin-bottom: 80px; - - svg { - width: 48px; - height: 48px; - margin-bottom: 16px; - } - - h5 { - @include font-barlow-condensed; - font-size: 34px; - color: #1e94a3; - text-transform: uppercase; - font-weight: 500; - margin-bottom: 10px; - } - - p { - @include font-roboto; - font-size: 16px; - color: #555555; - line-height: 26px; - } -} - -.cross { - g { - stroke: #000; - } -} +} \ No newline at end of file diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx index 10e1809b..1a961fa5 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx @@ -56,8 +56,8 @@ function InputJobDescription() { 2000} - completenessStyle="input-job-description" + isProgressDisabled={jdString.length < 10 || jdString.length > 100000} + progressStyle="input-job-description" searchObject={searchObject} page="jd" onClick={onClick} @@ -67,6 +67,13 @@ function InputJobDescription() { title={
Input Job Description
} backTo="/taas/createnewteam" /> +

+ Input a Job Description for your opening and the Topcoder Platform + will identify the skills required to perform the job duties and find + the best matching freelancers for your job opening. After inputting + the Job Description click on the "Search" button to see the skills + identified. +

2000 - ? "Maximum of 2000 characters. Please reduce job description length." + jdString.length > 100000 + ? "Maximum of 100,000 characters. Please reduce job description length." : "" } /> diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss index 6b3f1a38..e24aa845 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss @@ -8,6 +8,13 @@ flex: 1; } +.subtitle { + position: relative; + font-size: 12px; + line-height: normal; + top: -20px; +} + .title { font-weight: 500; } diff --git a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/index.jsx b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/index.jsx index effefd34..dbccd509 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/index.jsx @@ -8,6 +8,7 @@ import React, { useCallback, useState } from "react"; import PT from "prop-types"; import SkillItem from "../SkillItem"; import ItemList from "../../../../components/ItemList"; +import "./styles.module.scss"; import { formatPlural } from "utils/format"; function SkillsList({ skills, selectedSkills, toggleSkill }) { @@ -38,6 +39,11 @@ function SkillsList({ skills, selectedSkills, toggleSkill }) { : null } > +

+ Please select one or more essential skills you require your talent to + have. Topcoder will match to profiles which contain most or all of these + skills. +

{filteredSkills.map(({ id, name }) => ( ( - Description + View Description & Skills
); diff --git a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx index 2158655b..057bb02e 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx +++ b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx @@ -46,10 +46,10 @@ function SelectRole() { ( <> { ...state, matchingRole: action.payload, }; + case ACTION_TYPE.DELETE_MATCHING_ROLE: return { ...state, matchingRole: null, }; + + case ACTION_TYPE.EDIT_MATCHING_ROLE: + const index = _.findIndex(state.addedRoles, { + searchId: action.payload.searchId, + }); + state.addedRoles[index] = _.extend( + {}, + state.addedRoles[index], + _.omit(action.payload, "searchId") + ); + return { + ...state, + addedRoles: [...state.addedRoles], + }; + case ACTION_TYPE.ADD_SEARCHED_ROLE: return { ...state, diff --git a/src/routes/CreateNewTeam/styles.module.scss b/src/routes/CreateNewTeam/styles.module.scss index 379eb6ff..ba56c3ed 100644 --- a/src/routes/CreateNewTeam/styles.module.scss +++ b/src/routes/CreateNewTeam/styles.module.scss @@ -1,3 +1,25 @@ -.title { - font-weight: 500; +.logos { + position: fixed; + bottom: 15px; + width: calc(100vw - 270px); + z-index: -1; +} + +@media only screen and (max-width: 1023px) { + .logos { + width: 100vw; + } +} + +@media only screen and (max-height: 859px) { + .logos { + position: relative; + width: 100vw; + } +} + +@media only screen and (max-height: 859px) and (min-width: 1024px) { + .logos { + left: -270px; + } } diff --git a/src/routes/JobForm/utils.js b/src/routes/JobForm/utils.js index 0ee34141..2b4c0fe5 100644 --- a/src/routes/JobForm/utils.js +++ b/src/routes/JobForm/utils.js @@ -11,6 +11,7 @@ import { RESOURCE_TYPE_OPTIONS, FORM_ROW_TYPE, FORM_FIELD_TYPE, + MIN_DURATION, } from "../../constants"; const EDIT_JOB_ROWS = [ @@ -25,6 +26,17 @@ const EDIT_JOB_ROWS = [ { type: FORM_ROW_TYPE.SINGLE, fields: ["status"] }, ]; +const validateDuration = (x, y, {duration}) => { + if (!duration) return undefined; + const converted = Number(duration); + + if (isNaN(converted) || converted < MIN_DURATION) { + return `Talent as a Service engagements have a ${MIN_DURATION} week minimum commitment.`; + } + + return undefined; +} + /** * return edit job configuration * @param {any} skillOptions skill options @@ -92,6 +104,7 @@ export const getEditJobConfig = ( placeholder: "Duration", disabled: onlyEnableStatus, step: 1, + customValidator: validateDuration, }, { label: "Resource Type", diff --git a/src/routes/PositionDetails/index.jsx b/src/routes/PositionDetails/index.jsx index b18df067..361f1dc4 100644 --- a/src/routes/PositionDetails/index.jsx +++ b/src/routes/PositionDetails/index.jsx @@ -5,6 +5,7 @@ */ import React, { useCallback, useEffect, useState } from "react"; import PT from "prop-types"; +import { navigate } from "@reach/router"; import Page from "components/Page"; import LoadingIndicator from "components/LoadingIndicator"; import PageHeader from "components/PageHeader"; @@ -22,7 +23,12 @@ const inReviewStatusFilter = _.find(CANDIDATE_STATUS_FILTERS, { key: CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW, }); -const PositionDetails = ({ teamId, positionId }) => { +const getKeyFromParam = (urlParam) => { + const filter = _.find(CANDIDATE_STATUS_FILTERS, { urlParam }); + return filter?.key || undefined; +} + +const PositionDetails = ({ teamId, positionId, candidateStatus }) => { // by default show "interested" tab const [candidateStatusFilterKey, setCandidateStatusFilterKey] = useState( CANDIDATE_STATUS_FILTER_KEY.INTERESTED @@ -34,22 +40,25 @@ const PositionDetails = ({ teamId, positionId }) => { const onCandidateStatusChange = useCallback( (statusFilter) => { - setCandidateStatusFilterKey(statusFilter.key); + navigate(`/taas/myteams/${teamId}/positions/${positionId}/candidates/${statusFilter.urlParam}`); }, - [setCandidateStatusFilterKey] + [teamId, positionId] ); // if there are some candidates to review, then show "To Review" tab by default useEffect(() => { - if ( - position && - _.filter(position.candidates, (candidate) => + if (position) { + const key = getKeyFromParam(candidateStatus); + if (key) { + setCandidateStatusFilterKey(key); + } else if (_.filter(position.candidates, (candidate) => inReviewStatusFilter.statuses.includes(candidate.status) - ).length > 0 - ) { - setCandidateStatusFilterKey(CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW); + ).length > 0 + ) { + setCandidateStatusFilterKey(CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW); + } } - }, [position]); + }, [position, candidateStatus]); return ( diff --git a/src/utils/helpers.js b/src/utils/helpers.js index c54cbe5f..60eea1d5 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -5,8 +5,17 @@ * If there are multiple methods which could be grouped into a separate file by their meaning they should be extracted from here to not make this file too big. */ import _ from "lodash"; +import { validate } from "uuid"; + import { CUSTOM_ROLE_NAMES } from "constants/"; +/** + * @param {String} string a possible uuid + * + * @returns {Boolean} true if uuid, false if not + */ +export const isUuid = validate; + /** * Delay code for some milliseconds using promise. *