diff --git a/package-lock.json b/package-lock.json index c5832bbb..da18caae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1249,6 +1249,25 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "@hypnosphi/create-react-context": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + }, + "dependencies": { + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -14434,24 +14453,24 @@ } }, "react-datepicker": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-3.4.1.tgz", - "integrity": "sha512-ASyVb7UmVx1vzeITidD7Cr/EXRXhKyjjbSkBndPc1MipYq4rqQ3eMFgvRQzpsXc3JmIMFgICm7nmN6Otc1GE/Q==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-3.8.0.tgz", + "integrity": "sha512-iFVNEp8DJoX5yEvEiciM7sJKmLGrvE70U38KhpG13XrulNSijeHw1RZkhd/0UmuXR71dcZB/kdfjiidifstZjw==", "requires": { "classnames": "^2.2.6", "date-fns": "^2.0.1", "prop-types": "^15.7.2", - "react-onclickoutside": "^6.9.0", - "react-popper": "^1.3.4" + "react-onclickoutside": "^6.10.0", + "react-popper": "^1.3.8" }, "dependencies": { "react-popper": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", - "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", + "integrity": "sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==", "requires": { "@babel/runtime": "^7.1.2", - "create-react-context": "^0.3.0", + "@hypnosphi/create-react-context": "^0.3.1", "deep-equal": "^1.1.1", "popper.js": "^1.14.4", "prop-types": "^15.6.1", @@ -14548,9 +14567,9 @@ } }, "react-onclickoutside": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.10.0.tgz", - "integrity": "sha512-7i2L3ef+0ILXpL6P+Hg304eCQswh4jl3ynwR71BSlMU49PE2uk31k8B2GkP6yE9s2D4jTGKnzuSpzWxu4YxfQQ==" + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz", + "integrity": "sha512-640486eSwU/t5iD6yeTlefma8dI3bxPXD93hM9JGKyYITAd0P1JFkkcDeyHZRqNpY/fv1YW0Fad9BXr44OY8wQ==" }, "react-outside-click-handler": { "version": "1.3.0", diff --git a/package.json b/package.json index 4215d91d..61cd6967 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "prop-types": "^15.7.2", "react": "^16.12.0", "react-avatar": "^3.9.7", - "react-datepicker": "^3.4.1", + "react-datepicker": "^3.8.0", "react-dom": "^16.12.0", "react-final-form": "^6.5.2", "react-final-form-arrays": "^3.1.3", diff --git a/src/assets/images/icon-information-circle.svg b/src/assets/images/icon-information-circle.svg new file mode 100644 index 00000000..2047ab02 --- /dev/null +++ b/src/assets/images/icon-information-circle.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/assets/images/icon-thick-calendar.svg b/src/assets/images/icon-thick-calendar.svg new file mode 100644 index 00000000..222202ca --- /dev/null +++ b/src/assets/images/icon-thick-calendar.svg @@ -0,0 +1,23 @@ + + + 89C4F8D6-2320-499E-BC02-40DE84B9F35C + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/FormField/index.jsx b/src/components/FormField/index.jsx index f43d48fc..c7de9520 100644 --- a/src/components/FormField/index.jsx +++ b/src/components/FormField/index.jsx @@ -82,6 +82,7 @@ const FormField = ({ field }) => { onBlur={input.onBlur} onFocus={input.onFocus} className={meta.error && meta.touched ? "error" : ""} + maxLength={field.maxLength} /> )} {field.type === FORM_FIELD_TYPE.DATE && ( diff --git a/src/components/InformationTooltip/index.jsx b/src/components/InformationTooltip/index.jsx new file mode 100644 index 00000000..996168a2 --- /dev/null +++ b/src/components/InformationTooltip/index.jsx @@ -0,0 +1,87 @@ +/** + * Information Tooltip + * Icon which reveals a tooltip on mouse hover + */ +import React, { useCallback, useState } from "react"; +import PT from "prop-types"; +import { usePopper } from "react-popper"; +import IconInformationCircle from "../../assets/images/icon-information-circle.svg"; +import "./styles.module.scss"; + +function InformationTooltip({ text, iconSize = "16px" }) { + const [isTooltipShown, setIsTooltipShown] = useState(false); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [arrowElement, setArrowElement] = useState(null); + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "top", + modifiers: [ + { + name: "flip", + options: { + fallbackPlacements: ["top"], + }, + }, + { + name: "offset", + options: { + // use offset to move the tooltip slightly up + offset: [0, 12], + }, + }, + { + name: "arrow", + // padding should be equal to border-radius of the tooltip + options: { element: arrowElement, padding: 5 }, + }, + ], + }); + + const showTooltip = useCallback(() => { + setIsTooltipShown(true); + }, []); + + const hideTooltip = useCallback(() => { + setIsTooltipShown(false); + }, []); + + const iconStyle = { + width: iconSize, + height: iconSize, + }; + + return ( +
+
+ +
+ {isTooltipShown && ( +
+
{text}
+
+
+ )} +
+ ); +} + +InformationTooltip.propTypes = { + iconSize: PT.string, + text: PT.string, +}; + +export default InformationTooltip; diff --git a/src/components/InformationTooltip/styles.module.scss b/src/components/InformationTooltip/styles.module.scss new file mode 100644 index 00000000..bf1084c1 --- /dev/null +++ b/src/components/InformationTooltip/styles.module.scss @@ -0,0 +1,34 @@ +@import "styles/include"; + +.circle { + * { + fill: #aaa; + } +} + +.tooltip { + border-radius: 5px; + box-shadow: 0px 5px 25px #c6c6c6; + width: 165px; + z-index: 1000; +} + +.tooltip-arrow { + bottom: -9px; + border-style: solid; + border-width: 10px 8px 0 8px; + border-color: #2a2a2a transparent transparent transparent; + height: 0; + width: 0; +} + +.tooltip-content { + border-radius: 5px; + @include font-roboto; + font-size: 12px; + line-height: 16px; + font-weight: 400; + color: #fff; + background-color: #2a2a2a; + padding: 10px; +} \ No newline at end of file diff --git a/src/components/MarkdownEditor/index.jsx b/src/components/MarkdownEditor/index.jsx index 6c38b851..83939222 100644 --- a/src/components/MarkdownEditor/index.jsx +++ b/src/components/MarkdownEditor/index.jsx @@ -53,6 +53,9 @@ const MarkdownEditor = (props) => { ]} plugins={[]} /> + {props.errorMessage && ( +
{props.errorMessage}
+ )}
); }; diff --git a/src/components/MarkdownEditor/styles.module.scss b/src/components/MarkdownEditor/styles.module.scss index d444b248..13c0aa00 100644 --- a/src/components/MarkdownEditor/styles.module.scss +++ b/src/components/MarkdownEditor/styles.module.scss @@ -12,21 +12,6 @@ overflow-y: auto; background: #fafafb; } - .message { - @include font-roboto; - - width: 100%; - text-align: center; - min-height: 40px; - line-height: 20px; - padding: 9px 10px; - margin: 10px 0 5px; - font-size: 14px; - color: #ff5b52; - border: 1px solid #ffd5d1; - cursor: auto; - outline: none; - } } .editor-container { :global { @@ -72,3 +57,19 @@ } } } + +.message { + @include font-roboto; + + width: 100%; + text-align: center; + min-height: 40px; + line-height: 20px; + padding: 9px 10px; + margin: 10px 0 5px; + font-size: 14px; + color: #ff5b52; + border: 1px solid #ffd5d1; + cursor: auto; + outline: none; +} \ No newline at end of file diff --git a/src/components/MonthPicker/index.jsx b/src/components/MonthPicker/index.jsx new file mode 100644 index 00000000..9b1f1b85 --- /dev/null +++ b/src/components/MonthPicker/index.jsx @@ -0,0 +1,45 @@ +/** + * Month Picker + * An styled input component for selecting date by month. + * Compatible with react-final-form + */ +import React from "react"; +import PT from "prop-types"; +import DatePicker from "react-datepicker"; +import "./styles.module.scss"; + +function getCurrMonthYear() { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + return new Date(`${year}-${month + 1}`); +} + +function MonthPicker({ name, value, onChange, onBlur, onFocus }) { + return ( +
+ +
+ ); +} + +MonthPicker.propTypes = { + name: PT.string, + value: PT.any, + onChange: PT.func, + onBlur: PT.func, +}; + +export default MonthPicker; diff --git a/src/components/MonthPicker/styles.module.scss b/src/components/MonthPicker/styles.module.scss new file mode 100644 index 00000000..ba93e757 --- /dev/null +++ b/src/components/MonthPicker/styles.module.scss @@ -0,0 +1,60 @@ +@import "styles/include"; + +.month-picker { + input { + width: 118px; + background-image: url("../../assets/images/icon-thick-calendar.svg"); + background-repeat: no-repeat; + background-position: 95px 7px; + } + + :global { + .react-datepicker__header { + @include font-barlow; + font-size: 20px; + color: #2a2a2a; + font-weight: 600; + padding-top: 14px; + padding-bottom: 14px; + background-color: transparent; + border-bottom: 1px solid #d4d4d4; + margin: 0 16px; + } + + .react-datepicker__month-text { + @include font-roboto; + font-size: 14px; + font-weight: 400; + line-height: 40px; + width: 40px; + height: 40px; + margin: 8px; + border-radius: 100%; + } + .react-datepicker__month--selected { + border-radius: 100%; + background-color: #0AB88A; + } + + .react-datepicker__navigation { + top: 18px; + border: 2px solid transparent; + height: 12px; + width: 12px; + } + .react-datepicker__navigation--next, .react-datepicker__navigation--previous { + border-right-color: transparent; + border-bottom-color: transparent; + border-left-color: #137D60; + border-top-color: #137D60; + } + .react-datepicker__navigation--next { + right: 24px; + transform: rotate(135deg); + } + .react-datepicker__navigation--previous { + left: 24px; + transform: rotate(-45deg); + } + } +} \ No newline at end of file diff --git a/src/components/NumberInput/index.jsx b/src/components/NumberInput/index.jsx new file mode 100644 index 00000000..3456308f --- /dev/null +++ b/src/components/NumberInput/index.jsx @@ -0,0 +1,56 @@ +/** + * Number Input + * A custom number input to be used in forms + * Removed default buttons and adds custom buttons + */ +import React from "react"; +import PT from "prop-types"; +import cn from "classnames"; +import "./styles.module.scss"; + +function NumberInput({ name, value, onChange, onBlur, onFocus, min, error }) { + const incrementVal = (step) => { + const newVal = +value + step; + if (typeof newVal === "number" && !isNaN(newVal)) { + onChange(newVal); + } + }; + const decrementVal = (step) => { + const newVal = value - step; + if (newVal >= min) { + onChange(value - step); + } + }; + + return ( +
+ + + +
+ ); +} + +NumberInput.propTypes = { + name: PT.string, + value: PT.string, + onChange: PT.func, + onFocus: PT.func, + onBlur: PT.func, + min: PT.string, + error: PT.bool, +}; + +export default NumberInput; diff --git a/src/components/NumberInput/styles.module.scss b/src/components/NumberInput/styles.module.scss new file mode 100644 index 00000000..e5941c56 --- /dev/null +++ b/src/components/NumberInput/styles.module.scss @@ -0,0 +1,43 @@ +@import "styles/include"; + +// remove default buttons and style input as text input +.input { + appearance: textfield; + text-align: center; + + &.error { + border-color: #fe665d; + } +} +.input::-webkit-inner-spin-button, +.input::-webkit-outer-spin-button { + appearance: none; + margin: 0; +} + +.container { + position: relative; + width: fit-content; +} + +.left-button, +.right-button { + @include font-roboto; + font-size: 22px; + color: #137D60; + outline: none; + border: none; + background: none; + position: absolute; + top: 0; + height: 100%; + padding: 0 11px; +} + +.left-button { + left: 0; +} + +.right-button { + right: 0; +} \ No newline at end of file diff --git a/src/components/TextArea/index.jsx b/src/components/TextArea/index.jsx index 8ec9e7e5..fc247056 100644 --- a/src/components/TextArea/index.jsx +++ b/src/components/TextArea/index.jsx @@ -18,6 +18,7 @@ function TextArea(props) { autoFocus={props.autoFocus} onBlur={props.onBlur} onFocus={props.onFocus} + maxLength={props.maxLength} /> ); } diff --git a/src/constants/index.js b/src/constants/index.js index 5bc6228f..46a60d98 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -257,7 +257,7 @@ export const ACTION_TYPE = { CLEAR_SEARCHED_ROLES: "CLEAR_SEARCHED_ROLES", ADD_SEARCHED_ROLE: "ADD_SEARCHED_ROLE", ADD_ROLE_SEARCH_ID: "ADD_ROLE_SEARCH_ID", - REPLACE_SEARCHED_ROLES: "REPLACE_SEARCHED_ROLES", + DELETE_SEARCHED_ROLE: "DELETE_SEARCHED_ROLE", }; /** @@ -357,6 +357,6 @@ export const INTERVIEW_POPUP_MEDIA_URL = export const MAX_ALLOWED_INTERVIEWS = 3; /** - * Matching rate to show in CreateNewTeam ResultCard + * Custom role names to remove from RoleList component */ -export const MATCHING_RATE = "80"; +export const CUSTOM_ROLE_NAMES = ["custom", "niche"]; diff --git a/src/root.component.jsx b/src/root.component.jsx index f1e5c3b7..bdd3b79b 100644 --- a/src/root.component.jsx +++ b/src/root.component.jsx @@ -25,7 +25,7 @@ export default function Root() { - + @@ -34,9 +34,9 @@ export default function Root() { - - - + + + {/* Global config for Toastr popups */} diff --git a/src/routes/CreateNewTeam/actions/index.js b/src/routes/CreateNewTeam/actions/index.js index bb78565f..a67e6569 100644 --- a/src/routes/CreateNewTeam/actions/index.js +++ b/src/routes/CreateNewTeam/actions/index.js @@ -22,9 +22,9 @@ const addPreviousSearchId = (id) => ({ payload: id, }); -export const replaceSearchedRoles = (roles) => ({ - type: ACTION_TYPE.REPLACE_SEARCHED_ROLES, - payload: { roles, lastRoleId: roles[roles.length - 1].searchId }, +const deleteRole = (id) => ({ + type: ACTION_TYPE.DELETE_SEARCHED_ROLE, + payload: id, }); export const clearSearchedRoles = () => (dispatch, getState) => { @@ -41,3 +41,8 @@ export const addRoleSearchId = (id) => (dispatch, getState) => { dispatch(addPreviousSearchId(id)); updateLocalStorage(getState().searchedRoles); }; + +export const deleteSearchedRole = (id) => (dispatch, getState) => { + dispatch(deleteRole(id)); + updateLocalStorage(getState().searchedRoles); +}; diff --git a/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx b/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx index 2c1d9a6e..ece5e080 100644 --- a/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx +++ b/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx @@ -19,7 +19,8 @@ function AddedRolesAccordion({ addedRoles }) {

{addedRoles.length}{" "} - {addedRoles.length > 1 ? "roles have" : "role has"} been added. + {addedRoles.length > 1 ? "positions have" : "position has"} been + added.

diff --git a/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx b/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx index 7a19f03e..06ea736c 100644 --- a/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx +++ b/src/routes/CreateNewTeam/components/CircularProgressBar/index.jsx @@ -14,7 +14,7 @@ const CircularProgressBar = ({ size, progress, children, strokeWidth }) => { const radius = size / 2 - strokeWidth / 2; const circumference = 2 * Math.PI * radius; useEffect(() => { - const progressOffset = ((100 - progress) / 100) * circumference; + const progressOffset = (1 - progress) * circumference; setOffset(progressOffset); circleRef.current.style = "transition: stroke-dashoffset 850ms ease-in-out"; }, [setOffset, progress, circumference, offset]); diff --git a/src/routes/CreateNewTeam/components/ItemList/index.jsx b/src/routes/CreateNewTeam/components/ItemList/index.jsx index 12033b13..56b6496a 100644 --- a/src/routes/CreateNewTeam/components/ItemList/index.jsx +++ b/src/routes/CreateNewTeam/components/ItemList/index.jsx @@ -37,7 +37,7 @@ function ItemList({
header { padding: 16px 24px; @@ -33,9 +33,9 @@ input:not([type="checkbox"]).filter-input { color: #2a2a2a; font-size: 14px; height: 40px; - line-height: 38px; + line-height: normal; outline: none; - padding: 0 15px; + padding: 8px 15px; &:not(:focus) { background-image: url("../../../../assets/images/icon-search.svg"); diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx index 2b1fd779..4daee929 100644 --- a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx @@ -4,12 +4,14 @@ */ import React from "react"; import { Link } from "@reach/router"; +import PT from "prop-types"; 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"; +import { formatMoney } from "utils/format"; -function NoMatchingProfilesResultCard() { +function NoMatchingProfilesResultCard({ role }) { return (
@@ -23,12 +25,20 @@ function NoMatchingProfilesResultCard() { We will be looking internally for members matching your requirements and be back at them in about 2 weeks.

-
-

Niche Rate

-

$1,200

-

/Week

-
- + {role.rates && role.name ? ( +
+

{role.name} Rate

+

{formatMoney(role.rates[0].global)}

+

/Week

+
+ ) : ( +
+

Custom Rate

+

$1,200

+

/Week

+
+ )} + @@ -38,4 +48,8 @@ function NoMatchingProfilesResultCard() { ); } +NoMatchingProfilesResultCard.propTypes = { + role: PT.object, +}; + export default NoMatchingProfilesResultCard; diff --git a/src/routes/CreateNewTeam/components/ResultCard/index.jsx b/src/routes/CreateNewTeam/components/ResultCard/index.jsx index c6920c22..07f21c13 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/ResultCard/index.jsx @@ -15,7 +15,6 @@ 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 formatRate(value) { @@ -23,18 +22,27 @@ function formatRate(value) { return formatMoney(value); } +function formatPercent(value) { + return `${Math.round(value * 100)}%`; +} + function ResultCard({ role }) { const { numberOfMembersAvailable, isExternalMember, + skillsMatch, rates: [rates], + jobTitle, + name, + timeToCandidate, + timeToInterview, } = role; const [userHandle, setUserHandle] = useState(null); const [showRates, setShowRates] = useState(false); useEffect(() => { getAuthUserProfile().then((res) => { - setUserHandle(res.handle || null); + setUserHandle(res?.handle || null); }); }, []); @@ -44,12 +52,15 @@ function ResultCard({ role }) {

We have matching profiles

- We have qualified candidates who match {MATCHING_RATE}% or more of - your job requirements. + We have qualified candidates who match {formatPercent(skillsMatch)} + {skillsMatch < 1 ? " or more " : " "} of your job requirements.

+

+ {jobTitle && jobTitle.length ? jobTitle : name} +

Interviews can start within

-
48h
+
{timeToInterview}h
@@ -217,12 +228,12 @@ function ResultCard({ role }) {
-

{MATCHING_RATE}%

-

Matching rate

+

{formatPercent(skillsMatch)}

+

Skills Match

} /> diff --git a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss index 95ec69ae..6babd0a1 100644 --- a/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/ResultCard/styles.module.scss @@ -13,7 +13,7 @@ justify-content: flex-start; align-items: center; padding: 30px 0 60px 0; - margin-bottom: 30px; + margin-bottom: 14px; color: #fff; background-image: linear-gradient(225deg, #0ab88a 0%, #137d60 100%); position: relative; @@ -40,6 +40,18 @@ } } +.job-title { + @include font-barlow; + font-size: 22px; + margin-bottom: 18px; + font-weight: 600; + text-align: center; + text-transform: uppercase; + // position text over bottom of header image + position: relative; + z-index: 100; +} + .button-group { display: flex; flex-direction: row; diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx index a10f2de7..153a8a96 100644 --- a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx @@ -48,7 +48,7 @@ function RoleDetailsModal({ roleId, open, onClose }) { [role, imgError] ); - const skills = role ? role.listOfSkills : []; + const skills = role && role.listOfSkills ? role.listOfSkills : []; const hideSkills = () => { onClose(); @@ -90,19 +90,21 @@ function RoleDetailsModal({ roleId, open, onClose }) { Skills
- {showSkills ? ( - - ) : ( -
- -
- )} +
+ {showSkills ? ( + + ) : ( +
+ +
+ )} +
); diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss index e7470ce5..8aa70eac 100644 --- a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss +++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss @@ -14,12 +14,23 @@ } .markdown-container { - // not adds specificity to override style - p:not(table) { - @include font-roboto; - color: #2a2a2a; - font-size: 16px; - line-height: 26px; + :global { + // resets styles in markdown-viewer + .tui-editor-contents { + @include font-roboto; + color: #2a2a2a; + font-size: 16px; + line-height: 26px; + ul { + list-style: initial; + >li { + margin-bottom: 10px; + &::before { + background: none; + } + } + } + } } } @@ -47,3 +58,8 @@ margin-bottom: 10px; font-size: 12px; } + +.content { + height: 180px; + overflow-y: auto; +} diff --git a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx index b801e2d1..778fd984 100644 --- a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx @@ -1,23 +1,71 @@ import { Router } from "@reach/router"; -import React from "react"; -import { useSelector } from "react-redux"; +import _ from "lodash"; +import React, { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { searchRoles } from "services/teams"; +import { isCustomRole, setCurrentStage } from "utils/helpers"; +import { addRoleSearchId, addSearchedRole } from "../../actions"; import SearchContainer from "../SearchContainer"; import SubmitContainer from "../SubmitContainer"; function SearchAndSubmit(props) { + const { stages, setStages, searchObject, onClick } = props; + + const [searchState, setSearchState] = useState(null); + const [matchingRole, setMatchingRole] = useState(null); + const { addedRoles, previousSearchId } = useSelector( (state) => state.searchedRoles ); + const dispatch = useDispatch(); + + const search = useCallback(() => { + setCurrentStage(1, stages, setStages); + setSearchState("searching"); + setMatchingRole(null); + const searchObjectCopy = { ...searchObject }; + if (previousSearchId) { + searchObjectCopy.previousRoleSearchRequestId = previousSearchId; + } + searchRoles(searchObjectCopy) + .then((res) => { + const name = _.get(res, "data.name"); + const searchId = _.get(res, "data.roleSearchRequestId"); + if (name && !isCustomRole({ name })) { + dispatch(addSearchedRole({ searchId, name })); + } else if (searchId) { + dispatch(addRoleSearchId(searchId)); + } + setMatchingRole(res.data); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setCurrentStage(2, stages, setStages); + setSearchState("done"); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, previousSearchId, searchObject]); + return ( + - ); } diff --git a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx index 8e91bc3b..a79c5bbc 100644 --- a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx @@ -5,73 +5,37 @@ * search pages. Contains logic and supporting * components for searching for roles. */ -import React, { useCallback, useState } from "react"; +import React, { useCallback } from "react"; import PT from "prop-types"; -import _ from "lodash"; -import { useDispatch } from "react-redux"; import AddedRolesAccordion from "../AddedRolesAccordion"; import Completeness from "../Completeness"; import SearchCard from "../SearchCard"; import ResultCard from "../ResultCard"; import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; -import { searchRoles } from "services/teams"; -import { setCurrentStage } from "utils/helpers"; -import { addRoleSearchId, addSearchedRole } from "../../actions"; +import { isCustomRole } from "utils/helpers"; import "./styles.module.scss"; function SearchContainer({ stages, - setStages, isCompletenessDisabled, toRender, - searchObject, + onClick, + search, completenessStyle, navigate, addedRoles, - previousSearchId, + searchState, + matchingRole, }) { - const [searchState, setSearchState] = useState(null); - const [matchingRole, setMatchingRole] = useState(null); - - const dispatch = useDispatch(); - const onSubmit = useCallback(() => { - navigate("result", { state: { matchingRole } }); - }, [navigate, matchingRole]); - - const search = () => { - setCurrentStage(1, stages, setStages); - setSearchState("searching"); - setMatchingRole(null); - const searchObjectCopy = { ...searchObject }; - if (previousSearchId) { - searchObjectCopy.previousRoleSearchRequestId = previousSearchId; - } - searchRoles(searchObjectCopy) - .then((res) => { - const name = _.get(res, "data.name"); - const searchId = _.get(res, "data.roleSearchRequestId"); - if (name && !name.toLowerCase().includes("niche")) { - setMatchingRole(res.data); - dispatch(addSearchedRole({ searchId, name })); - } else if (searchId) { - dispatch(addRoleSearchId(searchId)); - } - }) - .catch((err) => { - console.error(err); - }) - .finally(() => { - setCurrentStage(2, stages, setStages); - setSearchState("done"); - }); - }; + navigate("result"); + }, [navigate]); const renderLeftSide = () => { - if (!searchState) return toRender; + if (!searchState) return toRender(search); if (searchState === "searching") return ; - if (matchingRole) return ; - return ; + if (!isCustomRole(matchingRole)) return ; + return ; }; const getPercentage = useCallback(() => { @@ -90,9 +54,9 @@ function SearchContainer({ isDisabled={ isCompletenessDisabled || searchState === "searching" || - (searchState === "done" && !matchingRole) + (searchState === "done" && (!addedRoles || !addedRoles.length)) } - onClick={searchState ? onSubmit : search} + onClick={searchState ? onSubmit : onClick ? onClick : search} extraStyleName={completenessStyle} buttonLabel={searchState ? "Submit Request" : "Search"} stages={stages} @@ -105,14 +69,15 @@ function SearchContainer({ SearchContainer.propTypes = { stages: PT.array, - setStages: PT.func, isCompletenessDisabled: PT.bool, - searchObject: PT.object, - toRender: PT.node, + onClick: PT.func, + search: PT.func, + toRender: PT.func, completenessStyle: PT.string, navigate: PT.func, addedRoles: PT.array, - previousSearchId: PT.string, + searchState: PT.string, + matchingRole: PT.object, }; export default SearchContainer; diff --git a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx index 55c493bb..aacd05e4 100644 --- a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx @@ -23,7 +23,7 @@ import TeamDetailsModal from "../TeamDetailsModal"; import ConfirmationModal from "../ConfirmationModal"; import withAuthentication from "../../../../hoc/withAuthentication"; import "./styles.module.scss"; -import { setCurrentStage } from "utils/helpers"; +import { isCustomRole, setCurrentStage } from "utils/helpers"; import { clearSearchedRoles } from "../../actions"; import { postTeamRequest } from "services/teams"; import SuccessCard from "../SuccessCard"; @@ -32,11 +32,9 @@ function SubmitContainer({ stages, setStages, completenessStyle, - location, + matchingRole, addedRoles, }) { - const matchingRole = location?.state?.matchingRole; - const [addAnotherOpen, setAddAnotherOpen] = useState(true); const [teamDetailsOpen, setTeamDetailsOpen] = useState(false); const [teamObject, setTeamObject] = useState(null); @@ -46,20 +44,16 @@ function SubmitContainer({ useEffect(() => { setCurrentStage(2, stages, setStages); - if (!addedRoles || addedRoles.length === 0) { - navigate("/taas/myteams/createnewteam"); - } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // redirects user if they enter the page URL directly - // without adding any roles. + // without adding any roles or delete all roles. useLayoutEffect(() => { if (!addedRoles || addedRoles.length === 0) { - navigate("/taas/myteams/createnewteam"); + navigate("/taas/createnewteam"); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [addedRoles]); const openTeamDetails = () => { setAddAnotherOpen(false); @@ -67,7 +61,7 @@ function SubmitContainer({ }; const addAnother = () => { - navigate("/taas/myteams/createnewteam"); + navigate("/taas/createnewteam"); }; const assembleTeam = (formData) => { @@ -78,13 +72,14 @@ function SubmitContainer({ if (key === "teamName" || key === "teamDescription") { continue; } - const position = _.pick( - formData[key], - "numberOfResources", - "durationWeeks", - "startMonth" + const position = _.mapValues(formData[key], (val, key) => + key === "startMonth" ? val : parseInt(val, 10) ); + if (position.startMonth === null) { + delete position.startMonth; + } + position.roleSearchRequestId = key; position.roleName = addedRoles.find((role) => role.searchId === key).name; @@ -99,10 +94,9 @@ function SubmitContainer({ const requestTeam = useCallback(() => { setRequestLoading(true); postTeamRequest(teamObject) - .then((res) => { - const projectId = _.get(res, ["data", "projectId"]); + .then(() => { dispatch(clearSearchedRoles()); - navigate(`/taas/myteams/${projectId}`); + navigate("/taas/myteams"); }) .catch((err) => { setRequestLoading(false); @@ -112,7 +106,11 @@ function SubmitContainer({ return (
- {matchingRole ? : } + {!isCustomRole(matchingRole) ? ( + + ) : ( + + )}

We have matching profiles

-

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

+

We have qualified candidates who match your job requirements.

@@ -30,7 +26,7 @@ function SuccessCard() { Please use the button to the right to submit your request, or the button below to search for additional roles.

- + diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx index 716d375d..11802f60 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx @@ -6,18 +6,24 @@ import React, { useState } from "react"; import PT from "prop-types"; import { Form, Field, useField } from "react-final-form"; +import { useDispatch } from "react-redux"; import FormField from "components/FormField"; import BaseCreateModal from "../BaseCreateModal"; import { FORM_FIELD_TYPE } from "constants/"; import { formatPlural } from "utils/format"; import Button from "components/Button"; +import MonthPicker from "components/MonthPicker"; +import InformationTooltip from "components/InformationTooltip"; +import { deleteSearchedRole } from "../../actions"; +import IconCrossLight from "../../../../assets/images/icon-cross-light.svg"; import "./styles.module.scss"; +import NumberInput from "components/NumberInput"; const Error = ({ name }) => { const { - meta: { touched, error }, - } = useField(name, { subscription: { touched: true, error: true } }); - return touched && error ? {error} : null; + meta: { dirty, error }, + } = useField(name, { subscription: { dirty: true, error: true } }); + return dirty && error ? {error} : null; }; function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { @@ -30,6 +36,8 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { return roles; }); + const dispatch = useDispatch(); + const toggleDescription = () => { setShowDescription((prevState) => !prevState); }; @@ -95,10 +103,21 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { return (
{ + changeValue(state, fieldName, () => undefined); + }, + }} initialValues={{ teamName: "My Great Team" }} validate={validator} > - {({ handleSubmit, hasValidationErrors }) => { + {({ + handleSubmit, + hasValidationErrors, + form: { + mutators: { clearField }, + }, + }) => { return ( @@ -134,13 +153,16 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { name: "teamDescription", label: "Short description about the team/ project", placeholder: "Short description about the team/ project", - maxLength: 1000, + maxLength: 600, }} /> )} +
+ + +
)} + + + ))} diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss index eb6080c6..80ced5a3 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/styles.module.scss @@ -8,6 +8,7 @@ font-size: 12px; font-weight: 500; color: #137D60; + padding: 1px 6px 0 6px; &.toggle-description { margin-top: 12px; @@ -42,20 +43,31 @@ color: #2a2a2a; border-bottom: 1px solid #e9e9e9; + &:last-child { + padding-right: 0; + } + input { @include font-roboto; font-size: 14px; - line-height: 22px; + line-height: normal; height: 34px; - width: 98px; - &[type="month"] { - width: 170px; + &[type="number"] { + width: 98px; } } } } } +.flex-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + width: 118px; +} + .error { font-size: 14px; font-weight: 400; @@ -63,8 +75,20 @@ display: block; } +.delete-role { + border: none; + background: none; + + &:hover { + g { + stroke: red; + } + } +} + .modal-body { + overflow-x: auto; textarea { height: 95px; } -} \ No newline at end of file +} diff --git a/src/routes/CreateNewTeam/index.jsx b/src/routes/CreateNewTeam/index.jsx index efd722e5..3ca8343d 100644 --- a/src/routes/CreateNewTeam/index.jsx +++ b/src/routes/CreateNewTeam/index.jsx @@ -31,21 +31,21 @@ 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={() => goToRoute("/taas/myteams/createnewteam/role")} + onClick={() => goToRoute("/taas/createnewteam/role")} /> } backgroundImage="linear-gradient(221.5deg, #2C95D7 0%, #9D41C9 100%)" - onClick={() => goToRoute("/taas/myteams/createnewteam/skills")} + onClick={() => goToRoute("/taas/createnewteam/skills")} /> } backgroundImage="linear-gradient(135deg, #2984BD 0%, #0AB88A 100%)" - onClick={() => goToRoute("/taas/myteams/createnewteam/jd")} + onClick={() => goToRoute("/taas/createnewteam/jd")} /> ); diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx index 60317253..6d095ca5 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/components/SkillListPopup/index.jsx @@ -5,67 +5,50 @@ 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"; +import BaseCreateModal from "../../../../components/BaseCreateModal"; -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, isLoading, onClose, onContinueClick }) { + const Buttons = ( + <> + + + + ); -function SkillListPopup({ open, skills, onClose, isLoading, onContinueClick }) { return ( - + headerIcon={} + title="Skills" + subtitle={ + skills.length + ? "These skills are found in your Job Description" + : "No skills are found in your Job Description" } - styles={{ - modal: modalStyle, - modalContainer: containerStyle, - }} + isLoading={isLoading} + loadingMessage="Loading skills..." + maxWidth="460px" + buttons={Buttons} > -
- {isLoading ? ( - <> - -
loading skills
- - ) : ( - <> - -
skills
- {_.map(skills, (s) => { - return
{s.tag}
; - })} - - )} -
-
- +
+ {_.map(skills, (s) => { + return
{s.tag}
; + })}
- + ); } diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx index 776267b3..2ba16303 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/index.jsx @@ -4,11 +4,14 @@ * Allows user to search for roles by * job description */ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import PageHeader from "components/PageHeader"; import MarkdownEditor from "../../../../components/MarkdownEditor"; import "./styles.module.scss"; import SearchAndSubmit from "../../components/SearchAndSubmit"; +import TextInput from "components/TextInput"; +import { getSkillsByJobDescription } from "services/teams"; +import SkillListPopup from "./components/SkillListPopup"; function InputJobDescription() { const [stages, setStages] = useState([ @@ -17,33 +20,80 @@ function InputJobDescription() { { name: "Overview of the Results" }, ]); const [jdString, setJdString] = useState(""); + const [jobTitle, setJobTitle] = useState(""); + const [skills, setSkills] = useState([]); + const [loadingSkills, setLoadingSkills] = useState(true); + const [popupOpen, setPopupOpen] = useState(false); const onEditChange = useCallback((value) => { setJdString(value); }, []); + const searchObject = useMemo(() => { + if (jobTitle && jobTitle.length) { + return { jobDescription: jdString, jobTitle }; + } + return { jobDescription: jdString }; + }, [jobTitle, jdString]); + + const onClick = useCallback(() => { + setLoadingSkills(true); + setSkills([]); + setPopupOpen(true); + getSkillsByJobDescription(jdString) + .then((res) => { + setSkills(res.data); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setLoadingSkills(false); + }); + }, [jdString]); + return ( 255} completenessStyle="input-job-description" - searchObject={{ jobDescription: jdString }} - toRender={ - <> -
- - ( +
+ +
+
- - } + 255 + ? "Maximum of 255 characters. Please reduce job description length." + : "" + } + /> + setPopupOpen(false)} + skills={skills} + isLoading={loadingSkills} + onContinueClick={searchFunc} + /> +
+ )} /> ); } diff --git a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss index 9fe69610..892befbe 100644 --- a/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss +++ b/src/routes/CreateNewTeam/pages/InputJobDescription/styles.module.scss @@ -7,3 +7,12 @@ padding: 0 30px 30px; flex: 1; } + +.job-title { + margin-bottom: 15px; + + input:not([type="checkbox"]) { + line-height: normal; + padding: 8px 15px; + } +} \ No newline at end of file diff --git a/src/routes/CreateNewTeam/pages/InputSkills/index.jsx b/src/routes/CreateNewTeam/pages/InputSkills/index.jsx index a9ace938..ac0a7b2b 100644 --- a/src/routes/CreateNewTeam/pages/InputSkills/index.jsx +++ b/src/routes/CreateNewTeam/pages/InputSkills/index.jsx @@ -51,13 +51,13 @@ function InputSkills() { isCompletenessDisabled={selectedSkills.length < 1} searchObject={{ skills: selectedSkills }} completenessStyle="input-skills" - toRender={ + toRender={() => ( - } + )} /> ); } diff --git a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx index 7486b59e..73dddfa7 100644 --- a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx +++ b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx @@ -11,6 +11,11 @@ import { getRoles } from "services/roles"; import LoadingIndicator from "components/LoadingIndicator"; import RoleDetailsModal from "../../components/RoleDetailsModal"; import SearchAndSubmit from "../../components/SearchAndSubmit"; +import { isCustomRole } from "utils/helpers"; + +// Remove custom roles from role list +const removeCustomRoles = (roles) => + roles.filter((role) => !isCustomRole(role)); function SelectRole() { const [stages, setStages] = useState([ @@ -44,10 +49,10 @@ function SelectRole() { isCompletenessDisabled={!selectedRoleId} searchObject={{ roleId: selectedRoleId }} completenessStyle="role-selection" - toRender={ + toRender={() => ( <> setRoleDetailsModalOpen(false)} /> - } + )} /> ); } diff --git a/src/routes/CreateNewTeam/reducers/index.js b/src/routes/CreateNewTeam/reducers/index.js index 74c9e6ce..d90316bf 100644 --- a/src/routes/CreateNewTeam/reducers/index.js +++ b/src/routes/CreateNewTeam/reducers/index.js @@ -18,9 +18,18 @@ const loadState = () => { return defaultState; } }; - const initialState = loadState(); +const deleteRoleInState = (state, deleteId) => { + const filteredRoles = state.addedRoles.filter( + (role) => role.searchId !== deleteId + ); + return { + ...state, + addedRoles: filteredRoles, + }; +}; + const reducer = (state = initialState, action) => { switch (action.type) { case ACTION_TYPE.CLEAR_SEARCHED_ROLES: @@ -42,12 +51,8 @@ const reducer = (state = initialState, action) => { previousSearchId: action.payload, }; - case ACTION_TYPE.REPLACE_SEARCHED_ROLES: - return { - ...state, - addedRoles: action.payload.roles, - previousSearchId: action.payload.lastRoleId, - }; + case ACTION_TYPE.DELETE_SEARCHED_ROLE: + return deleteRoleInState(state, action.payload); default: return state; diff --git a/src/services/teams.js b/src/services/teams.js index 7fedd403..b8b4200c 100644 --- a/src/services/teams.js +++ b/src/services/teams.js @@ -215,13 +215,13 @@ export const postProject = () => { * @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.jobTitle job title to associate with search * @param {string} searchObject.previousRoleSearchRequestId id of the last search made * * @returns {Promise} the role found */ export const searchRoles = (searchObject) => { const newObject = { ...searchObject }; - delete newObject.previousRoleSearchRequestId; const url = `${config.API.V5}/taas-teams/sendRoleSearchRequest`; return axios.post(url, newObject); }; diff --git a/src/styles/main.vendor.scss b/src/styles/main.vendor.scss index a2a998d4..e0608194 100644 --- a/src/styles/main.vendor.scss +++ b/src/styles/main.vendor.scss @@ -4,6 +4,7 @@ @import "~react-redux-toastr/src/styles/index"; @import "~react-responsive-modal/styles"; @import "~react-loader-spinner/dist/loader/css/react-spinner-loader.css"; +@import "~react-datepicker/dist/react-datepicker.css"; // toast-ui.editor styles @import "~codemirror/lib/codemirror.css"; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 5de1d653..c54cbe5f 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -5,6 +5,7 @@ * If there are multiple methods which could be grouped into a separate file by their meaning they should be extracted from here to not make this file too big. */ import _ from "lodash"; +import { CUSTOM_ROLE_NAMES } from "constants/"; /** * Delay code for some milliseconds using promise. @@ -62,3 +63,11 @@ export const setCurrentStage = (currentStepIdx, stages, setStagesCallback) => { .map((s) => ({ ...s, completed: false, isCurrent: false })), ]); }; + +/** + * Checks if role is custom/niche + * @param {Object} role role to check + * @returns {boolean} whether the role is custom/niche + */ +export const isCustomRole = (role) => + !role || !role.name || CUSTOM_ROLE_NAMES.includes(role.name.toLowerCase());