diff --git a/config/dev.js b/config/dev.js index dfe168fd..2daf4829 100644 --- a/config/dev.js +++ b/config/dev.js @@ -13,4 +13,7 @@ module.exports = { V5: "https://api.topcoder-dev.com/v5", //"http://localhost:3030/api/v5" V3: "https://api.topcoder-dev.com/v3", }, + + // matching rate for search role result page + MATCHING_RATE: process.env.MATCHING_RATE || 80, }; diff --git a/config/prod.js b/config/prod.js index 21cdc373..5416e540 100644 --- a/config/prod.js +++ b/config/prod.js @@ -13,4 +13,7 @@ module.exports = { V5: "https://api.topcoder.com/v5", V3: "https://api.topcoder.com/v3", }, + + // matching rate for search role result page + MATCHING_RATE: process.env.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 100% rename from src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/index.jsx rename to src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx 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/Completeness/styles.module.scss b/src/routes/CreateNewTeam/components/Completeness/styles.module.scss index 3227642c..8e488b15 100644 --- a/src/routes/CreateNewTeam/components/Completeness/styles.module.scss +++ b/src/routes/CreateNewTeam/components/Completeness/styles.module.scss @@ -6,6 +6,7 @@ position: relative; width: 250px; color: #fff; + overflow: hidden; button { border: none; } diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx index 0bde6659..2938cccf 100644 --- a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx @@ -3,13 +3,17 @@ * Card that appears when there are no matching profiles after searching. */ import React from "react"; +import PT from "prop-types"; import { navigate } from "@reach/router"; +import { + formatMoney, +} from "utils/format"; 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({rates = [{global: 0}], onSearch}) { return (
@@ -25,11 +29,11 @@ function NoMatchingProfilesResultCard() {

Niche Rate

-

$1,200

+

{formatMoney(rates[0].global)}

/Week

- {showRates && showSpecialRates && ( + {showRates && !isExternalMember && (

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

(40h / week)

-

Senior Member

+

Global Rate

-

$2,000

+

{formatMoney(rates[0].global)}

/Week

-

Standard Member

+

In-Conutry Rate

-

$1,500

+

{formatMoney(rates[0].inCountry)}

/Week

-

Junior Member

+

Offshore Rate

-

$1,000

+

{formatMoney(rates[0].offShore)}

/Week

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

(30h / week)

-

Senior Member

+

Global Rate

-

$1,800

+

{formatMoney(rates[0].rate30Global)}

/Week

-

Standard Member

+

In-Conutry Rate

-

$1,300

+

{formatMoney(rates[0].rate30InCountry)}

/Week

-

Junior Member

+

Offshore Rate

-

$800

+

{formatMoney(rates[0].rate30OffShore)}

/Week

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

(20h / week)

-

Senior Member

+

Global Rate

-

$1,600

+

{formatMoney(rates[0].rate20Global)}

/Week

-

Standard Member

+

In-Conutry Rate

-

$1,100

+

{formatMoney(rates[0].rate20InCountry)}

/Week

-

Junior Member

+

Offshore Rate

-

$600

+

{formatMoney(rates[0].rate20OffShore)}

/Week

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

(40h / week)

-
$1,800
+
{formatMoney(rates[0].global)}

/Week

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

(30h / week)

-
$1,250
+
{formatMoney(rates[0].inCountry)}

/Week

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

(20h / week)

-
$800
+
{formatMoney(rates[0].offShore)}

/Week

@@ -190,14 +197,14 @@ function ResultCard() {

Qualified candidates within

-
24h
+
{timeToCandidate}h

Interviews can start within

-
48h
+
{timeToInterview}h
@@ -209,11 +216,11 @@ function ResultCard() {
-

80%

+

{config.MATCHING_RATE}%

Matching rate

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

300+

+

{numberOfMembersAvailable}+

Members matched

@@ -246,4 +253,12 @@ function ResultCard() { ); } +ResultCard.propTypes = { + numberOfMembersAvailable:PT.number, + numberOfMembers: PT.string, + timeToCandidate: PT.number, + timeToInterview: PT.number, + isExternalMember: PT.bool, + rates: PT.array, +}; 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..cba4f0a9 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss @@ -3,6 +3,7 @@ .result-card { @include rounded-card; max-width: 746px; + overflow: hidden; width: 50vw; margin-right: 30px; } @@ -23,7 +24,6 @@ position: relative; text-align: center; border-radius: 8px 8px 0 0; - cursor: pointer; svg { margin-bottom: 8px; @@ -72,9 +72,12 @@ margin: 0 40px; } .progressbar-child { - margin-top: 5px; width: 90px; height: 90px; + overflow: hidden; + > h4 { + margin-top: 10px; + } } } @@ -154,6 +157,9 @@ &:not(:first-child) { margin-left: 40px; } + &:not(:last-child) { + border-right: 1px solid #E9E9E9; + } .rate-heading { display: flex; diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx index 17039e66..179619c3 100644 --- a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx @@ -6,17 +6,18 @@ import React, { useState, useEffect } from "react"; import PT from "prop-types"; import Modal from "react-responsive-modal"; import Button from "components/Button"; +import MarkdownEditorViewer from "../../../../components/MarkdownEditorViewer"; 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", + borderRadius: "10px", + padding: "70px 80px 60px", + maxWidth: "680px", width: "100%", + height: "650px", margin: 0, "overflow-x": "hidden", }; @@ -44,9 +45,7 @@ function RoleDetailsModal({ roleId, open, onClose }) { open={open} center onClose={onClose} - closeIcon={ - - } + showCloseIcon={false} styles={{ modal: modalStyle, modalContainer: containerStyle, @@ -56,7 +55,7 @@ function RoleDetailsModal({ roleId, open, onClose }) { {isLoading ? ( <> -
Loading...
+
Loading...
) : ( <> @@ -70,6 +69,7 @@ function RoleDetailsModal({ roleId, open, onClose }) { ) : ( )} +
{role?.name}
-
{role?.name}
-

{role?.description}

+ {!showSkills &&
} + {showSkills &&

{_.map(role?.listOfSkills, (s)=> + {s} + )}

} )} diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss index 81daa623..3f964c9a 100644 --- a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss @@ -1,5 +1,39 @@ @import "styles/include"; + +.loading { + margin-bottom: 100px; + margin-top: 200px; +} + +.skill-tag-list { + display: flex; + height: 260px; + flex-wrap: wrap; +} + +.desc-container { + width: 100%; + height: 260px; + overflow: scroll; +} + +.skill-tag { + @include font-roboto; + + background-color: #E9E9E9; + border-radius: 5px; + padding: 6px 10px; + margin-right: 6px; + margin-bottom: 10px; + color: #2A2A2A; + font-size: 12px; + letter-spacing: 0.25px; + line-height: 16px; + height: 26px; + display: inline-block; +} + .button-group { display: flex; flex-direction: row; @@ -15,7 +49,8 @@ flex-direction: row; align-items: center; justify-content: center; - margin-bottom: 42px; + margin-top: 14px; + margin-bottom: 30px; } .modal-body { @@ -23,18 +58,19 @@ flex-direction: column; justify-content: flex-start; align-items: center; - text-align: center; - margin-bottom: 80px; + // text-align: center; + margin-bottom: 40px; .role-icon { width: 42px; height: 42px; + margin-bottom: 18px; } h5 { @include font-barlow-condensed; font-size: 34px; - color: #1e94a3; + color: #000; text-transform: uppercase; font-weight: 500; margin-bottom: 10px; diff --git a/src/routes/CreateNewTeam/components/SearchableList/index.jsx b/src/routes/CreateNewTeam/components/SearchableList/index.jsx new file mode 100644 index 00000000..88ab6624 --- /dev/null +++ b/src/routes/CreateNewTeam/components/SearchableList/index.jsx @@ -0,0 +1,88 @@ +/** + * SearchableList + * wrap roles list and skills list + * Allows selecting and filtering by name. + */ +import React, { useEffect, useState } from "react"; +import { useDebounce } from "react-use"; +import PT from "prop-types"; +import Input from "components/Input"; +import PageHeader from "components/PageHeader"; +import "./styles.module.scss"; +import { INPUT_DEBOUNCE_DELAY } from "constants/"; + +function SearchableList({title, inputPlaceholder, itemList, selectedRoleId, onDescriptionClick, toggleRole, renderItem, countElement}) { + const [filteredItems, setFilteredItems] = useState(itemList); + const [filter, setFilter] = useState(""); + const [debouncedFilter, setDebouncedFilter] = useState(""); + + const onFilterChange = (e) => { + setFilter(e.target.value); + }; + + useDebounce( + () => { + setDebouncedFilter(filter); + }, + INPUT_DEBOUNCE_DELAY, + [filter] + ); + + useEffect(() => { + if (debouncedFilter.length > 0) { + const filterText = debouncedFilter.toLowerCase(); + setFilteredItems( + itemList.filter((item) => item.name.toLowerCase().includes(filterText)) + ); + } else { + setFilteredItems(itemList); + } + }, [debouncedFilter, itemList]); + + return ( +
+ + + {filter && ( + setFilter("")} + > + X + + )} +
+ } + /> + {countElement} +
+ {filteredItems.map((item) => ( + renderItem(item) + ))} +
+ + ); +} + +SearchableList.propTypes = { + itemList: PT.array, + title: PT.string, + inputPlaceholder: PT.string, + renderItem: PT.func, + countElement: PT.element, + toggleRole: PT.func, +}; + +export default SearchableList; diff --git a/src/routes/CreateNewTeam/components/SearchableList/styles.module.scss b/src/routes/CreateNewTeam/components/SearchableList/styles.module.scss new file mode 100644 index 00000000..e17803f5 --- /dev/null +++ b/src/routes/CreateNewTeam/components/SearchableList/styles.module.scss @@ -0,0 +1,73 @@ +@import "styles/include"; + + +.input-container { + position: relative; +} + +.searchable-list { + @include rounded-card; + max-width: 746px; + margin-right: 20px; + position: relative; + flex: 1; + + > header { + padding: 16px 24px; + } +} + +.role-count { + position: absolute; + font-size: 12px; + top: 72px; + left: 73px; +} + +// 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; + border-radius: 6px; + box-sizing: border-box; + color: #2a2a2a; + font-size: 14px; + height: 40px; + line-height: 38px; + 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; + } +} + +.clear-input-button { + position: absolute; + right: 15px; + font-size: 14px; + font-weight: 700; + line-height: 38px; + cursor: pointer; + &:hover { + color: rgb(216, 24, 24); + } +} + +.searchable-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: wrap; + margin-right: 24px; +} diff --git a/src/routes/CreateNewTeam/index.jsx b/src/routes/CreateNewTeam/index.jsx index 62fd1826..763791cf 100644 --- a/src/routes/CreateNewTeam/index.jsx +++ b/src/routes/CreateNewTeam/index.jsx @@ -14,24 +14,10 @@ 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); - }); - }; - - const goToJobDescription = () => { - navigate(`/taas/myteams/createnewteam/jd`); + const onSearchClick = (page) => { + navigate(`/taas/myteams/createnewteam/${page}`); }; return ( @@ -45,24 +31,24 @@ 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={() => onSearchClick("role")} /> } backgroundImage="linear-gradient(221.5deg, #2C95D7 0%, #9D41C9 100%)" - onClick={() => createProjectAndNavigate("skills")} + onClick={() => onSearchClick("skills")} /> } backgroundImage="linear-gradient(135deg, #2984BD 0%, #0AB88A 100%)" - onClick={goToJobDescription} + onClick={() => onSearchClick("jd")} /> ); } -export default withAuthentication(CreateNewTeam); +export default CreateNewTeam; diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx deleted file mode 100644 index 60317253..00000000 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Temporary Popup for skill list - * show skill list - */ -import React from "react"; -import _ from "lodash"; -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 IconSingleManAdd from "../../../../../../assets/images/icon-single-man-add.svg"; -import "./styles.module.scss"; -import CenteredSpinner from "components/CenteredSpinner"; - -const modalStyle = { - borderRadius: "8px", - padding: "32px 32px 22px 32px", - maxWidth: "460px", - width: "100%", - margin: 0, - "overflow-x": "hidden", -}; - -const containerStyle = { - padding: "10px", -}; - -function SkillListPopup({ open, skills, onClose, isLoading, onContinueClick }) { - return ( - - } - styles={{ - modal: modalStyle, - modalContainer: containerStyle, - }} - > -
- {isLoading ? ( - <> - -
loading skills
- - ) : ( - <> - -
skills
- {_.map(skills, (s) => { - return
{s.tag}
; - })} - - )} -
-
- -
-
- ); -} - -SkillListPopup.propTypes = { - open: PT.bool, - onClose: PT.func, - isLoading: PT.bool, - onContinueClick: PT.func, - skills: PT.arrayOf(PT.shape()), -}; - -export default SkillListPopup; diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss deleted file mode 100644 index b0270326..00000000 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/styles.module.scss +++ /dev/null @@ -1,48 +0,0 @@ -@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; - 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; - } -} diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx index f6bbc2de..8020ce43 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx @@ -15,12 +15,13 @@ import MarkdownEditor from "../../../../components/MarkdownEditor"; import { getSkillsByJobDescription } from "../../../../services/teams"; import Completeness from "../../components/Completeness"; import { getSkills } from "services/skills"; +import { sendRoleSearchRequest } from "services/teams"; import SearchCard from "../../components/SearchCard"; import ResultCard from "../../components/ResultCard"; +import NoMatchingProfilesResultCard from "../../components/NoMatchingProfilesResultCard"; import AddAnotherModal from "../../components/AddAnotherModal"; -import SkillListPopup from "./components/SkillListPopup"; import "./styles.module.scss"; -import withAuthentication from "../../../../hoc/withAuthentication"; +import AddedRolesAccordion from "../../components/AddedRolesAccordion"; import IconOfficeFileText from "../../../../assets/images/icon-office-file-text.svg"; function InputJobDescription() { @@ -29,39 +30,18 @@ function InputJobDescription() { { name: "Search Member" }, { name: "Overview of the Results" }, ]); + const [roleSearchResult, setRoleSearchResult] = useState(null); + const [addedRoles, setAddedRoles] = useState([]); + const [ + previousRoleSearchRequestId, + setPreviousRoleSearchRequestId, + ] = useState(null); const [jdString, setJdString] = useState(""); + const [matchingProfiles, setMatchingProfiles] = useState(null); 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`); @@ -75,6 +55,36 @@ function InputJobDescription() { }, 3000); }; + const search = useCallback(() => { + setCurrentStage(1, stages, setStages); + setSearchState("searching"); + sendRoleSearchRequest({ + jobDescription: jdString, + previousRoleSearchRequestId, + }) + .then(({ data }) => { + setRoleSearchResult(data); + setPreviousRoleSearchRequestId(data.roleSearchRequestId); + setCurrentStage(2, stages, setStages); + setMatchingProfiles(null); // display no matching profiles screen for a while + if (data.name && data.name.toLowerCase() !== "niche") { + setMatchingProfiles(true); + setAddedRoles((addedRoles) => [ + ...addedRoles, + { id: data.id, name: data.name }, + ]); + } else { + setMatchingProfiles(false); + } + setSearchState("done"); + }) + .catch((e) => { + setCurrentStage(2, stages, setStages); + setMatchingProfiles(false); // display no matching profiles screen for a while + setSearchState("done"); + }); + }, [jdString]); + const onEditChange = useCallback((value) => { setJdString(value); }, []); @@ -94,21 +104,20 @@ function InputJobDescription() { onChange={onEditChange} /> + +
+ {addedRoles.length > 0 && ( + + )} - setSkillModalOpen(false)} - isLoading={isLoadingSkills} - onContinueClick={onConfirationClick} - /> +
) : searchState === "searching" ? (
@@ -123,7 +132,19 @@ function InputJobDescription() {
) : (
- + {matchingProfiles ? ( + + ) : ( + + )} + +
+ {addedRoles.length > 0 && ( + + )}
+
)} ); @@ -147,4 +169,4 @@ InputJobDescription.propTypes = { projectId: PT.string, }; -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..a1b3188b 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss @@ -14,4 +14,11 @@ padding: 0 30px 30px; flex: 1; } + .right-side { + display: flex; + flex-direction: column; + & > div:not(:first-child) { + margin-top: 16px; + } + } } diff --git a/src/routes/CreateNewTeam/pages/InputSkills/index.jsx b/src/routes/CreateNewTeam/pages/InputSkills/index.jsx index 9341e86b..e513ac5c 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputSkills/index.jsx @@ -12,17 +12,20 @@ import { useData } from "hooks/useData"; import { navigate } from "@reach/router"; import { toastr } from "react-redux-toastr"; import PT from "prop-types"; -import SkillsList from "./components/SkillsList"; +import SearchableList from "../../components/SearchableList"; +import SkillItem from "./components/SkillItem"; import Completeness from "../../components/Completeness"; import "./styles.module.scss"; import { getSkills } from "services/skills"; +import { sendRoleSearchRequest } from "services/teams"; 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 withAuthentication from "../../../../hoc/withAuthentication"; +import AddedRolesAccordion from "../../components/AddedRolesAccordion"; function InputSkills({ projectId }) { const [stages, setStages] = useState([ @@ -30,10 +33,17 @@ function InputSkills({ projectId }) { { name: "Search Member" }, { name: "Overview of the Results" }, ]); + const [ + previousRoleSearchRequestId, + setPreviousRoleSearchRequestId, + ] = useState(null); + const [roleSearchResult, setRoleSearchResult] = useState(null); const [selectedSkills, setSelectedSkills] = useState([]); + const [matchingProfiles, setMatchingProfiles] = useState(null); const [searchState, setSearchState] = useState(null); const [modalOpen, setModalOpen] = useState(false); const [submitDone, setSubmitDone] = useState(false); + const [addedRoles, setAddedRoles] = useState([]); const [skills, loadingError] = useData(getSkills); @@ -66,6 +76,7 @@ function InputSkills({ projectId }) { const toggleSkill = useCallback( (id) => { + setPreviousRoleSearchRequestId(null); if (selectedSkills.includes(id)) { setSelectedSkills(selectedSkills.filter((skill) => skill !== id)); } else { @@ -77,27 +88,64 @@ function InputSkills({ projectId }) { [selectedSkills] ); - // mocked search for users with given skills - const search = () => { - setSearchState("searching"); + const search = useCallback(() => { setCurrentStage(1, stages, setStages); - searchTimer = setTimeout(() => { - setSearchState("done"); - setCurrentStage(2, stages, setStages); - }, 3000); - }; - - useEffect(() => clearTimeout(searchTimer)); + setSearchState("searching"); + sendRoleSearchRequest({ + skills: selectedSkills, + previousRoleSearchRequestId, + }) + .then(({ data }) => { + setRoleSearchResult(data); + setPreviousRoleSearchRequestId(data.roleSearchRequestId); + setCurrentStage(2, stages, setStages); + setMatchingProfiles(null); // display no matching profiles screen for a while + if (data.name && data.name.toLowerCase() !== "niche") { + setAddedRoles((addedRoles) => [ + ...addedRoles, + { id: data.id, name: data.name }, + ]); + setMatchingProfiles(true); + } else { + setMatchingProfiles(false); + } + setSearchState("done"); + }) + .catch((e) => { + setCurrentStage(2, stages, setStages); + setMatchingProfiles(false); // display no matching profiles screen for a while + setSearchState("done"); + }); + }, [selectedSkills]); return !skills ? ( ) : !searchState ? (
- 0 && ( +

+ {selectedSkills.length} skills selected +

+ ) + } + renderItem={({ id, name }) => { + return ( + + ); + }} /> + {addedRoles.length > 0 && } ) : (
- + {matchingProfiles ? ( + + ) : ( + + )} +
+ {addedRoles.length > 0 && ( + + )}
+
); } @@ -142,4 +199,4 @@ InputSkills.propTypes = { projectId: PT.string, }; -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 index b47da072..a6c5fff9 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputSkills/styles.module.scss @@ -4,4 +4,20 @@ justify-content: center; align-items: flex-start; margin: 42px 35px; + + .right-side { + display: flex; + flex-direction: column; + & > div:not(:first-child) { + margin-top: 16px; + } + } +} + + +.skill-count { + position: absolute; + font-size: 12px; + top: 72px; + left: 73px; } 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..a1d5560d 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss @@ -33,6 +33,7 @@ } .button { + width: 75px; font-size: 14px; line-height: 22px; padding: 0; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx index a8137e94..9e15b3b7 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx +++ b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx @@ -11,7 +11,8 @@ import { useData } from "hooks/useData"; import { navigate } from "@reach/router"; import { toastr } from "react-redux-toastr"; import PT from "prop-types"; -import RolesList from "./components/RolesList"; +import RoleItem from "./components/RoleItem"; +import SearchableList from '../../components/SearchableList' import Completeness from "../../components/Completeness"; import "./styles.module.scss"; import { getRoles } from "services/roles"; @@ -21,10 +22,11 @@ import SearchCard from "../../components/SearchCard"; import ResultCard from "../../components/ResultCard"; import NoMatchingProfilesResultCard from "../../components/NoMatchingProfilesResultCard"; import { createJob } from "services/jobs"; +import { sendRoleSearchRequest } from "services/teams"; import AddAnotherModal from "../../components/AddAnotherModal"; import RoleDetailsModal from "../../components/RoleDetailsModal"; -import withAuthentication from "../../../../hoc/withAuthentication"; -import AddedRolesAccordion from "./components/AddedRolesAccordion"; +import AddedRolesAccordion from "../../components/AddedRolesAccordion"; + function SelectRole({ projectId }) { const [stages, setStages] = useState([ @@ -32,6 +34,8 @@ function SelectRole({ projectId }) { { name: "Search Member" }, { name: "Overview of the Results" }, ]); + const [roleSearchResult, setRoleSearchResult] = useState(null); + const [previousRoleSearchRequestId, setPreviousRoleSearchRequestId] = useState(null); const [addedRoles, setAddedRoles] = useState([]); const [selectedRoleId, setSelectedRoleId] = useState(null); const [searchState, setSearchState] = useState(null); @@ -76,6 +80,7 @@ function SelectRole({ projectId }) { const toggleRole = useCallback( (id) => { + setPreviousRoleSearchRequestId(null) setSelectedRoleId((selectedRoleId) => id === selectedRoleId ? null : id ); @@ -88,22 +93,27 @@ function SelectRole({ projectId }) { setRoleDetailsModalOpen(true); }, []); - // mocked search for users with given roles - const search = () => { + const search = useCallback(() => { 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); - }; - - useEffect(() => clearTimeout(searchTimer)); + sendRoleSearchRequest({roleId: selectedRoleId, previousRoleSearchRequestId}).then(({data})=> { + setRoleSearchResult(data) + setCurrentStage(2, stages, setStages); + setMatchingProfiles(null); // display no matching profiles screen for a while + setPreviousRoleSearchRequestId(data.roleSearchRequestId) + if (data.name && data.name.toLowerCase() !== 'niche' ) { + setAddedRoles((addedRoles) => [...addedRoles, { id: data.id, name: data.name }]); + setMatchingProfiles(true) + } else { + setMatchingProfiles(false) + } + setSearchState("done"); + }).catch(e=> { + setCurrentStage(2, stages, setStages); + setMatchingProfiles(false); // display no matching profiles screen for a while + setSearchState("done"); + }) + }, [selectedRoleId, previousRoleSearchRequestId]); if (!roles) { return ; @@ -112,11 +122,23 @@ function SelectRole({ projectId }) { if (roles && !searchState) { return (
- { + return ( + + ) + }} />
{addedRoles.length > 0 && ( @@ -130,11 +152,15 @@ function SelectRole({ projectId }) { stages={stages} percentage="26" /> - setRoleDetailsModalOpen(false)} - /> + { + roleDetailsModalOpen && ( + setRoleDetailsModalOpen(false)} + /> + ) + }
); @@ -158,7 +184,7 @@ function SelectRole({ projectId }) { if (searchState === "done") { return (
- {matchingProfiles ? : } + {matchingProfiles ? : }
{matchingProfiles && } { return axios.get(`${config.API.V5}/taas-teams?${query}`); }; +/** + * search role detail. + * + * @param {object} data search param, roleId | jobDescription | skills + * @returns {Promise<{}>} role detail + */ +export const sendRoleSearchRequest = (data) => { + // mock data + delete data.previousRoleSearchRequestId + return axios.post(`${config.API.V5}/taas-teams/sendRoleSearchRequest`, data); +}; + /** * Get v5 user profile. * @@ -190,19 +202,3 @@ export const postMembers = (teamId, handles, emails) => { return axios.post(url, bodyObj); }; - -/** - * Post new project - * - * @returns {Promise} project object - */ -export const postProject = () => { - const url = `${config.API.V5}/taas-teams/createTeamRequest`; - - const bodyObj = { - name: `project-${Date()}`, - type: "talent-as-a-service", - }; - - return axios.post(url, bodyObj); -};