diff --git a/src/assets/images/icon-earth-x.svg b/src/assets/images/icon-earth-x.svg new file mode 100644 index 00000000..40037611 --- /dev/null +++ b/src/assets/images/icon-earth-x.svg @@ -0,0 +1,13 @@ + + + F087A2AF-1A0A-4B0C-9EFD-323859E6F7A9 + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-role-fallback.svg b/src/assets/images/icon-role-fallback.svg new file mode 100644 index 00000000..9e0f1123 --- /dev/null +++ b/src/assets/images/icon-role-fallback.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-search.svg b/src/assets/images/icon-search.svg new file mode 100644 index 00000000..f6941732 --- /dev/null +++ b/src/assets/images/icon-search.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/root.component.jsx b/src/root.component.jsx index a50a3023..578361c0 100644 --- a/src/root.component.jsx +++ b/src/root.component.jsx @@ -10,12 +10,12 @@ import JobDetails from "./routes/JobDetails"; import JobForm from "./routes/JobForm"; import TeamAccess from "./routes/TeamAccess"; import CreateNewTeam from "./routes/CreateNewTeam"; -import InputSkills from "./routes/InputSkills"; +import InputSkills from "./routes/CreateNewTeam/pages/InputSkills"; +import SelectRole from "./routes/CreateNewTeam/pages/SelectRole"; import ReduxToastr from "react-redux-toastr"; import store from "./store"; import "./styles/main.vendor.scss"; import styles from "./styles/main.module.scss"; -import SelectRole from "./routes/SelectRole"; export default function Root() { return ( diff --git a/src/routes/InputSkills/components/AddAnotherModal/index.jsx b/src/routes/CreateNewTeam/components/AddAnotherModal/index.jsx similarity index 88% rename from src/routes/InputSkills/components/AddAnotherModal/index.jsx rename to src/routes/CreateNewTeam/components/AddAnotherModal/index.jsx index e031669a..c99efcb9 100644 --- a/src/routes/InputSkills/components/AddAnotherModal/index.jsx +++ b/src/routes/CreateNewTeam/components/AddAnotherModal/index.jsx @@ -26,7 +26,13 @@ const containerStyle = { padding: "10px", }; -function AddAnotherModal({ open, onClose, submitDone, addAnother }) { +function AddAnotherModal({ + open, + onClose, + onContinueClick, + submitDone, + addAnother, +}) { return ( Add Another Position - diff --git a/src/routes/InputSkills/components/AddAnotherModal/styles.module.scss b/src/routes/CreateNewTeam/components/AddAnotherModal/styles.module.scss similarity index 100% rename from src/routes/InputSkills/components/AddAnotherModal/styles.module.scss rename to src/routes/CreateNewTeam/components/AddAnotherModal/styles.module.scss diff --git a/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx b/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx new file mode 100644 index 00000000..7a19f03e --- /dev/null +++ b/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx @@ -0,0 +1,58 @@ +/** + * CircularProgressBar component. + * Displays a circular progress bar. + * Size and strokeWidth are customizable. + * Allows appending children inside bar. + */ +import React, { useEffect, useState, useRef } from "react"; +import PT from "prop-types"; + +const CircularProgressBar = ({ size, progress, children, strokeWidth }) => { + const [offset, setOffset] = useState(0); + const circleRef = useRef(null); + const center = size / 2; + const radius = size / 2 - strokeWidth / 2; + const circumference = 2 * Math.PI * radius; + useEffect(() => { + const progressOffset = ((100 - progress) / 100) * circumference; + setOffset(progressOffset); + circleRef.current.style = "transition: stroke-dashoffset 850ms ease-in-out"; + }, [setOffset, progress, circumference, offset]); + return ( + <> + + + + + {children ? children : `${progress}%`} + + + + ); +}; + +CircularProgressBar.propTypes = { + size: PT.number.isRequired, + progress: PT.number.isRequired, + children: PT.node, + strokeWidth: PT.number.isRequired, +}; + +export default CircularProgressBar; diff --git a/src/routes/InputSkills/components/CompleteProgress/index.jsx b/src/routes/CreateNewTeam/components/CompleteProgress/index.jsx similarity index 100% rename from src/routes/InputSkills/components/CompleteProgress/index.jsx rename to src/routes/CreateNewTeam/components/CompleteProgress/index.jsx diff --git a/src/routes/InputSkills/components/CompleteProgress/styles.module.scss b/src/routes/CreateNewTeam/components/CompleteProgress/styles.module.scss similarity index 100% rename from src/routes/InputSkills/components/CompleteProgress/styles.module.scss rename to src/routes/CreateNewTeam/components/CompleteProgress/styles.module.scss diff --git a/src/routes/CreateNewTeam/components/Completeness/index.jsx b/src/routes/CreateNewTeam/components/Completeness/index.jsx new file mode 100644 index 00000000..f1b73ee3 --- /dev/null +++ b/src/routes/CreateNewTeam/components/Completeness/index.jsx @@ -0,0 +1,65 @@ +/** + * Completeness Sidebar + * Shows level of completeness 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 CompleteProgress from "../CompleteProgress"; +import "./styles.module.scss"; +import IconMultipleActionsCheck from "../../../../assets/images/icon-multiple-actions-check-2.svg"; +import IconListQuill from "../../../../assets/images/icon-list-quill.svg"; + +function Completeness({ + extraStyleName, + isDisabled, + onClick, + buttonLabel, + stages, + percentage, +}) { + return ( +
+ +
    + {stages.map((stage) => ( +
  • + {stage.name} +
  • + ))} +
+ + {extraStyleName === "input-skills" ? ( + + ) : ( + + )} +
+ ); +} + +Completeness.propTypes = { + extraStyleName: PT.string, + isDisabled: PT.bool, + onClick: PT.func, + buttonLabel: PT.string, + currentStageIdx: PT.number, + stages: PT.arrayOf(PT.string), +}; + +export default Completeness; diff --git a/src/routes/InputSkills/components/Completeness/styles.module.scss b/src/routes/CreateNewTeam/components/Completeness/styles.module.scss similarity index 84% rename from src/routes/InputSkills/components/Completeness/styles.module.scss rename to src/routes/CreateNewTeam/components/Completeness/styles.module.scss index 5c44deb9..4c1e1d5a 100644 --- a/src/routes/InputSkills/components/Completeness/styles.module.scss +++ b/src/routes/CreateNewTeam/components/Completeness/styles.module.scss @@ -4,9 +4,19 @@ @include rounded-card; padding: 12px; position: relative; - background-image: linear-gradient(221.5deg, #2c95d7 0%, #9d41c9 100%); width: 250px; color: #fff; + button { + border: none; + } +} + +.input-skills { + background-image: linear-gradient(221.5deg, #2c95d7 0%, #9d41c9 100%); +} + +.role-selection { + background-image: linear-gradient(45deg, #8B41B0 0%, #EF476F 100%); } .list { @@ -43,7 +53,7 @@ content: "✓"; font-size: 9px; line-height: 14px; - padding-left: 2px; + padding-left: 3px; } } } diff --git a/src/routes/CreateNewTeam/components/LandingBox/styles.module.scss b/src/routes/CreateNewTeam/components/LandingBox/styles.module.scss index fac43b11..e37cf8e2 100644 --- a/src/routes/CreateNewTeam/components/LandingBox/styles.module.scss +++ b/src/routes/CreateNewTeam/components/LandingBox/styles.module.scss @@ -16,6 +16,9 @@ align-items: center; position: relative; z-index: 10; + button { + border: none; + } } .title { diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx new file mode 100644 index 00000000..0bde6659 --- /dev/null +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx @@ -0,0 +1,43 @@ +/** + * No Matching Profiles Result Card + * Card that appears when there are no matching profiles after searching. + */ +import React from "react"; +import { navigate } 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() { + return ( +
+
+ +

No Matching Profiles

+ + +
+
+

+ We will be looking internally for members matching your requirements + and be back at them in about 2 weeks. +

+
+

Niche Rate

+

$1,200

+

/Week

+
+ +
+
+ ); +} + +export default NoMatchingProfilesResultCard; diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss new file mode 100644 index 00000000..7334e07f --- /dev/null +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss @@ -0,0 +1,104 @@ +@import "styles/include"; + +.result-card { + @include rounded-card; + max-width: 746px; + width: 50vw; + margin-right: 30px; +} + +.heading { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + padding: 30px 0 60px 0; + margin-bottom: 30px; + color: #fff; + background-image: linear-gradient(225deg, #555555 0%, #2A2A2A 100%); + position: relative; + text-align: center; + border-radius: 8px 8px 0 0; + + svg { + margin-bottom: 8px; + g { + stroke: #fff; + } + } + + h3 { + @include font-barlow-condensed; + text-transform: uppercase; + font-size: 34px; + margin-bottom: 8px; + font-weight: 500; + } +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + padding-bottom: 61px; + p.info-txt { + @include font-roboto; + font-size: 14px; + line-height: 22px; + width: 357px; + text-align: center; + } + .niche-rate-box { + margin-top: 32px; + background-color: #FBFBFB; + padding: 30px; + border: 1px solid #F4F4F4; + border-radius: 6px; + width: 196px; + height: 164px; + p:first-child { + @include font-barlow; + margin-top: 4px; + font-size: 16px; + line-height: 20px; + font-weight: 600; + text-align: center; + text-transform: uppercase; + } + p:last-child { + @include font-roboto; + color: #555555; + font-size: 14px; + line-height: 22px; + text-align: center; + } + .cost { + @include font-barlow-condensed; + margin-top: 8px; + font-size: 48px; + line-height: 50px; + font-weight: 500; + text-align: center; + } + } + + .button { + margin-top: 62px; + } +} + +.curve { + position: absolute; + left: 0; + bottom: -70px; + width: 100%; +} + +.transparent-icon { + position: absolute; + top: -40px; + right: 10px; + opacity: 12%; + height: 142px; + width: 142px; +} diff --git a/src/routes/CreateNewTeam/components/ResultCard/index.jsx b/src/routes/CreateNewTeam/components/ResultCard/index.jsx new file mode 100644 index 00000000..910162b4 --- /dev/null +++ b/src/routes/CreateNewTeam/components/ResultCard/index.jsx @@ -0,0 +1,249 @@ +/** + * Result Card + * Card that appears after searching for + * users matching given skills. Gives information + * about costs and number of matching candidates. + */ +import React, { useState, useEffect } from "react"; +import cn from "classnames"; +import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app"; +import "./styles.module.scss"; +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 Curve from "../../../../assets/images/curve.svg"; +import CircularProgressBar from "../CircularProgressBar"; +import Button from "components/Button"; + +function ResultCard() { + const [userHandle, setUserHandle] = useState("handle"); + const [showSpecialRates, setShowSpecialRates] = useState(false); + const [showRates, setShowRates] = useState(false); + + useEffect(() => { + getAuthUserProfile().then((res) => { + setUserHandle(res.handle || "handle"); + }); + }, []); + + 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. +

+ + +
+
+ + +
+ {showRates && showSpecialRates && ( +
+

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

+
+
+
+

Full-Time

+

(40h / week)

+
+
+

Senior Member

+
+

$2,000

+

/Week

+
+
+
+

Standard Member

+
+

$1,500

+

/Week

+
+
+
+

Junior Member

+
+

$1,000

+

/Week

+
+
+
+
+
+

Part-Time

+

(30h / week)

+
+
+

Senior Member

+
+

$1,800

+

/Week

+
+
+
+

Standard Member

+
+

$1,300

+

/Week

+
+
+
+

Junior Member

+
+

$800

+

/Week

+
+
+
+
+
+

Part-Time

+

(20h / week)

+
+
+

Senior Member

+
+

$1,600

+

/Week

+
+
+
+

Standard Member

+
+

$1,100

+

/Week

+
+
+
+

Junior Member

+
+

$600

+

/Week

+
+
+
+
+
+ )} + {showRates && !showSpecialRates && ( +
+
+
+
+
Full-Time
+

(40h / week)

+
+
+
$1,800
+

/Week

+
+
+
+
+
Part-Time
+

(30h / week)

+
+
+
$1,250
+

/Week

+
+
+
+
+
Part-Time
+

(20h / week)

+
+
+
$800
+

/Week

+
+
+
+
+
+
+ +
+

Qualified candidates within

+
24h
+
+
+
+ +
+

Interviews can start within

+
48h
+
+
+
+
+ )} + {!showRates && ( +
+
+
+ +

80%

+

Matching rate

+
+ } + /> +
+
+
+ +

300+

+

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) +

+
+
+ )} +
+ ); +} + +export default ResultCard; diff --git a/src/routes/InputSkills/components/ResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss similarity index 51% rename from src/routes/InputSkills/components/ResultCard/styles.module.scss rename to src/routes/CreateNewTeam/components/ResultCard/styles.module.scss index efb4cb47..1b4e7347 100644 --- a/src/routes/InputSkills/components/ResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss @@ -5,7 +5,10 @@ max-width: 746px; width: 50vw; margin-right: 30px; - height: 80vh; +} + +.non-clickable { + pointer-events: none; } .heading { @@ -20,6 +23,7 @@ position: relative; text-align: center; border-radius: 8px 8px 0 0; + cursor: pointer; svg { margin-bottom: 8px; @@ -51,25 +55,40 @@ .content { display: flex; - flex-direction: row; + flex-direction: column; align-items: center; justify-content: center; - height: 170px; - margin-bottom: 35px; + padding-bottom: 44px; - > div { + .matching-info { display: flex; - flex-direction: column; + text-align: center; align-items: center; - justify-content: flex-start; + > div.vertical-line { + display: block; + height: 170px; + width: 1px; + background-color: #e9e9e9; + margin: 0 40px; + } + .progressbar-child { + margin-top: 5px; + width: 90px; + height: 90px; + } } - > div.vertical-line { - display: block; - height: 100%; - width: 1px; - background-color: #e9e9e9; - margin: 0 40px; + .footer { + margin-top: 35px; + display: flex; + flex-direction: column; + justify-content: flex-start; + font-size: 14px; + line-height: 26px; + + span { + font-weight: 900; + } } h4 { @@ -104,27 +123,112 @@ width: 142px; } -.footer { - text-align: center; - font-size: 14px; - line-height: 26px; - - span { - font-weight: 900; - } -} - .rate-content { display: flex; flex-direction: row; justify-content: center; align-items: center; - height: 200px; + padding-bottom: 50px; +} + +.xeno-rates { + display: flex; + flex-direction: column; + padding: 0 25px 50px 52px; + .greeting-txt { + @include font-roboto; + font-size: 14px; + line-height: 22px; + font-weight: 600; + width: 100%; + text-align: left; + } + + .rates { + display: flex; + margin-top: 24px; + .rate-info { + display: flex; + flex-direction: column; + width: 100%; + &:not(:first-child) { + margin-left: 40px; + } + + .rate-heading { + display: flex; + align-items: flex-end; + h4 { + @include font-barlow; + font-size: 16px; + line-height: 20px; + font-weight: 700; + text-transform: uppercase; + } + p { + @include font-barlow; + font-size: 14px; + line-height: 16px; + margin-left: 3px; + } + } + .senior, .standard, .junior { + display: flex; + flex-direction: column; + position: relative; + margin-top: 22px; + &::before { + content: ""; + position: absolute; + width: 3px; + height: 100%; + } + + h4 { + margin-left: 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 1px; + line-height: 16px; + color: #2A2A2A; + text-transform: uppercase; + } + .cost { + display: flex; + align-items: flex-end; + h4 { + @include font-barlow-condensed; + font-size: 34px; + font-weight: 500; + line-height: 38px; + color: #2A2A2A; + } + p { + @include font-roboto; + font-size: 14px; + line-height: 22px; + color: #555555; + margin-left: 4px; + align-items: flex-end; + } + } + } + .senior::before { + background-color: #C99014; + } + .standard::before { + background-color: #716D67; + } + .junior::before { + background-color: #854E29; + } + } + } } .vertical-line { display: block; - height: 100%; + height: 200px; width: 1px; background-color: #e9e9e9; margin: 0 40px; @@ -135,6 +239,9 @@ flex-direction: row; align-items: center; justify-content: flex-start; + &:not(:first-child) { + margin-top: 24px; + } svg { height: 36px; @@ -205,6 +312,9 @@ flex-direction: row; align-items: center; justify-content: flex-end; + &:not(:first-child) { + margin-top: 34px; + } } .rate-left-side { diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx new file mode 100644 index 00000000..17039e66 --- /dev/null +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx @@ -0,0 +1,109 @@ +/** + * Role Details Modal + * Display role details. + */ +import React, { useState, useEffect } 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", +}; + +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); + }); + }, [roleId]); + + return ( + + } + styles={{ + modal: modalStyle, + modalContainer: containerStyle, + }} + > +
+ {isLoading ? ( + <> + +
Loading...
+ + ) : ( + <> + {role && role.imageUrl && !imgError ? ( + setImgError(true)} + alt={role.name} + styleName="role-icon" + /> + ) : ( + + )} +
+ + +
+
{role?.name}
+

{role?.description}

+ + )} +
+
+ +
+
+ ); +} + +RoleDetailsModal.propTypes = { + roleId: PT.string, + open: PT.bool, + onClose: PT.func, +}; + +export default RoleDetailsModal; diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss new file mode 100644 index 00000000..81daa623 --- /dev/null +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss @@ -0,0 +1,55 @@ +@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; +} + +.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; + } + + p { + @include font-roboto; + font-size: 16px; + color: #555555; + line-height: 26px; + } +} + +.cross { + g { + stroke: #000; + } +} diff --git a/src/routes/InputSkills/components/SearchCard/index.jsx b/src/routes/CreateNewTeam/components/SearchCard/index.jsx similarity index 98% rename from src/routes/InputSkills/components/SearchCard/index.jsx rename to src/routes/CreateNewTeam/components/SearchCard/index.jsx index dbdebd5a..487f9ceb 100644 --- a/src/routes/InputSkills/components/SearchCard/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchCard/index.jsx @@ -32,7 +32,7 @@ function SearchCard() {
-

Search..

+

Search...

Matching the criteria with 1.5 million members around the world..

diff --git a/src/routes/InputSkills/components/SearchCard/styles.module.scss b/src/routes/CreateNewTeam/components/SearchCard/styles.module.scss similarity index 100% rename from src/routes/InputSkills/components/SearchCard/styles.module.scss rename to src/routes/CreateNewTeam/components/SearchCard/styles.module.scss diff --git a/src/routes/CreateNewTeam/index.jsx b/src/routes/CreateNewTeam/index.jsx index 08701d91..2ff109dc 100644 --- a/src/routes/CreateNewTeam/index.jsx +++ b/src/routes/CreateNewTeam/index.jsx @@ -18,11 +18,11 @@ import { postProject } from "services/teams"; import withAuthentication from "../../hoc/withAuthentication"; function CreateNewTeam() { - const createProject = async () => { + const createProjectAndNavigate = async (navigateTo) => { postProject() .then((res) => { const id = _.get(res, "data.id"); - navigate(`/taas/myteams/createnewteam/${id}/skills`); + navigate(`/taas/myteams/createnewteam/${id}/${navigateTo}`); }) .catch((err) => { toastr.warning("Error", "Failed to create a new team."); @@ -41,14 +41,14 @@ 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%)" - isDisabled + onClick={() => createProjectAndNavigate("role")} /> } backgroundImage="linear-gradient(221.5deg, #2C95D7 0%, #9D41C9 100%)" - onClick={createProject} + onClick={() => createProjectAndNavigate("skills")} /> { - navigate(`/taas/myteams/createnewteam/${projectId}/role`); + navigate(`/taas/myteams/createnewteam/${projectId}/roles`); }, [projectId]); const toggleSkill = useCallback( @@ -74,8 +80,10 @@ function InputSkills({ projectId }) { // mocked search for users with given skills const search = () => { setSearchState("searching"); + setCurrentStage(1, stages, setStages); searchTimer = setTimeout(() => { setSearchState("done"); + setCurrentStage(2, stages, setStages); }, 3000); }; @@ -92,22 +100,32 @@ function InputSkills({ projectId }) { />
) : searchState === "searching" ? (
- +
) : (
+ + {isOpen && ( +
+ {addedRoles.map(({ name }) => ( +
{name}
+ ))} +
+ )} +
+ ); +} + +AddedRolesAccordion.propTypes = { + addedRoles: PT.arrayOf(PT.string), +}; + +export default AddedRolesAccordion; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/styles.module.scss b/src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/styles.module.scss new file mode 100644 index 00000000..6478f1f0 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/AddedRolesAccordion/styles.module.scss @@ -0,0 +1,72 @@ +@import "styles/include"; + +.accordion { + border-radius: 8px; + box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.02); + background-color: #FFFFFF; + width: 250px; +} + +.button { + cursor: pointer; + width: 100%; + border: none; + outline: none; + border-radius: 8px; + background-color: #fff; + color: #2a2a2a; + display: flex; + text-align: left; + flex-direction: row; + justify-content: flex-start; + align-items: center; + padding: 15px 14px 10px 16px; +} + +.arrow { + display: inline-block; + height: 10px; + width: 10px; + border-bottom: 3px solid #2A2A2A; + border-right: 3px solid #2A2A2A; + margin-bottom: 4px; + &.down { + transform: rotate(45deg); + } + &.up { + transform: rotate(-135deg); + } +} + +.heading { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.title { + @include font-roboto; + font-size: 14px; + line-height: 22px; +} + +.panel { + padding: 12px 18px 14px 10px; + .role-name { + height: 40px; + width: 100%; + background-color: #F4F4F4; + border-radius: 6px; + padding: 10px; + @include font-barlow; + font-size: 16px; + line-height: 20px; + font-weight: 600; + text-transform: uppercase; + &:not(:first-child) { + margin-top: 5px; + } + } +} \ No newline at end of file diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/index.jsx new file mode 100644 index 00000000..f1a47f05 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/index.jsx @@ -0,0 +1,63 @@ +/** + * Role Item + * An item for the Role List component. + * Shows an image and the name of the role. + */ +import React, { useState, useCallback } from "react"; +import PT from "prop-types"; +import cn from "classnames"; +import FallbackIcon from "../../../../../../assets/images/icon-role-fallback.svg"; +import "./styles.module.scss"; + +function RoleItem({ + id, + name, + imageUrl, + onClick, + onDescriptionClick, + isSelected, +}) { + const [error, setError] = useState(false); + const onImgError = useCallback(() => setError(true), []); + + return ( +
onClick(id)} + > + {imageUrl && !error ? ( + {name} + ) : ( + + )} +

{name}

+ +
+ ); +} + +RoleItem.propTypes = { + id: PT.string, + name: PT.string, + onClick: PT.func, + onDescriptionClick: PT.func, + isSelected: PT.bool, +}; + +export default RoleItem; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss new file mode 100644 index 00000000..3f5b7652 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RoleItem/styles.module.scss @@ -0,0 +1,48 @@ +@import "styles/include"; + +.item-card { + border: 1px solid #d4d4d4; + border-radius: 5px; + padding: 12px 16px; + width: 213px; + height: 136px; + display: flex; + flex-direction: column; + justify-content: space-evenly; + margin: 0 0 24px 24px; + cursor: pointer; + + &.selected { + border-color: #0ab88a; + background-color: #e0faf3; + } +} + +.role-icon { + width: 42px; + height: 42px; + margin-left: 8px; +} + +.item-text { + @include font-barlow; + font-size: 16px; + font-weight: 600; + line-height: 20px; + text-transform: uppercase; +} + +.button { + font-size: 14px; + line-height: 22px; + padding: 0; + outline: none; + background: none; + color: #0D61BF; + border: none; + text-align: left; + + &:hover { + text-decoration: underline; + } +} diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/index.jsx new file mode 100644 index 00000000..88bd66f7 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/index.jsx @@ -0,0 +1,94 @@ +/** + * Roles List + * Lists all roles available to apply to a job + * and search for. Allows selecting roles 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 RoleItem from "../RoleItem"; +import { INPUT_DEBOUNCE_DELAY } from "constants/"; + +function RolesList({ roles, selectedRoleId, onDescriptionClick, toggleRole }) { + const [filteredRoles, setFilteredRoles] = useState(roles); + 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(); + setFilteredRoles( + roles.filter((role) => role.name.toLowerCase().includes(filterText)) + ); + } else { + setFilteredRoles(roles); + } + }, [debouncedFilter, roles]); + + return ( +
+ + + {filter && ( + setFilter("")} + > + X + + )} + + } + /> +
+ {filteredRoles.map(({ id, name, imageUrl }) => ( + + ))} +
+
+ ); +} + +RolesList.propTypes = { + roles: PT.array, + selectedRoleId: PT.string, + toggleRole: PT.func, +}; + +export default RolesList; diff --git a/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss b/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss new file mode 100644 index 00000000..bc9e544e --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/components/RolesList/styles.module.scss @@ -0,0 +1,67 @@ +@import "styles/include"; + +.roles-list { + @include rounded-card; + max-width: 746px; + margin-right: 20px; + position: relative; + + > 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: 35px; + font-size: 14px; + font-weight: 700; + line-height: 38px; + cursor: pointer; + &:hover { + color: rgb(216, 24, 24); + } +} + +.role-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: wrap; + margin-right: 24px; +} diff --git a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx new file mode 100644 index 00000000..a8137e94 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx @@ -0,0 +1,191 @@ +/** + * Select Role Page + * + * Gets project id 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 PT from "prop-types"; +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"; + +function SelectRole({ projectId }) { + 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 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); + }; + + 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} + /> + )} +
+ ); + } +} + +SelectRole.propTypes = { + projectId: PT.string, +}; + +export default withAuthentication(SelectRole); diff --git a/src/routes/CreateNewTeam/pages/SelectRole/styles.module.scss b/src/routes/CreateNewTeam/pages/SelectRole/styles.module.scss new file mode 100644 index 00000000..7bacc294 --- /dev/null +++ b/src/routes/CreateNewTeam/pages/SelectRole/styles.module.scss @@ -0,0 +1,14 @@ +.page { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + margin: 42px 35px; + .right-side { + display: flex; + flex-direction: column; + & > div:not(:first-child) { + margin-top: 16px; + } + } +} diff --git a/src/routes/InputSkills/components/Completeness/index.jsx b/src/routes/InputSkills/components/Completeness/index.jsx deleted file mode 100644 index 4bc91ac9..00000000 --- a/src/routes/InputSkills/components/Completeness/index.jsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Completeness Sidebar - * Shows level of completeness 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 CompleteProgress from "../CompleteProgress"; -import "./styles.module.scss"; -import IconListQuill from "../../../../assets/images/icon-list-quill.svg"; - -function Completeness({ isDisabled, onClick, buttonLabel, stage }) { - return ( -
- -
    -
  • 1 } - )} - > - Input Skills -
  • -
  • - Search Member -
  • -
  • - Overview of the Results -
  • -
- - -
- ); -} - -Completeness.propTypes = { - isDisabled: PT.bool, - onClick: PT.func, - buttonLabel: PT.string, - stage: PT.number, -}; - -export default Completeness; diff --git a/src/routes/InputSkills/components/ResultCard/index.jsx b/src/routes/InputSkills/components/ResultCard/index.jsx deleted file mode 100644 index 01a98935..00000000 --- a/src/routes/InputSkills/components/ResultCard/index.jsx +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Result Card - * Card that appears after searching for - * users matching given skills. Gives information - * about costs and number of matching candidates. - */ -import React, { useState } from "react"; -import "./styles.module.scss"; -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 Curve from "../../../../assets/images/curve.svg"; -import Button from "components/Button"; - -function ResultCard() { - const [showRates, setShowRates] = useState(false); - - return ( -
-
- -

We have matching profiles

-

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

- - -
-
- - -
- {showRates ? ( -
-
-
-
-
Full-Time
-

(40h / week)

-
-
-
$1,800
-

/Week

-
-
-
-
-
Part-Time
-

(30h / week)

-
-
-
$1,250
-

/Week

-
-
-
-
-
Part-Time
-

(20h / week)

-
-
-
$800
-

/Week

-
-
-
-
-
-
- -
-

Qualified candidates within

-
24h
-
-
-
- -
-

Interviews can start within

-
48h
-
-
-
-
- ) : ( - <> -
-
-

80%

-

Matching rate

-
-
-
- -

300+

-

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) -

-
- - )} -
- ); -} - -export default ResultCard; diff --git a/src/routes/SelectRole/index.jsx b/src/routes/SelectRole/index.jsx deleted file mode 100644 index 5f6fa502..00000000 --- a/src/routes/SelectRole/index.jsx +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Select Role - * Page for selecting a role to add to your team - */ - -import React from "react"; - -function SelectRole() { - return

Select A Role

; -} - -export default SelectRole; diff --git a/src/services/roles.js b/src/services/roles.js new file mode 100644 index 00000000..f3d154ae --- /dev/null +++ b/src/services/roles.js @@ -0,0 +1,94 @@ +/** + * Topcoder TaaS Service for Roles + */ + +const mockRoles = [ + { + id: "78c1d981-f235-4a75-97fb-693a26d2a56d", + name: "Python Engineer", + description: "Python Engineer Description", + listOfSkills: [], + imageUrl: "https://svgur.com/i/XdC.svg", + }, + { + id: "bf1011c1-336a-4ec4-aa07-a916d648dbb4", + name: "Android Developer", + description: "Android Developer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/Xe_.svg", + }, + { + id: "532433a6-505d-41fe-beb2-815e9b999077", + name: "SQL Engineer", + description: "SQL Engineer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/Xe2.svg", + }, + { + id: "3f5ee777-7221-4fe8-ba3a-11cf09077d3f", + name: ".NET Developer", + description: ".NET Developer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/XeW.svg", + }, + { + id: "94653ab7-9e48-4ef2-b5c3-cbd720f134ad", + name: "C# Developer", + description: "C# Developer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/XeV.svg", + }, + { + id: "b663d69b-fd56-4ab6-8ef6-3560c4ad0be3", + name: "Angular Developer", + description: "Angular Developer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/XeX.svg", + }, + { + id: "e1f25908-664f-4956-a069-08017064b4be", + name: "Ajax Developer", + description: "Ajax Developer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/Xeu.svg", + }, + { + id: "8d7f43da-d126-4f53-8146-7d874fb44bff", + name: "API Developer", + description: "API Developer Description", + listOfSkills: [], + imageUrl: "http://svgur.com/i/Xcr.svg", + }, + { + id: "98895ea0-f9f8-4abb-ae6b-b00567eafa93", + name: "Python Engineer", + description: "Python Engineer Description", + listOfSkills: [], + imageUrl: "https://svgur.com/i/XdC.svg", + }, + { + id: "212de943-944f-40a1-871a-af384f462644", + name: "Role FallbackIcon", + description: "Role for FallbackIcon Description", + listOfSkills: [], + imageUrl: "https://svgur.com/i/XdC-nonexisting.svg", + }, +]; + +/** + * Mock API request. Returns a list of mock roles. + */ +export function getRoles() { + return new Promise((resolve) => + setTimeout(resolve, 1500, { data: mockRoles }) + ); +} + +/** + * Mock API request. Returns a single role. + */ +export function getRoleById(id) { + return new Promise((resolve) => + setTimeout(resolve, 1500, { data: mockRoles.find((r) => r.id === id) }) + ); +} diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 53f24cd2..5de1d653 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -38,3 +38,27 @@ export const getSelectOptionByValue = (value, selectOptions) => { return option; }; + +/** + * Activates the current step in the Completion Box. + * + * * Set `isCurrent: true` of the current step. + * * Set `isCurrent: false` for all other steps. + * * Set `complete: true` for steps prior to the current. + * * Set `complete: false` for the steps after the current. + * + * @param {Number} currentStepIdx 0-based index of the current step + * @param {Array} stages stages array + * @param {Function} setStagesCallback `setStages` callback to update state + */ +export const setCurrentStage = (currentStepIdx, stages, setStagesCallback) => { + setStagesCallback([ + ...stages + .slice(0, currentStepIdx) + .map((s) => ({ ...s, completed: true, isCurrent: false })), + { ...stages[currentStepIdx], isCurrent: true, completed: false }, + ...stages + .slice(currentStepIdx + 1) + .map((s) => ({ ...s, completed: false, isCurrent: false })), + ]); +};