diff --git a/src/constants/index.js b/src/constants/index.js index 34fd9c69..6eee3717 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -109,8 +109,8 @@ export const CANDIDATE_STATUS = { REJECTED_OTHER: "rejected - other", INTERVIEW: "interview", TOPCODER_REJECTED: "topcoder-rejected", - JOB_CLOSED:"job-closed", - OFFERED:"offered" + JOB_CLOSED: "job-closed", + OFFERED: "offered", }; /** @@ -146,7 +146,7 @@ export const CANDIDATE_STATUS_FILTERS = [ buttonText: "Selected", title: "Selected", noCandidateMessage: "No Selected Candidates", - statuses: [CANDIDATE_STATUS.SELECTED,CANDIDATE_STATUS.OFFERED], + statuses: [CANDIDATE_STATUS.SELECTED, CANDIDATE_STATUS.OFFERED], }, { key: CANDIDATE_STATUS_FILTER_KEY.NOT_INTERESTED, @@ -347,3 +347,8 @@ export const INTERVIEW_POPUP_MEDIA_URL = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; export const MAX_ALLOWED_INTERVIEWS = 3; + +/** + * Matching rate to show in CreateNewTeam ResultCard + */ +export const MATCHING_RATE = "80"; diff --git a/src/root.component.jsx b/src/root.component.jsx index 797270ff..97b8bdcb 100644 --- a/src/root.component.jsx +++ b/src/root.component.jsx @@ -35,8 +35,8 @@ export default function Root() { - - + + {/* Global config for Toastr popups */} diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/index.jsx b/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx similarity index 96% rename from src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/index.jsx rename to src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx index e09a64e3..2c1d9a6e 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/index.jsx +++ b/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx @@ -13,7 +13,7 @@ import "./styles.module.scss"; function AddedRolesAccordion({ addedRoles }) { const [isOpen, setIsOpen] = useState(false); - return ( + return addedRoles.length ? (
- ); + ) : null; } AddedRolesAccordion.propTypes = { diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/styles.module.scss b/src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss similarity index 100% rename from src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/styles.module.scss rename to src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss diff --git a/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx b/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx new file mode 100644 index 00000000..215277cb --- /dev/null +++ b/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx @@ -0,0 +1,92 @@ +import React from "react"; +import PT from "prop-types"; +import Modal from "react-responsive-modal"; +import IconCrossLight from "../../../../assets/images/icon-cross-light.svg"; +import CenteredSpinner from "components/CenteredSpinner"; +import "./styles.module.scss"; +import cn from "classnames"; + +const modalStyle = { + borderRadius: "8px", + padding: "72px 0 60px 0", + width: "100%", + margin: 0, + "overflow-x": "hidden", +}; + +const containerStyle = { + padding: "10px", +}; + +const closeButtonStyle = { + top: "31px", + right: "31px", +}; + +function BaseCreateModal({ + open, + onClose, + hideCloseIcon, + headerIcon, + title, + subtitle, + buttons, + isLoading, + loadingMessage, + maxWidth = "680px", + darkHeader, + children, +}) { + return ( + + } + styles={{ + modal: { ...modalStyle, maxWidth }, + modalContainer: containerStyle, + closeButton: closeButtonStyle, + }} + > +
+ {isLoading ? ( +
+ + {loadingMessage &&
{loadingMessage}
} +
+ ) : ( + <> +
+
{headerIcon}
+
{title}
+ {subtitle &&

{subtitle}

} +
+ {children} + + )} +
+
{buttons}
+
+ ); +} + +BaseCreateModal.propTypes = { + open: PT.bool, + onClose: PT.func, + hideCloseIcon: PT.bool, + headerIcon: PT.node, + title: PT.string, + subtitle: PT.string, + buttons: PT.node, + isLoading: PT.bool, + loadingMessage: PT.string, + maxWidth: PT.string, + darkHeader: PT.bool, + children: PT.node, +}; + +export default BaseCreateModal; diff --git a/src/routes/CreateNewTeam/components/BaseCreateModal/styles.module.scss b/src/routes/CreateNewTeam/components/BaseCreateModal/styles.module.scss new file mode 100644 index 00000000..086d9564 --- /dev/null +++ b/src/routes/CreateNewTeam/components/BaseCreateModal/styles.module.scss @@ -0,0 +1,65 @@ +@import "styles/include"; + +.button-group { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-end; + :first-child { + margin-right: 8px; + } +} + +.modal-body { + margin: 0 80px 40px 80px; +} + +.modal-header { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + text-align: center; + margin-bottom: 24px; + + .header-icon { + margin-bottom: 16px; + width: 42px; + height: 42px; + + img, + svg { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + 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; + } + + &.dark-header { + h5 { + color: #2a2a2a; + } + } +} + +.cross { + g { + stroke: #000; + } +} diff --git a/src/routes/CreateNewTeam/components/Completeness/styles.module.scss b/src/routes/CreateNewTeam/components/Completeness/styles.module.scss index 3227642c..6e12427f 100644 --- a/src/routes/CreateNewTeam/components/Completeness/styles.module.scss +++ b/src/routes/CreateNewTeam/components/Completeness/styles.module.scss @@ -16,7 +16,7 @@ } .input-skills { - background-image: linear-gradient(221.5deg, #2c95d7 0%, #9d41c9 100%); + background-image: linear-gradient(221.5deg, #646CD0 0%, #9d41c9 100%); } .role-selection { @@ -34,6 +34,7 @@ &:before { content: ""; + color: #fff; border: 1px solid #ffffff; border-radius: 100%; width: 16px; @@ -57,7 +58,7 @@ content: "✓"; font-size: 9px; line-height: 14px; - padding-left: 3px; + padding-left: 2px; } } } diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx index 0bde6659..492fd2b7 100644 --- a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx @@ -3,13 +3,13 @@ * Card that appears when there are no matching profiles after searching. */ import React from "react"; -import { navigate } from "@reach/router"; +import { Link } from "@reach/router"; import "./styles.module.scss"; import IconEarthX from "../../../../assets/images/icon-earth-x.svg"; import Curve from "../../../../assets/images/curve.svg"; import Button from "components/Button"; -function NoMatchingProfilesResultCard() { +function NoMatchingProfilesResultCard({ prevSearchId, addedRoles }) { return (
@@ -28,13 +28,14 @@ function NoMatchingProfilesResultCard() {

$1,200

/Week

- + +
); diff --git a/src/routes/CreateNewTeam/components/ResultCard/index.jsx b/src/routes/CreateNewTeam/components/ResultCard/index.jsx index 910162b4..ac88dcfe 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/ResultCard/index.jsx @@ -5,7 +5,7 @@ * about costs and number of matching candidates. */ import React, { useState, useEffect } from "react"; -import cn from "classnames"; +import PT from "prop-types"; import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app"; import "./styles.module.scss"; import IconEarthCheck from "../../../../assets/images/icon-earth-check.svg"; @@ -15,10 +15,21 @@ import IconTeamMeetingChat from "../../../../assets/images/icon-team-meeting-cha import Curve from "../../../../assets/images/curve.svg"; import CircularProgressBar from "../CircularProgressBar"; import Button from "components/Button"; +import { MATCHING_RATE } from "constants"; +import { formatMoney } from "utils/format"; -function ResultCard() { +function formatRate(value) { + if (!value) return "N/A"; + return formatMoney(value); +} + +function ResultCard({ role }) { + const { + numberOfMembersAvailable, + isExternalMember, + rates: [rates], + } = role; const [userHandle, setUserHandle] = useState("handle"); - const [showSpecialRates, setShowSpecialRates] = useState(false); const [showRates, setShowRates] = useState(false); useEffect(() => { @@ -29,17 +40,12 @@ function ResultCard() { return (
-
setShowSpecialRates(!showSpecialRates)} - styleName={cn("heading", { ["non-clickable"]: !showRates })} - > +

We have matching profiles

- We have qualified candidates who match 80% or more of your job - requirements. + We have qualified candidates who match {MATCHING_RATE}% or more of + your job requirements.

@@ -60,7 +66,7 @@ function ResultCard() { Rate Details
- {showRates && showSpecialRates && ( + {showRates && !isExternalMember && (

Hi {userHandle}, we have special rates for you as a Xeno User! @@ -72,23 +78,23 @@ function ResultCard() {

(40h / week)

-

Senior Member

+

Global Rate

-

$2,000

+

{formatRate(rates.global)}

/Week

-

Standard Member

+

In-Country Rate

-

$1,500

+

{formatRate(rates.inCountry)}

/Week

-

Junior Member

+

Offshore Rate

-

$1,000

+

{formatRate(rates.offShore)}

/Week

@@ -99,23 +105,23 @@ function ResultCard() {

(30h / week)

-

Senior Member

+

Global Rate

-

$1,800

+

{formatRate(rates.rate30Global)}

/Week

-

Standard Member

+

In-Country Rate

-

$1,300

+

{formatRate(rates.rate30InCountry)}

/Week

-

Junior Member

+

Offshore Rate

-

$800

+

{formatRate(rates.rate30OffShore)}

/Week

@@ -126,23 +132,23 @@ function ResultCard() {

(20h / week)

-

Senior Member

+

Global Rate

-

$1,600

+

{formatRate(rates.rate20Global)}

/Week

-

Standard Member

+

In-Country Rate

-

$1,100

+

{formatRate(rates.rate20InCountry)}

/Week

-

Junior Member

+

Offshore Rate

-

$600

+

{formatRate(rates.rate20OffShore)}

/Week

@@ -150,7 +156,7 @@ function ResultCard() { )} - {showRates && !showSpecialRates && ( + {showRates && isExternalMember && (
@@ -159,7 +165,7 @@ function ResultCard() {

(40h / week)

-
$1,800
+
{formatRate(rates.global)}

/Week

@@ -169,7 +175,7 @@ function ResultCard() {

(30h / week)

-
$1,250
+
{formatRate(rates.rate30Global)}

/Week

@@ -179,7 +185,7 @@ function ResultCard() {

(20h / week)

-
$800
+
{formatRate(rates.rate20Global)}

/Week

@@ -209,11 +215,11 @@ function ResultCard() {
-

80%

+

{MATCHING_RATE}%

Matching rate

} @@ -222,7 +228,7 @@ function ResultCard() {
-

300+

+

{numberOfMembersAvailable}+

Members matched

@@ -246,4 +252,8 @@ function ResultCard() { ); } +ResultCard.propTypes = { + role: PT.object, +}; + export default ResultCard; diff --git a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss index 1b4e7347..04744e66 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss @@ -7,10 +7,6 @@ margin-right: 30px; } -.non-clickable { - pointer-events: none; -} - .heading { display: flex; flex-direction: column; @@ -23,7 +19,6 @@ position: relative; text-align: center; border-radius: 8px 8px 0 0; - cursor: pointer; svg { margin-bottom: 8px; @@ -172,7 +167,9 @@ margin-left: 3px; } } - .senior, .standard, .junior { + .senior, + .standard, + .junior { display: flex; flex-direction: column; position: relative; @@ -190,7 +187,7 @@ font-weight: 700; letter-spacing: 1px; line-height: 16px; - color: #2A2A2A; + color: #2a2a2a; text-transform: uppercase; } .cost { @@ -201,7 +198,7 @@ font-size: 34px; font-weight: 500; line-height: 38px; - color: #2A2A2A; + color: #2a2a2a; } p { @include font-roboto; @@ -214,13 +211,13 @@ } } .senior::before { - background-color: #C99014; + background-color: #c99014; } .standard::before { - background-color: #716D67; + background-color: #716d67; } .junior::before { - background-color: #854E29; + background-color: #854e29; } } } @@ -298,7 +295,7 @@ @include font-barlow; font-size: 16px; text-transform: uppercase; - font-weight: 900; + font-weight: 700; line-height: 20px; } p { diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx index 17039e66..a10f2de7 100644 --- a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx @@ -2,101 +2,109 @@ * Role Details Modal * Display role details. */ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import PT from "prop-types"; -import Modal from "react-responsive-modal"; import Button from "components/Button"; -import IconCrossLight from "../../../../assets/images/icon-cross-light.svg"; import FallbackIcon from "../../../../assets/images/icon-role-fallback.svg"; import "./styles.module.scss"; -import CenteredSpinner from "components/CenteredSpinner"; import { getRoleById } from "services/roles"; - -const modalStyle = { - borderRadius: "8px", - padding: "32px 32px 22px 32px", - maxWidth: "460px", - width: "100%", - margin: 0, - "overflow-x": "hidden", -}; - -const containerStyle = { - padding: "10px", -}; +import BaseCreateModal from "../BaseCreateModal"; +import MarkdownViewer from "components/MarkdownEditorViewer"; function RoleDetailsModal({ roleId, open, onClose }) { const [isLoading, setIsLoading] = useState(true); const [imgError, setImgError] = useState(false); const [showSkills, setShowSkills] = useState(false); const [role, setRole] = useState(null); + useEffect(() => { - setRole(null); - setIsLoading(true); - getRoleById(roleId).then((response) => { - setRole(response.data); - setIsLoading(false); - }); + if (roleId) { + setImgError(false); + setIsLoading(true); + getRoleById(roleId) + .then((response) => { + setRole(response.data); + }) + .catch(() => { + setRole({ name: "Unable to Load Description" }); + }) + .finally(() => { + setIsLoading(false); + }); + } }, [roleId]); + const headerIcon = useMemo( + () => + role && role.imageUrl && !imgError ? ( + setImgError(true)} + alt={role.name} + /> + ) : ( + + ), + [role, imgError] + ); + + const skills = role ? role.listOfSkills : []; + + const hideSkills = () => { + onClose(); + setTimeout(() => setShowSkills(false), 0); + }; + + const closeButton = ( + + ); + return ( - - } - styles={{ - modal: modalStyle, - modalContainer: containerStyle, - }} + onClose={hideSkills} + hideCloseIcon + isLoading={isLoading} + loadingMessage="Loading..." + title={role?.name} + headerIcon={headerIcon} + buttons={closeButton} + darkHeader > -
- {isLoading ? ( - <> - -
Loading...
- +
+
+ + +
+ {showSkills ? ( +
    + {skills.map((skill, i) => ( +
  • + {skill} +
  • + ))} +
) : ( - <> - {role && role.imageUrl && !imgError ? ( - setImgError(true)} - alt={role.name} - styleName="role-icon" - /> - ) : ( - - )} -
- - -
-
{role?.name}
-

{role?.description}

- +
+ +
)}
-
- -
- + ); } diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss index 81daa623..e7470ce5 100644 --- a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss @@ -1,55 +1,49 @@ @import "styles/include"; .button-group { - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-end; - :first-child { - margin-right: 8px; - } -} - -.tab-button-group { display: flex; flex-direction: row; align-items: center; justify-content: center; - margin-bottom: 42px; + margin-bottom: 30px; } -.modal-body { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - text-align: center; - margin-bottom: 80px; - - .role-icon { - width: 42px; - height: 42px; - } - - h5 { - @include font-barlow-condensed; - font-size: 34px; - color: #1e94a3; - text-transform: uppercase; - font-weight: 500; - margin-bottom: 10px; - } +.body { + @include font-roboto; + color: #2a2a2a; +} - p { +.markdown-container { + // not adds specificity to override style + p:not(table) { @include font-roboto; + color: #2a2a2a; font-size: 16px; - color: #555555; line-height: 26px; } } -.cross { - g { - stroke: #000; - } +.description-item { + font-size: 16px; + line-height: 26px; + margin-bottom: 10px; + list-style-type: disc; +} + +.skill-list { + margin: 0 30px; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + flex-wrap: wrap; +} + +.skill-item { + background-color: #e9e9e9; + border-radius: 5px; + padding: 6px 9px; + margin-right: 6px; + margin-bottom: 10px; + font-size: 12px; } diff --git a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx new file mode 100644 index 00000000..7e8c2bb4 --- /dev/null +++ b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx @@ -0,0 +1,176 @@ +/** + * SearchContainer + * + * A container component for the different + * search pages. Contains logic and supporting + * components for searching for roles. + */ +import React, { useCallback, useState } from "react"; +import PT from "prop-types"; +import { toastr } from "react-redux-toastr"; +import { navigate } from "@reach/router"; +import _ from "lodash"; +import AddedRolesAccordion from "../AddedRolesAccordion"; +import Completeness from "../Completeness"; +import SearchCard from "../SearchCard"; +import ResultCard from "../ResultCard"; +import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; +import { createJob } from "services/jobs"; +import { postProject, searchRoles } from "services/teams"; +import { setCurrentStage } from "utils/helpers"; +import AddAnotherModal from "../AddAnotherModal"; +import "./styles.module.scss"; + +function SearchContainer({ + stages, + setStages, + isCompletenessDisabled, + children, + searchObject, + completenessStyle, + locationState, + reloadRolesPage, +}) { + const [addedRoles, setAddedRoles] = useState( + locationState?.addedRoles ? locationState.addedRoles : [] + ); + const [searchState, setSearchState] = useState(null); + const [matchingRole, setMatchingRole] = useState(null); + const [addAnotherModalOpen, setAddAnotherModalOpen] = useState(false); + const [submitDone, setSubmitDone] = useState(true); + const [prevSearchId, setPrevSearchId] = useState(locationState?.prevSearchId); + + const submitJob = () => { + setSubmitDone(false); + postProject() + .then((res) => { + const projectId = _.get(res, "data.id"); + + createJob({ + projectId, + title: `job-${Date()}`, + skills: [], + roleIds: addedRoles.map((r) => r.id), + numPositions: 1, + }) + .then(() => { + toastr.success("Job Submitted"); + }) + .catch((err) => { + console.error(err); + toastr.warning("Error Submitting Job"); + }); + }) + .catch((err) => { + console.error(err); + toastr.warning("Error Creating Project"); + }) + .finally(() => { + setSubmitDone(true); + navigate("/taas/myteams"); + }); + }; + + const addAnother = () => { + if (!reloadRolesPage) { + navigate("/taas/myteams/createnewteam/role", { + state: { addedRoles, prevSearchId }, + }); + return; + } + setCurrentStage(0, stages, setStages); + setSearchState(null); + setMatchingRole(null); + setAddAnotherModalOpen(false); + reloadRolesPage(); + }; + + const search = () => { + setCurrentStage(1, stages, setStages); + setSearchState("searching"); + setMatchingRole(null); + const searchObjectCopy = { ...searchObject }; + if (prevSearchId) { + searchObjectCopy.previousRoleSearchRequestId = prevSearchId; + } + searchRoles(searchObjectCopy) + .then((res) => { + const id = _.get(res, "data.id"); + const name = _.get(res, "data.name"); + setPrevSearchId(_.get(res, "data.roleSearchRequestId")); + if (name && !name.toLowerCase().includes("niche")) { + setMatchingRole(res.data); + setAddedRoles((addedRoles) => [...addedRoles, { id, name }]); + } + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setCurrentStage(2, stages, setStages); + setSearchState("done"); + }); + }; + + const renderLeftSide = () => { + if (!searchState) return children; + if (searchState === "searching") return ; + if (matchingRole) return ; + return ( + + ); + }; + + const getPercentage = useCallback(() => { + if (!searchState) return "26"; + if (searchState === "searching") return "52"; + if (matchingRole) return "98"; + return "88"; + }, [searchState, matchingRole]); + + return ( +
+ {renderLeftSide()} +
+ + setAddAnotherModalOpen(true) : search} + extraStyleName={completenessStyle} + buttonLabel={searchState ? "Submit Request" : "Search"} + stages={stages} + percentage={getPercentage()} + /> +
+ {searchState === "done" && matchingRole && ( + setAddAnotherModalOpen(false)} + submitDone={submitDone} + onContinueClick={submitJob} + addAnother={addAnother} + /> + )} +
+ ); +} + +SearchContainer.propTypes = { + stages: PT.array, + setStages: PT.func, + isCompletenessDisabled: PT.bool, + searchObject: PT.object, + children: PT.node, + completenessStyle: PT.string, + locationState: PT.object, + reloadRolesPage: PT.func, +}; + +export default SearchContainer; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/styles.module.scss b/src/routes/CreateNewTeam/components/SearchContainer/styles.module.scss similarity index 99% rename from src/routes/CreateNewTeam/pages/SelectRole/styles.module.scss rename to src/routes/CreateNewTeam/components/SearchContainer/styles.module.scss index 7bacc294..99cec905 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/styles.module.scss +++ b/src/routes/CreateNewTeam/components/SearchContainer/styles.module.scss @@ -11,4 +11,4 @@ margin-top: 16px; } } -} +} \ No newline at end of file diff --git a/src/routes/CreateNewTeam/index.jsx b/src/routes/CreateNewTeam/index.jsx index 62fd1826..34eab93e 100644 --- a/src/routes/CreateNewTeam/index.jsx +++ b/src/routes/CreateNewTeam/index.jsx @@ -1,5 +1,9 @@ /** * Create New Team + * + * Gets location state from router to pass + * along to search pages + * * Landing page for creating new teams * by selecting a role, inputting skills, * or inputting a job description @@ -7,31 +11,20 @@ import React from "react"; import { navigate } from "@reach/router"; import _ from "lodash"; -import { toastr } from "react-redux-toastr"; +import PT from "prop-types"; import Page from "components/Page"; import PageHeader from "components/PageHeader"; import LandingBox from "./components/LandingBox"; 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 { postProject } from "services/teams"; -import withAuthentication from "../../hoc/withAuthentication"; -function CreateNewTeam() { - const createProjectAndNavigate = async (navigateTo) => { - postProject() - .then((res) => { - const id = _.get(res, "data.id"); - navigate(`/taas/myteams/createnewteam/${id}/${navigateTo}`); - }) - .catch((err) => { - toastr.warning("Error", "Failed to create a new team."); - console.error(err); - }); - }; +function CreateNewTeam({ location: { state: locationState } }) { + const prevSearchId = locationState?.prevSearchId; + const addedRoles = locationState?.addedRoles; - const goToJobDescription = () => { - navigate(`/taas/myteams/createnewteam/jd`); + const goToRoute = (path) => { + navigate(path, { state: { prevSearchId, addedRoles } }); }; return ( @@ -45,24 +38,28 @@ function CreateNewTeam() { description="You know you want a front end developer, or a full stack developer, mobile one or others." icon={} backgroundImage="linear-gradient(101.95deg, #8B41B0 0%, #EF476F 100%)" - onClick={() => createProjectAndNavigate("role")} + onClick={() => goToRoute("/taas/myteams/createnewteam/role")} /> } backgroundImage="linear-gradient(221.5deg, #2C95D7 0%, #9D41C9 100%)" - onClick={() => createProjectAndNavigate("skills")} + onClick={() => goToRoute("/taas/myteams/createnewteam/skills")} /> } backgroundImage="linear-gradient(135deg, #2984BD 0%, #0AB88A 100%)" - onClick={goToJobDescription} + onClick={() => goToRoute("/taas/myteams/createnewteam/jd")} /> ); } -export default withAuthentication(CreateNewTeam); +CreateNewTeam.propTypes = { + locationState: PT.object, +}; + +export default CreateNewTeam; diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx index f6bbc2de..6182c9a7 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx @@ -1,150 +1,56 @@ /** * Input Job Description page * + * Gets location state from router + * + * Allows user to search for roles by + * job description */ -import React, { useCallback, useEffect, useState } from "react"; -import { useData } from "hooks/useData"; -import { navigate } from "@reach/router"; -import { toastr } from "react-redux-toastr"; -import { setCurrentStage } from "utils/helpers"; -import Page from "components/Page"; +import React, { useCallback, useState } from "react"; import PT from "prop-types"; import PageHeader from "components/PageHeader"; -import LoadingIndicator from "components/LoadingIndicator"; import MarkdownEditor from "../../../../components/MarkdownEditor"; -import { getSkillsByJobDescription } from "../../../../services/teams"; -import Completeness from "../../components/Completeness"; -import { getSkills } from "services/skills"; -import SearchCard from "../../components/SearchCard"; -import ResultCard from "../../components/ResultCard"; -import AddAnotherModal from "../../components/AddAnotherModal"; -import SkillListPopup from "./components/SkillListPopup"; import "./styles.module.scss"; -import withAuthentication from "../../../../hoc/withAuthentication"; -import IconOfficeFileText from "../../../../assets/images/icon-office-file-text.svg"; +import SearchContainer from "../../components/SearchContainer"; -function InputJobDescription() { +function InputJobDescription({ location: { state: locationState } }) { const [stages, setStages] = useState([ - { name: "Input Job Desccription", isCurrent: true }, + { name: "Input Job Description", isCurrent: true }, { name: "Search Member" }, { name: "Overview of the Results" }, ]); const [jdString, setJdString] = useState(""); - const [searchState, setSearchState] = useState(null); - const [modalOpen, setModalOpen] = useState(false); - const [skillModalOpen, setSkillModalOpen] = useState(false); - const [submitDone, setSubmitDone] = useState(false); - const [skills, setSkills] = useState([]); - const [isLoadingSkills, setIsLoadingSkills] = useState(false); - - const onSearch = useCallback( - (value) => { - setSkillModalOpen(true); - setIsLoadingSkills(true); - getSkillsByJobDescription(jdString) - .then((response) => { - setSkills(response.data); - setIsLoadingSkills(false); - setSkillModalOpen(true); - }) - .catch(() => { - setIsLoadingSkills(false); - }); - }, - [jdString] - ); - - const onConfirationClick = useCallback(() => { - setSearchState("searching"); - setCurrentStage(1, stages, setStages); - setTimeout(() => { - setCurrentStage(2, stages, setStages); - setSearchState("done"); - }, 3000); - }, []); - - const addAnother = useCallback(() => { - // navigate(`/taas/myteams/createnewteam/${projectId}/role`); - }, []); - - const submitJob = () => { - setSubmitDone(false); - setModalOpen(true); - setTimeout(() => { - setSubmitDone(true); - }, 3000); - }; const onEditChange = useCallback((value) => { setJdString(value); }, []); return ( -
- {!searchState ? ( -
-
- - -
- - setSkillModalOpen(false)} - isLoading={isLoadingSkills} - onContinueClick={onConfirationClick} - /> -
- ) : searchState === "searching" ? ( -
- - -
- ) : ( -
- - - setModalOpen(false)} - submitDone={submitDone} - addAnother={addAnother} - /> -
- )} -
+ +
+ + +
+
); } InputJobDescription.propTypes = { - projectId: PT.string, + locationState: PT.object, }; -export default withAuthentication(InputJobDescription); +export default InputJobDescription; diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss index 31e3ca4b..9fe69610 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss @@ -1,17 +1,9 @@ -.page { - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; - margin: 42px 35px; - - .edit-container { - background-color: #ffffff; - border-radius: 8px; - max-width: 746px; - position: relative; - margin-right: 30px; - padding: 0 30px 30px; - flex: 1; - } +.edit-container { + background-color: #ffffff; + border-radius: 8px; + max-width: 746px; + position: relative; + margin-right: 30px; + padding: 0 30px 30px; + flex: 1; } diff --git a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/styles.module.scss b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/styles.module.scss index ecc6b566..8d3c180a 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/styles.module.scss @@ -9,11 +9,14 @@ align-items: center; margin: 0 0 24px 24px; cursor: pointer; + color: #555; + font-weight: 500; &.selected { border-color: #0ab88a; background-color: #e0faf3; font-weight: 500; + color: #2a2a2a; } } diff --git a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/styles.module.scss b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/styles.module.scss index 15b6fbce..452f06f1 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillsList/styles.module.scss @@ -22,6 +22,8 @@ // adding "input:not([type="checkbox"])" to make sure that we override reset styles input:not([type="checkbox"]).filter-input { + display: inline-block; + position: relative; width: 300px; background-color: #ffffff; border: 1px solid #aaaaaa; @@ -34,6 +36,13 @@ input:not([type="checkbox"]).filter-input { outline: none; padding: 0 15px; + &:not(:focus) { + background-image: url("../../../../../../assets/images/icon-search.svg"); + background-repeat: no-repeat; + background-position: 10px center; + text-indent: 20px; + } + &::placeholder { color: #aaaaaa; } diff --git a/src/routes/CreateNewTeam/pages/InputSkills/index.jsx b/src/routes/CreateNewTeam/pages/InputSkills/index.jsx index 9341e86b..f105df60 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputSkills/index.jsx @@ -2,68 +2,29 @@ * Input Skills page * Page that user reaches after choosing to input job skills. * - * Gets a project id from the router. + * Gets location state from the router. * * Allows selecting a number of skills, searching for users * with those skills, and submitting a job requiring the skills. */ -import React, { useCallback, useEffect, useState } from "react"; -import { useData } from "hooks/useData"; -import { navigate } from "@reach/router"; -import { toastr } from "react-redux-toastr"; +import React, { useCallback, useState } from "react"; import PT from "prop-types"; +import { useData } from "hooks/useData"; import SkillsList from "./components/SkillsList"; -import Completeness from "../../components/Completeness"; -import "./styles.module.scss"; import { getSkills } from "services/skills"; -import { setCurrentStage } from "utils/helpers"; import LoadingIndicator from "components/LoadingIndicator"; -import SearchCard from "../../components/SearchCard"; -import ResultCard from "../../components/ResultCard"; -import { createJob } from "services/jobs"; -import AddAnotherModal from "../../components/AddAnotherModal"; -import withAuthentication from "../../../../hoc/withAuthentication"; +import SearchContainer from "../../components/SearchContainer"; -function InputSkills({ projectId }) { +function InputSkills({ location: { state: locationState } }) { const [stages, setStages] = useState([ { name: "Input Skills", isCurrent: true }, { name: "Search Member" }, { name: "Overview of the Results" }, ]); const [selectedSkills, setSelectedSkills] = useState([]); - const [searchState, setSearchState] = useState(null); - const [modalOpen, setModalOpen] = useState(false); - const [submitDone, setSubmitDone] = useState(false); const [skills, loadingError] = useData(getSkills); - let searchTimer; - - const submitJob = () => { - setSubmitDone(false); - setModalOpen(true); - createJob({ - projectId, - title: `job-${Date()}`, - skills: selectedSkills, - numPositions: 1, - }) - .then(() => { - toastr.success("Job Submitted"); - }) - .catch((err) => { - console.error(err); - toastr.warning("Error Submitting Job"); - }) - .finally(() => { - setSubmitDone(true); - }); - }; - - const addAnother = useCallback(() => { - navigate(`/taas/myteams/createnewteam/${projectId}/roles`); - }, [projectId]); - const toggleSkill = useCallback( (id) => { if (selectedSkills.includes(id)) { @@ -77,69 +38,34 @@ function InputSkills({ projectId }) { [selectedSkills] ); - // mocked search for users with given skills - const search = () => { - setSearchState("searching"); - setCurrentStage(1, stages, setStages); - searchTimer = setTimeout(() => { - setSearchState("done"); - setCurrentStage(2, stages, setStages); - }, 3000); - }; + if (!Array.isArray(skills)) { + return ; + } - useEffect(() => clearTimeout(searchTimer)); + if (skills.length === 0) { + return

Failed to load skills

; + } - return !skills ? ( - - ) : !searchState ? ( -
+ return ( + - -
- ) : searchState === "searching" ? ( -
- - -
- ) : ( -
- - - setModalOpen(false)} - submitDone={submitDone} - addAnother={addAnother} - /> -
+ ); } InputSkills.propTypes = { - projectId: PT.string, + locationState: PT.object, }; -export default withAuthentication(InputSkills); +export default InputSkills; diff --git a/src/routes/CreateNewTeam/pages/InputSkills/styles.module.scss b/src/routes/CreateNewTeam/pages/InputSkills/styles.module.scss deleted file mode 100644 index b47da072..00000000 --- a/src/routes/CreateNewTeam/pages/InputSkills/styles.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -.page { - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; - margin: 42px 35px; -} diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss index 3f5b7652..361d40e8 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss @@ -4,12 +4,13 @@ border: 1px solid #d4d4d4; border-radius: 5px; padding: 12px 16px; - width: 213px; + width: 212px; height: 136px; display: flex; flex-direction: column; justify-content: space-evenly; - margin: 0 0 24px 24px; + align-items: flex-start; + margin: 0 0 23px 23px; cursor: pointer; &.selected { @@ -22,6 +23,7 @@ width: 42px; height: 42px; margin-left: 8px; + object-fit: cover; } .item-text { @@ -38,7 +40,7 @@ padding: 0; outline: none; background: none; - color: #0D61BF; + color: #0d61bf; border: none; text-align: left; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss b/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss index bc9e544e..5cd71f66 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss @@ -5,6 +5,8 @@ max-width: 746px; margin-right: 20px; position: relative; + height: 80vh; + overflow-y: scroll; > header { padding: 16px 24px; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx index a8137e94..d2dda3fe 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx +++ b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx @@ -1,191 +1,78 @@ /** * Select Role Page * - * Gets project id from the router. + * Gets locationState from the router. * * Allows selecting a role, searching for users * with that role, and submitting a job requiring the roles. */ -import React, { useCallback, useEffect, useState } from "react"; -import { useData } from "hooks/useData"; -import { navigate } from "@reach/router"; -import { toastr } from "react-redux-toastr"; +import React, { useCallback, useState } from "react"; import PT from "prop-types"; +import { useData } from "hooks/useData"; import RolesList from "./components/RolesList"; -import Completeness from "../../components/Completeness"; -import "./styles.module.scss"; import { getRoles } from "services/roles"; -import { setCurrentStage } from "utils/helpers"; import LoadingIndicator from "components/LoadingIndicator"; -import SearchCard from "../../components/SearchCard"; -import ResultCard from "../../components/ResultCard"; -import NoMatchingProfilesResultCard from "../../components/NoMatchingProfilesResultCard"; -import { createJob } from "services/jobs"; -import AddAnotherModal from "../../components/AddAnotherModal"; import RoleDetailsModal from "../../components/RoleDetailsModal"; -import withAuthentication from "../../../../hoc/withAuthentication"; -import AddedRolesAccordion from "./components/AddedRolesAccordion"; +import SearchContainer from "../../components/SearchContainer"; -function SelectRole({ projectId }) { +function SelectRole({ location: { state: locationState } }) { const [stages, setStages] = useState([ { name: "Select a Role", isCurrent: true }, { name: "Search Member" }, { name: "Overview of the Results" }, ]); - const [addedRoles, setAddedRoles] = useState([]); const [selectedRoleId, setSelectedRoleId] = useState(null); - const [searchState, setSearchState] = useState(null); - const [matchingProfiles, setMatchingProfiles] = useState(null); - const [addAnotherModalOpen, setAddAnotherModalOpen] = useState(false); const [roleDetailsModalOpen, setRoleDetailsModalOpen] = useState(false); const [roleDetailsModalId, setRoleDetailsModalId] = useState(null); - const [submitDone, setSubmitDone] = useState(true); const [roles, loadingError] = useData(getRoles); - let searchTimer; - - const submitJob = () => { - setSubmitDone(false); - createJob({ - projectId, - title: `job-${Date()}`, - skills: [], - roleIds: addedRoles.map((r) => r.id), - numPositions: 1, - }) - .then(() => { - toastr.success("Job Submitted"); - }) - .catch((err) => { - console.error(err); - toastr.warning("Error Submitting Job"); - }) - .finally(() => { - setSubmitDone(true); - navigate("/taas/myteams"); - }); - }; - - const addAnother = useCallback(() => { - setSelectedRoleId(null); - setCurrentStage(0, stages, setStages); - setAddAnotherModalOpen(false); - setSearchState(null); - }, [stages]); - - const toggleRole = useCallback( - (id) => { - setSelectedRoleId((selectedRoleId) => - id === selectedRoleId ? null : id - ); - }, - [setSelectedRoleId] - ); + const toggleRole = useCallback((id) => { + setSelectedRoleId((selectedRoleId) => (id === selectedRoleId ? null : id)); + }, []); const onDescriptionClick = useCallback((roleId) => { setRoleDetailsModalId(roleId); setRoleDetailsModalOpen(true); }, []); - // mocked search for users with given roles - const search = () => { - setCurrentStage(1, stages, setStages); - setSearchState("searching"); - searchTimer = setTimeout(() => { - setCurrentStage(2, stages, setStages); - setMatchingProfiles(null); // display no matching profiles screen for a while - setSearchState("done"); - setTimeout(() => setMatchingProfiles(true), 2000); - // add selected role - const { id, name } = roles.find((r) => r.id === selectedRoleId); - setAddedRoles((addedRoles) => [...addedRoles, { id, name }]); - }, 3000); + const resetState = () => { + setSelectedRoleId(null); + setRoleDetailsModalId(false); + setRoleDetailsModalId(null); }; - useEffect(() => clearTimeout(searchTimer)); - if (!roles) { return ; } - if (roles && !searchState) { - return ( -
- -
- {addedRoles.length > 0 && ( - - )} - - setRoleDetailsModalOpen(false)} - /> -
-
- ); - } - - if (searchState === "searching") { - return ( -
- - -
- ); - } - - if (searchState === "done") { - return ( -
- {matchingProfiles ? : } -
- {matchingProfiles && } - setAddAnotherModalOpen(true)} - /> -
- {matchingProfiles && ( - setAddAnotherModalOpen(false)} - submitDone={submitDone} - onContinueClick={submitJob} - addAnother={addAnother} - /> - )} -
- ); - } + return ( + + + setRoleDetailsModalOpen(false)} + /> + + ); } SelectRole.propTypes = { - projectId: PT.string, + locationState: PT.object, }; -export default withAuthentication(SelectRole); +export default SelectRole; diff --git a/src/services/skills.js b/src/services/skills.js index d2a91299..36a91af8 100644 --- a/src/services/skills.js +++ b/src/services/skills.js @@ -16,7 +16,7 @@ export function getSkills() { getAllSkills().catch((ex) => { console.error("Error loading skills", ex); cachedSkillsAsPromise = null; - return []; + return { data: [] }; }); return cachedSkillsAsPromise; diff --git a/src/services/teams.js b/src/services/teams.js index f220a1d6..658f98f8 100644 --- a/src/services/teams.js +++ b/src/services/teams.js @@ -206,3 +206,21 @@ export const postProject = () => { return axios.post(url, bodyObj); }; + +/** + * Search for roles matching a role id, job description + * or list of skills + * + * @param {Object} searchObject object containing data for search + * @param {string} searchObject.roleId a role id to search for + * @param {string} searchObject.jobDescription job description used for search + * @param {string[]} searchObject.skills array of skill ids used for role search + * @param {string} searchObject.previousRoleSearchRequestId id of the last search made + * @returns + */ +export const searchRoles = (searchObject) => { + const newObject = { ...searchObject }; + delete newObject.previousRoleSearchRequestId; + const url = `${config.API.V5}/taas-teams/sendRoleSearchRequest`; + return axios.post(url, newObject); +};