From 5bf28b175901b3d6c938b6bc0470fc1626c3ad89 Mon Sep 17 00:00:00 2001 From: dat Date: Thu, 22 Jun 2023 18:39:37 +0700 Subject: [PATCH 01/25] - onboarding app page 1/7 - onboarding app page 2/7 --- .vscode/settings.json | 3 + src/apps/onboarding/assets/images/x-icon.svg | 1 + .../components/progress-bar/index.tsx | 29 +++ .../progress-bar/styles.module.scss | 27 +++ .../onboarding/components/skill-tag/index.tsx | 24 +++ .../components/skill-tag/styles.module.scss | 19 ++ src/apps/onboarding/config/index.ts | 11 ++ src/apps/onboarding/models/SkillInfo.ts | 7 + src/apps/onboarding/onboarding.routes.tsx | 38 ++++ .../onboarding/pages/onboarding/index.tsx | 46 +++++ .../pages/onboarding/styles.module.scss | 4 + src/apps/onboarding/pages/skills/index.tsx | 166 ++++++++++++++++++ .../pages/skills/styles.module.scss | 19 ++ src/apps/onboarding/pages/start/index.tsx | 80 +++++++++ .../onboarding/pages/start/styles.module.scss | 22 +++ src/apps/onboarding/redux/actions/member.ts | 46 +++++ src/apps/onboarding/redux/reducers/index.ts | 13 ++ src/apps/onboarding/redux/reducers/member.ts | 38 ++++ src/apps/onboarding/redux/store.ts | 31 ++++ src/apps/onboarding/services/skills.ts | 9 + src/apps/onboarding/styles/global/_flex.scss | 71 ++++++++ src/apps/onboarding/styles/global/_index.scss | 2 + .../onboarding/styles/global/_layout.scss | 3 + src/apps/platform/src/platform.routes.tsx | 2 + src/config/constants.ts | 1 + 25 files changed, 712 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/apps/onboarding/assets/images/x-icon.svg create mode 100644 src/apps/onboarding/components/progress-bar/index.tsx create mode 100644 src/apps/onboarding/components/progress-bar/styles.module.scss create mode 100644 src/apps/onboarding/components/skill-tag/index.tsx create mode 100644 src/apps/onboarding/components/skill-tag/styles.module.scss create mode 100644 src/apps/onboarding/config/index.ts create mode 100644 src/apps/onboarding/models/SkillInfo.ts create mode 100644 src/apps/onboarding/onboarding.routes.tsx create mode 100644 src/apps/onboarding/pages/onboarding/index.tsx create mode 100644 src/apps/onboarding/pages/onboarding/styles.module.scss create mode 100644 src/apps/onboarding/pages/skills/index.tsx create mode 100644 src/apps/onboarding/pages/skills/styles.module.scss create mode 100644 src/apps/onboarding/pages/start/index.tsx create mode 100644 src/apps/onboarding/pages/start/styles.module.scss create mode 100644 src/apps/onboarding/redux/actions/member.ts create mode 100644 src/apps/onboarding/redux/reducers/index.ts create mode 100644 src/apps/onboarding/redux/reducers/member.ts create mode 100644 src/apps/onboarding/redux/store.ts create mode 100644 src/apps/onboarding/services/skills.ts create mode 100644 src/apps/onboarding/styles/global/_flex.scss create mode 100644 src/apps/onboarding/styles/global/_index.scss create mode 100644 src/apps/onboarding/styles/global/_layout.scss diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a2a88ec68 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "vue.features.codeActions.enable": false +} \ No newline at end of file diff --git a/src/apps/onboarding/assets/images/x-icon.svg b/src/apps/onboarding/assets/images/x-icon.svg new file mode 100644 index 000000000..6d7f9b501 --- /dev/null +++ b/src/apps/onboarding/assets/images/x-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/onboarding/components/progress-bar/index.tsx b/src/apps/onboarding/components/progress-bar/index.tsx new file mode 100644 index 000000000..3232b5972 --- /dev/null +++ b/src/apps/onboarding/components/progress-bar/index.tsx @@ -0,0 +1,29 @@ +import React, { FC } from 'react' + +import styles from './styles.module.scss' + +interface ProgressBarProps { + progress: number + className?: string +} + +export const ProgressBar: FC = (props: ProgressBarProps) => { + + const progressProps: React.CSSProperties & { '--progress': number } = { + '--progress': props.progress, + } + + return ( +
+ #/## +
+
+
+
+ ) +} + +export default ProgressBar diff --git a/src/apps/onboarding/components/progress-bar/styles.module.scss b/src/apps/onboarding/components/progress-bar/styles.module.scss new file mode 100644 index 000000000..1146056d9 --- /dev/null +++ b/src/apps/onboarding/components/progress-bar/styles.module.scss @@ -0,0 +1,27 @@ +@import '../../../../libs/ui/lib/styles/includes'; + +.wrap { + background: $black-10; + border-radius: $sp-1; + height: 4px; + width: 100%; + display: flex; + + :global(.progress) { + background: gray; + border-radius: inherit; + width: calc(var(--progress, 0) * 100%); + position: relative; + + .percentage { + position: absolute; + top: 1px; + font-family: $font-barlow; + font-style: normal; + font-weight: $font-weight-bold; + font-size: 11px; + line-height: 14px; + color: $black-100; + } + } +} \ No newline at end of file diff --git a/src/apps/onboarding/components/skill-tag/index.tsx b/src/apps/onboarding/components/skill-tag/index.tsx new file mode 100644 index 000000000..271bc9314 --- /dev/null +++ b/src/apps/onboarding/components/skill-tag/index.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react' +import classNames from 'classnames' + +import { Skill } from '~/apps/talent-search/src/lib/models' + +import XIcon from '../../assets/images/x-icon.svg' + +import styles from './styles.module.scss' + +interface SkillTagProps { + skill: Skill + onDelete?: () => void +} + +const SkillTag: FC = (props: SkillTagProps) => ( +
+ {props.skill.name} + +
+) + +export default SkillTag diff --git a/src/apps/onboarding/components/skill-tag/styles.module.scss b/src/apps/onboarding/components/skill-tag/styles.module.scss new file mode 100644 index 000000000..2af4e9a89 --- /dev/null +++ b/src/apps/onboarding/components/skill-tag/styles.module.scss @@ -0,0 +1,19 @@ +.container { + background-color: gray; + color: white; + border-radius: 3px; + padding: 0 10px; + font-size: 14px; + gap: 5px; +} + +.btnDelete { + display: flex; + align-items: center; + justify-content: center; + background-color: white; + border-radius: 100%; + flex-shrink: 0; + width: 15px; + height: 15px; +} \ No newline at end of file diff --git a/src/apps/onboarding/config/index.ts b/src/apps/onboarding/config/index.ts new file mode 100644 index 000000000..100565d55 --- /dev/null +++ b/src/apps/onboarding/config/index.ts @@ -0,0 +1,11 @@ +export const ACTIONS: { + MEMBER: { + GET_MEMBER: string; + UPDATE_MEMBER_SKILLS: string; + }; +} = { + MEMBER: { + GET_MEMBER: 'GET_MEMBER', + UPDATE_MEMBER_SKILLS: 'UPDATE_MEMBER_SKILLS', + }, +} diff --git a/src/apps/onboarding/models/SkillInfo.ts b/src/apps/onboarding/models/SkillInfo.ts new file mode 100644 index 000000000..ea886ae37 --- /dev/null +++ b/src/apps/onboarding/models/SkillInfo.ts @@ -0,0 +1,7 @@ +export default interface SkillInfo { + category: string; + emsiId: string; + name: string; + skillSources?: string[]; + subCategory: string; +} diff --git a/src/apps/onboarding/onboarding.routes.tsx b/src/apps/onboarding/onboarding.routes.tsx new file mode 100644 index 000000000..a797abb31 --- /dev/null +++ b/src/apps/onboarding/onboarding.routes.tsx @@ -0,0 +1,38 @@ +import { Navigate } from 'react-router-dom' + +import { ToolTitle } from '~/config/constants' +import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' + +const PageOnboarding: LazyLoadedComponent = lazyLoad(() => import('./pages/onboarding/index'), 'OnboardingWrapper') +const PageStart: LazyLoadedComponent = lazyLoad(() => import('./pages/start/index'), 'PageStart') +const PageSkills: LazyLoadedComponent = lazyLoad(() => import('./pages/skills/index'), 'PageSkills') +const toolTitle: string = ToolTitle.onboarding +const onboardingRootRoute: string = '/onboarding' + +export const onboardRouteId: string = `${toolTitle} Onbarding` + +export const onboardingRoutes: ReadonlyArray = [ + { + authRequired: true, + children: [ + { + element: , + route: '/', + }, + { + element: , + route: '/start', + title: toolTitle, + }, + { + element: , + route: '/skills', + title: toolTitle, + }, + ], + element: , + id: onboardRouteId, + route: onboardingRootRoute, + title: toolTitle, + }, +] diff --git a/src/apps/onboarding/pages/onboarding/index.tsx b/src/apps/onboarding/pages/onboarding/index.tsx new file mode 100644 index 000000000..96bd37517 --- /dev/null +++ b/src/apps/onboarding/pages/onboarding/index.tsx @@ -0,0 +1,46 @@ +import { FC, useContext, useEffect } from 'react' +import { Outlet, Routes } from 'react-router-dom' +import { connect, Provider } from 'react-redux' +import classNames from 'classnames' + +import { ContentLayout } from '~/libs/ui' +import { routerContext, RouterContextData } from '~/libs/core' + +import { onboardRouteId } from '../../onboarding.routes' +import { fetchMemberInfo } from '../../redux/actions/member' +import store from '../../redux/store' +import '../../styles/global/_index.scss' + +import styles from './styles.module.scss' + +const OnboardingContent: FC<{ fetchMemberInfo: () => void }> = props => { + const { getChildRoutes }: RouterContextData = useContext(routerContext) + useEffect(() => { + props.fetchMemberInfo() + /* eslint-disable react-hooks/exhaustive-deps */ + }, []) + + return ( +
+ + + {getChildRoutes(onboardRouteId)} + +
+ ) +} + +const mapDispatchToProps: any = { + fetchMemberInfo, +} +const Onboarding: any = connect(undefined, mapDispatchToProps)(OnboardingContent) + +export const OnboardingWrapper: FC<{}> = () => ( + + + + + +) + +export default OnboardingWrapper diff --git a/src/apps/onboarding/pages/onboarding/styles.module.scss b/src/apps/onboarding/pages/onboarding/styles.module.scss new file mode 100644 index 000000000..9017edb99 --- /dev/null +++ b/src/apps/onboarding/pages/onboarding/styles.module.scss @@ -0,0 +1,4 @@ +.container { + min-height: 100%; + padding: 60px 0 20px 0; +} \ No newline at end of file diff --git a/src/apps/onboarding/pages/skills/index.tsx b/src/apps/onboarding/pages/skills/index.tsx new file mode 100644 index 000000000..ad2c6f95c --- /dev/null +++ b/src/apps/onboarding/pages/skills/index.tsx @@ -0,0 +1,166 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable unicorn/no-null */ +import { useNavigate } from 'react-router-dom' +import { connect } from 'react-redux' +import { FC, useEffect, useState } from 'react' +import _ from 'lodash' +import AsyncSelect from 'react-select/async' +import classNames from 'classnames' + +import { Member } from '~/apps/talent-search/src/lib/models' +import { Button, PageDivider } from '~/libs/ui' + +import { ProgressBar } from '../../components/progress-bar' +import { autoCompleteSkills } from '../../services/skills' +import { updateMemberSkills } from '../../redux/actions/member' +import SkillInfo from '../../models/SkillInfo' +import SkillTag from '../../components/skill-tag' + +import styles from './styles.module.scss' + +const PageSkillsContent: FC<{ + memberInfo?: Member, + updateMemberSkills: (skills: SkillInfo[]) => void +}> = props => { + const navigate: any = useNavigate() + const [skillsFilter, setSkillsFilter] = useState>([]) + const [loading, setLoading] = useState(false) + useEffect(() => { + if (!skillsFilter.length) { + setSkillsFilter(props.memberInfo?.emsiSkills || []) + } + }, [props.memberInfo, skillsFilter]) + + return ( +
+

What skills do you have?

+ +
+
+

Select your skills

+
+ + Add industry standard skills to your profile to let employers + search and find you for opportunities that fit your capabilities. + +
+ {(skillsFilter.length > 0) ? ( +
+ {skillsFilter.map(skillItem => ( + { + setSkillsFilter( + _.filter(skillsFilter, skill => skill.name !== skillItem.name), + ) + }} + /> + ))} +
+ ) : null} + skill.name} + getOptionValue={(skill: SkillInfo) => skill.emsiId} + onChange={(options: readonly SkillInfo[]) => { + if (options.length > 0) { + const newSkillFilter: SkillInfo[] = _.uniqBy([...skillsFilter, ...options], 'name') + _.forEach(options, option => { + const matchSkill: SkillInfo | undefined = _.find( + newSkillFilter, + { name: option.name }, + ) + if (matchSkill && !matchSkill?.skillSources) { + matchSkill.skillSources = ['SelfPicked'] + } else if ( + matchSkill + && matchSkill.skillSources + && matchSkill.skillSources.indexOf('SelfPicked') < 0 + ) { + matchSkill.skillSources.push('SelfPicked') + } + }) + setSkillsFilter(newSkillFilter) + } + }} + value={[]} + isDisabled={!props.memberInfo || loading} + /> +
+
+
+ Wait! Get my skills from LinkedIn instead... + +
+
+ + + +
+ + +
+
+ ) +} + +const mapStateToProps: any = (state: any) => { + const { + memberInfo, + }: any = state.member + + return { + memberInfo, + } +} + +const mapDispatchToProps: any = { + updateMemberSkills, +} + +export const PageSkills: any = connect(mapStateToProps, mapDispatchToProps)(PageSkillsContent) + +export default PageSkills diff --git a/src/apps/onboarding/pages/skills/styles.module.scss b/src/apps/onboarding/pages/skills/styles.module.scss new file mode 100644 index 000000000..8efee791a --- /dev/null +++ b/src/apps/onboarding/pages/skills/styles.module.scss @@ -0,0 +1,19 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 20px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; +} +.blockLeft { + max-width: 530px; +} + +.blockSkilTags { + gap: 10px; +} \ No newline at end of file diff --git a/src/apps/onboarding/pages/start/index.tsx b/src/apps/onboarding/pages/start/index.tsx new file mode 100644 index 000000000..fadbf1ca8 --- /dev/null +++ b/src/apps/onboarding/pages/start/index.tsx @@ -0,0 +1,80 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +import { FC } from 'react' +import { Button, PageDivider } from '~/libs/ui' +import { useNavigate } from 'react-router-dom' +import classNames from 'classnames' +import { ProgressBar } from '../../components/progress-bar' +import styles from './styles.module.scss' + +export const PageStart: FC<{}> = () => { + const navigate: any = useNavigate() + + return ( +
+

It's easy to create an awesome profile!

+ + +
+ + When you share your skills, education and work experience, your Topcoder profile will + help put your best foot forward. + + Let potential employers and others in our network see your exceptional talent! +
+

How would you like to share your skills and experience?

+
+ +
+
+ We can extract data from your LinkedIn profile or a digital version of your resume. +
+ + +
+
+

OR

+
+ You can enter your information manually +
+ +
+
+
+
+ + + +
+ +
+
+ ) +} + +export default PageStart diff --git a/src/apps/onboarding/pages/start/styles.module.scss b/src/apps/onboarding/pages/start/styles.module.scss new file mode 100644 index 000000000..3e8c24712 --- /dev/null +++ b/src/apps/onboarding/pages/start/styles.module.scss @@ -0,0 +1,22 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 20px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; +} + +.blockImportButtons { + gap: 30px; + margin-top: 30px; +} + +.blockOr { + max-width: 1000px; + gap: 20px; +} diff --git a/src/apps/onboarding/redux/actions/member.ts b/src/apps/onboarding/redux/actions/member.ts new file mode 100644 index 000000000..713ab79e3 --- /dev/null +++ b/src/apps/onboarding/redux/actions/member.ts @@ -0,0 +1,46 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable sort-keys */ +import { Member } from '~/apps/talent-search/src/lib/models' +import { getAsync, putAsync } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' +import { getAsync as getAsyncToken } from '~/libs/core/lib/auth/token-functions/token.functions' +import { profile } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config' +import { TokenModel } from '~/libs/core' +import { ACTIONS } from '../../config' +import SkillInfo from '../../models/SkillInfo' + +export const updateMemberInfo: any = (memberInfo: Member) => ({ + type: ACTIONS.MEMBER.GET_MEMBER, + payload: memberInfo, +}) + +export const fetchMemberInfo: any = () => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + const memberInfo: Member = await getAsync(profile(tokenInfo.handle || '')) + dispatch(updateMemberInfo(memberInfo)) + } catch (error) { + } +} + +export const setMemberSkills: any = (skills: SkillInfo[]) => ({ + type: ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS, + payload: skills, +}) + +export const updateMemberSkills: any = (skills: SkillInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + await putAsync(profile(tokenInfo.handle || ''), { + emsiSkills: skills.map(skill => ({ + skillSources: skill.skillSources, + subCategory: skill.subCategory, + emsiId: skill.emsiId, + name: skill.name, + category: skill.category, + })), + }) + dispatch(setMemberSkills(skills)) + } catch (error) { + } +} diff --git a/src/apps/onboarding/redux/reducers/index.ts b/src/apps/onboarding/redux/reducers/index.ts new file mode 100644 index 000000000..87392cd93 --- /dev/null +++ b/src/apps/onboarding/redux/reducers/index.ts @@ -0,0 +1,13 @@ +/** + * Root Redux Reducer + */ +import { combineReducers } from 'redux' + +import memberReducer from './member' + +// redux root reducer +const rootReducer: any = combineReducers({ + member: memberReducer, +}) + +export default rootReducer diff --git a/src/apps/onboarding/redux/reducers/member.ts b/src/apps/onboarding/redux/reducers/member.ts new file mode 100644 index 000000000..666b4ddb1 --- /dev/null +++ b/src/apps/onboarding/redux/reducers/member.ts @@ -0,0 +1,38 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable default-param-last */ +import { Member } from '~/apps/talent-search/src/lib/models' +import { ACTIONS } from '../../config' + +const initialState: { + memberInfo?: Member +} = { +} + +const memberReducer: any = (state = initialState, action: { type: any; payload: any; }) => { + switch (action.type) { + case ACTIONS.MEMBER.GET_MEMBER: + return { + ...state, + memberInfo: action.payload, + } + case ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS: { + if (!state.memberInfo) { + return state + } + + return { + ...state, + memberInfo: { + ...state.memberInfo, + emsiSkills: action.payload, + }, + } + } + + default: + return state + } +} + +export default memberReducer diff --git a/src/apps/onboarding/redux/store.ts b/src/apps/onboarding/redux/store.ts new file mode 100644 index 000000000..d87a843fb --- /dev/null +++ b/src/apps/onboarding/redux/store.ts @@ -0,0 +1,31 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable global-require */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/** + * Configure Redux Store + */ +import { applyMiddleware, compose, createStore } from 'redux' +import { createPromise } from 'redux-promise-middleware' +import thunk from 'redux-thunk' +import rootReducer from './reducers' + +const middlewares: any = [ + // if payload of action is promise it would split action into 3 states + createPromise({ + promiseTypeSuffixes: ['PENDING', 'SUCCESS', 'ERROR'], + }), + thunk, +] + +// enable Redux Logger in in DEV environment +if (process.env.APPMODE !== 'production') { + const { createLogger }: any = require('redux-logger') + + const logger: any = createLogger() + middlewares.push(logger) +} + +const store: any = createStore(rootReducer, compose(applyMiddleware(...middlewares))) + +export default store diff --git a/src/apps/onboarding/services/skills.ts b/src/apps/onboarding/services/skills.ts new file mode 100644 index 000000000..bd92089c9 --- /dev/null +++ b/src/apps/onboarding/services/skills.ts @@ -0,0 +1,9 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +import { EnvironmentConfig } from '~/config' +import { xhrGetAsync } from '~/libs/core' +import SkillInfo from '../models/SkillInfo' + +export async function autoCompleteSkills(search: string): Promise> { + return xhrGetAsync(`${EnvironmentConfig.API.V5}/emsi-skills/skills/auto-complete?term=${search}`) +} diff --git a/src/apps/onboarding/styles/global/_flex.scss b/src/apps/onboarding/styles/global/_flex.scss new file mode 100644 index 000000000..14274549e --- /dev/null +++ b/src/apps/onboarding/styles/global/_flex.scss @@ -0,0 +1,71 @@ +.flex-1 { + flex: 1 1 0%; +} +.flex-none { + flex: none; +} +.d-flex { + display: flex; + + &.flex-wrap { + flex-wrap: wrap; + } + + &.align-items- { + ¢er { + align-items: center; + } + + &stretch { + align-items: stretch; + } + + &end { + align-items: flex-end; + } + + &start { + align-items: flex-start; + } + } + + .align-self- { + ¢er { + align-self: center; + } + + &start { + align-self: flex-start; + } + + &end { + align-self: flex-end; + } + } + + &.justify-content- { + ¢er { + justify-content: center; + } + + &end { + justify-content: flex-end; + } + + &between { + justify-content: space-between; + } + + &around { + justify-content: space-around; + } + } + + &.flex-column { + flex-direction: column; + } + + &.flex-row { + flex-direction: row; + } +} diff --git a/src/apps/onboarding/styles/global/_index.scss b/src/apps/onboarding/styles/global/_index.scss new file mode 100644 index 000000000..31505310d --- /dev/null +++ b/src/apps/onboarding/styles/global/_index.scss @@ -0,0 +1,2 @@ +@import "flex"; +@import "layout"; diff --git a/src/apps/onboarding/styles/global/_layout.scss b/src/apps/onboarding/styles/global/_layout.scss new file mode 100644 index 000000000..03cc86de4 --- /dev/null +++ b/src/apps/onboarding/styles/global/_layout.scss @@ -0,0 +1,3 @@ +.mt-30 { + margin-top: 30px; +} \ No newline at end of file diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index ff88720ae..88a498cc5 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -8,6 +8,7 @@ import { selfServiceRoutes } from '~/apps/self-service' import { profilesRoutes } from '~/apps/profiles' import { talentSearchRoutes } from '~/apps/talent-search' import { accountsRoutes } from '~/apps/accounts' +import { onboardingRoutes } from '~/apps/onboarding/onboarding.routes' const Home: LazyLoadedComponent = lazyLoad(() => import('./routes/home'), 'HomePage') @@ -24,6 +25,7 @@ export const platformRoutes: Array = [ // is determined by finding the first route // that matches the current path ...selfServiceRoutes, + ...onboardingRoutes, ...devCenterRoutes, // ...earnRoutes, ...learnRoutes, diff --git a/src/config/constants.ts b/src/config/constants.ts index f6f25100c..98cb64460 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -17,6 +17,7 @@ export enum ToolTitle { profiles = 'Profiles', tcAcademy = 'Topcoder Academy', selfService = 'Self Service Challenges', + onboarding = 'Welcome to Topcoder', talentSearch = 'Talent Search' } From 7eaa80d84ce8f55815b65b0555ff13884a66c6a9 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 23 Jun 2023 07:19:13 +1000 Subject: [PATCH 02/25] Deploy onboarding to dev --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4c623cb2e..16e161844 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -262,7 +262,7 @@ workflows: branches: only: - dev - - talent-search + - onboarding - deployQa: context : org-global From 98858207e67692e60db02452c837d5ecb29f3b15 Mon Sep 17 00:00:00 2001 From: dat Date: Fri, 23 Jun 2023 09:36:37 +0700 Subject: [PATCH 03/25] Onboarding update --- src/apps/onboarding/index.ts | 1 + src/apps/onboarding/{ => src}/assets/images/x-icon.svg | 0 .../onboarding/{ => src}/components/progress-bar/index.tsx | 3 ++- .../{ => src}/components/progress-bar/styles.module.scss | 2 +- .../onboarding/{ => src}/components/skill-tag/index.tsx | 0 .../{ => src}/components/skill-tag/styles.module.scss | 0 src/apps/onboarding/{ => src}/config/index.ts | 0 src/apps/onboarding/src/index.tsx | 1 + src/apps/onboarding/{ => src}/models/SkillInfo.ts | 0 src/apps/onboarding/{ => src}/onboarding.routes.tsx | 0 src/apps/onboarding/{ => src}/pages/onboarding/index.tsx | 0 .../{ => src}/pages/onboarding/styles.module.scss | 0 src/apps/onboarding/{ => src}/pages/skills/index.tsx | 6 +++++- .../onboarding/{ => src}/pages/skills/styles.module.scss | 0 src/apps/onboarding/{ => src}/pages/start/index.tsx | 6 +++++- .../onboarding/{ => src}/pages/start/styles.module.scss | 0 src/apps/onboarding/{ => src}/redux/actions/member.ts | 0 src/apps/onboarding/{ => src}/redux/reducers/index.ts | 0 src/apps/onboarding/{ => src}/redux/reducers/member.ts | 0 src/apps/onboarding/{ => src}/redux/store.ts | 0 src/apps/onboarding/{ => src}/services/skills.ts | 0 src/apps/onboarding/{ => src}/styles/global/_flex.scss | 0 src/apps/onboarding/{ => src}/styles/global/_index.scss | 0 src/apps/onboarding/{ => src}/styles/global/_layout.scss | 0 src/apps/platform/src/platform.routes.tsx | 2 +- 25 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 src/apps/onboarding/index.ts rename src/apps/onboarding/{ => src}/assets/images/x-icon.svg (100%) rename src/apps/onboarding/{ => src}/components/progress-bar/index.tsx (91%) rename src/apps/onboarding/{ => src}/components/progress-bar/styles.module.scss (91%) rename src/apps/onboarding/{ => src}/components/skill-tag/index.tsx (100%) rename src/apps/onboarding/{ => src}/components/skill-tag/styles.module.scss (100%) rename src/apps/onboarding/{ => src}/config/index.ts (100%) create mode 100644 src/apps/onboarding/src/index.tsx rename src/apps/onboarding/{ => src}/models/SkillInfo.ts (100%) rename src/apps/onboarding/{ => src}/onboarding.routes.tsx (100%) rename src/apps/onboarding/{ => src}/pages/onboarding/index.tsx (100%) rename src/apps/onboarding/{ => src}/pages/onboarding/styles.module.scss (100%) rename src/apps/onboarding/{ => src}/pages/skills/index.tsx (97%) rename src/apps/onboarding/{ => src}/pages/skills/styles.module.scss (100%) rename src/apps/onboarding/{ => src}/pages/start/index.tsx (95%) rename src/apps/onboarding/{ => src}/pages/start/styles.module.scss (100%) rename src/apps/onboarding/{ => src}/redux/actions/member.ts (100%) rename src/apps/onboarding/{ => src}/redux/reducers/index.ts (100%) rename src/apps/onboarding/{ => src}/redux/reducers/member.ts (100%) rename src/apps/onboarding/{ => src}/redux/store.ts (100%) rename src/apps/onboarding/{ => src}/services/skills.ts (100%) rename src/apps/onboarding/{ => src}/styles/global/_flex.scss (100%) rename src/apps/onboarding/{ => src}/styles/global/_index.scss (100%) rename src/apps/onboarding/{ => src}/styles/global/_layout.scss (100%) diff --git a/src/apps/onboarding/index.ts b/src/apps/onboarding/index.ts new file mode 100644 index 000000000..6f39cd49b --- /dev/null +++ b/src/apps/onboarding/index.ts @@ -0,0 +1 @@ +export * from './src' diff --git a/src/apps/onboarding/assets/images/x-icon.svg b/src/apps/onboarding/src/assets/images/x-icon.svg similarity index 100% rename from src/apps/onboarding/assets/images/x-icon.svg rename to src/apps/onboarding/src/assets/images/x-icon.svg diff --git a/src/apps/onboarding/components/progress-bar/index.tsx b/src/apps/onboarding/src/components/progress-bar/index.tsx similarity index 91% rename from src/apps/onboarding/components/progress-bar/index.tsx rename to src/apps/onboarding/src/components/progress-bar/index.tsx index 3232b5972..e0672f03b 100644 --- a/src/apps/onboarding/components/progress-bar/index.tsx +++ b/src/apps/onboarding/src/components/progress-bar/index.tsx @@ -4,6 +4,7 @@ import styles from './styles.module.scss' interface ProgressBarProps { progress: number + label?: string className?: string } @@ -15,7 +16,7 @@ export const ProgressBar: FC = (props: ProgressBarProps) => { return (
- #/## + {props.label}
- +
- +
+
+) + +export default ConnectLinkedIn diff --git a/src/apps/onboarding/src/components/modal-add-education/index.tsx b/src/apps/onboarding/src/components/modal-add-education/index.tsx new file mode 100644 index 000000000..4afbd8a01 --- /dev/null +++ b/src/apps/onboarding/src/components/modal-add-education/index.tsx @@ -0,0 +1,200 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable sort-keys */ +import { FC, useEffect, useState } from 'react' +import classNames from 'classnames' +import moment from 'moment' +import _ from 'lodash' + +import { BaseModal, Button, InputText } from '~/libs/ui' +import { FormInputCheckbox } from '~/apps/self-service/src/components/form-elements' + +import styles from './styles.module.scss' +import EducationInfo, { emptyEducationInfo } from '../../models/EducationInfo' +import FormField from '../FormField' +import DateInput from '../DateInput' + +const FormInputCheckboxMiddleware: any = FormInputCheckbox as any + +interface ModalAddEducationProps { + onClose?: () => void + editingEducation?: EducationInfo | null + onAdd?: (educationInfo: EducationInfo) => void + onEdit?: (educationInfo: EducationInfo) => void +} + +const ModalAddEducation: FC = (props: ModalAddEducationProps) => { + const [educationInfo, setEducationInfo] = useState(emptyEducationInfo()) + const [formErrors, setFormErrors] = useState({ + collegeName: undefined, + major: undefined, + startDate: undefined, + }) + + const validateDate: any = (startDate: Date | undefined, endDate: Date | undefined) => { + const isInValid: any = endDate + && startDate + && moment(endDate) + .isSameOrBefore(startDate) + return !isInValid + } + + const validateField: any = () => { + const errorTmp: any = {} + if (!educationInfo.collegeName) { + errorTmp.collegeName = 'Required' + } + + if (!educationInfo.major) { + errorTmp.major = 'Required' + } + + if (!validateDate(educationInfo.startDate, educationInfo.endDate)) { + errorTmp.startDate = 'Start Date should be before End Date' + } + + setFormErrors(errorTmp) + return _.isEmpty(errorTmp) + } + + useEffect(() => { + if (props.editingEducation) { + setEducationInfo(props.editingEducation) + } + }, [props.editingEducation]) + + return ( + { + if (validateField()) { + const endDate: Date | undefined = educationInfo.endDate + let endDateString: string = endDate ? moment(endDate) + .format('YYYY') : '' + if (!educationInfo.graduated) { + endDateString = 'current' + } + + let startDateString: string = educationInfo.startDate ? moment(educationInfo.startDate) + .format('YYYY') : '' + if (startDateString) { + startDateString += '-' + } + + (props.editingEducation ? props.onEdit : props.onAdd)?.({ + ...educationInfo, + dateDescription: ( + educationInfo.startDate || educationInfo.endDate + ) ? `${startDateString}${endDateString}` : '', + }) + props.onClose?.() + } + }} + /> + )} + onClose={props.onClose || _.noop} + open + size='body' + title={props.editingEducation ? 'Edit education:' : 'Add education:'} + classNames={{ modal: styles.infoModal }} + > +
+
+ { + setEducationInfo({ + ...educationInfo, + collegeName: event.target.value, + }) + }} + placeholder='Name of College or University' + tabIndex={0} + type='text' + dirty + error={formErrors.collegeName} + /> +
+
+ setEducationInfo({ + ...educationInfo, + major: event.target.value, + })} + placeholder='Major' + tabIndex={0} + type='text' + dirty + error={formErrors.major} + /> +
+
+
+ + { + setEducationInfo({ + ...educationInfo, + startDate: v || undefined, + }) + }} + style2 + placeholder='Start date' + /> + +
+
+ + { + setEducationInfo({ + ...educationInfo, + endDate: v || undefined, + }) + }} + style2 + placeholder='End date' + /> + +
+
+ { + setEducationInfo({ + ...educationInfo, + graduated: e.target.checked, + }) + }} + /> +
+
+ ) +} + +export default ModalAddEducation diff --git a/src/apps/onboarding/src/components/modal-add-education/styles.module.scss b/src/apps/onboarding/src/components/modal-add-education/styles.module.scss new file mode 100644 index 000000000..a8d052c57 --- /dev/null +++ b/src/apps/onboarding/src/components/modal-add-education/styles.module.scss @@ -0,0 +1,11 @@ +@import '@libs/ui/styles/includes'; + +.infoModal { + :global(.react-responsive-modal-closeButton) { + display: flex; + } + + .modalContent { + width: 100%; + } +} diff --git a/src/apps/onboarding/src/components/modal-add-work/index.tsx b/src/apps/onboarding/src/components/modal-add-work/index.tsx new file mode 100644 index 000000000..6941e4451 --- /dev/null +++ b/src/apps/onboarding/src/components/modal-add-work/index.tsx @@ -0,0 +1,250 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable sort-keys */ +import { FC, useEffect, useState } from 'react' +import classNames from 'classnames' +import _ from 'lodash' +import moment from 'moment' + +import { BaseModal, Button, InputSelect, InputText, InputTextarea } from '~/libs/ui' +import { FormInputCheckbox } from '~/apps/self-service/src/components/form-elements' + +import styles from './styles.module.scss' +import WorkInfo, { emptyWorkInfo } from '../../models/WorkInfo' +import { INDUSTRIES_OPTIONS } from '../../config' +import FormField from '../FormField' +import DateInput from '../DateInput' + +const FormInputCheckboxMiddleware: any = FormInputCheckbox as any + +interface ModalAddWorkProps { + onClose?: () => void + editingWork?: WorkInfo | null + onAdd?: (workInfo: WorkInfo) => void + onEdit?: (workInfo: WorkInfo) => void +} + +const industryOptions: any = _.sortBy(INDUSTRIES_OPTIONS) + .map(v => ({ + value: v, + label: v, + })) + +const ModalAddWork: FC = (props: ModalAddWorkProps) => { + const [workInfo, setWorkInfo] = useState(emptyWorkInfo()) + const [formErrors, setFormErrors] = useState({ + company: undefined, + position: undefined, + startDate: undefined, + }) + + const validateDate: any = (startDate: Date | undefined, endDate: Date | undefined) => { + const isInValid: any = endDate + && startDate + && moment(endDate) + .isSameOrBefore(startDate) + return !isInValid + } + + const validateField: any = () => { + const errorTmp: any = {} + if (!workInfo.company) { + errorTmp.company = 'Required' + } + + if (!workInfo.position) { + errorTmp.position = 'Required' + } + + if (!validateDate(workInfo.startDate, workInfo.endDate)) { + errorTmp.startDate = 'Start Date should be before End Date' + } + + setFormErrors(errorTmp) + return _.isEmpty(errorTmp) + } + + useEffect(() => { + if (props.editingWork) { + setWorkInfo(props.editingWork) + } + }, [props.editingWork]) + + return ( + { + if (validateField()) { + const endDate: Date | undefined = workInfo.endDate + let endDateString: string = endDate ? moment(endDate) + .format('YYYY') : '' + if (workInfo.currentlyWorking) { + endDateString = 'current' + } + + let startDateString: string = workInfo.startDate ? moment(workInfo.startDate) + .format('YYYY') : '' + if (startDateString) { + startDateString += '-' + } + + (props.editingWork ? props.onEdit : props.onAdd)?.({ + ...workInfo, + dateDescription: ( + workInfo.startDate || workInfo.endDate + ) ? `${startDateString}${endDateString}` : '', + }) + props.onClose?.() + } + }} + /> + )} + onClose={props.onClose || _.noop} + open + size='body' + title={props.editingWork ? 'Edit work experience:' : 'Add work experience:'} + classNames={{ modal: styles.infoModal }} + > +
+
+ { + setWorkInfo({ + ...workInfo, + company: event.target.value, + }) + }} + placeholder='Company Name' + tabIndex={0} + type='text' + dirty + error={formErrors.company} + /> +
+
+
+ setWorkInfo({ + ...workInfo, + industry: event.target.value, + })} + name='industry' + label='Industry' + placeholder='Industry' + /> +
+
+ setWorkInfo({ + ...workInfo, + city: event.target.value, + })} + placeholder='Location' + tabIndex={0} + type='text' + /> +
+
+
+ setWorkInfo({ + ...workInfo, + position: event.target.value, + })} + placeholder='Position / Job Title' + tabIndex={0} + type='text' + dirty + error={formErrors.position} + /> +
+
+
+ + { + setWorkInfo({ + ...workInfo, + startDate: v || undefined, + }) + }} + style2 + placeholder='Start date' + /> + +
+
+ + { + setWorkInfo({ + ...workInfo, + endDate: v || undefined, + }) + }} + style2 + placeholder='End date' + /> + +
+
+ { + setWorkInfo({ + ...workInfo, + currentlyWorking: e.target.checked, + }) + }} + /> +
+ setWorkInfo({ + ...workInfo, + description: event.target.value, + })} + onBlur={_.noop} + placeholder='Description' + tabIndex={0} + /> +
+
+
+ ) +} + +export default ModalAddWork diff --git a/src/apps/onboarding/src/components/modal-add-work/styles.module.scss b/src/apps/onboarding/src/components/modal-add-work/styles.module.scss new file mode 100644 index 000000000..a8d052c57 --- /dev/null +++ b/src/apps/onboarding/src/components/modal-add-work/styles.module.scss @@ -0,0 +1,11 @@ +@import '@libs/ui/styles/includes'; + +.infoModal { + :global(.react-responsive-modal-closeButton) { + display: flex; + } + + .modalContent { + width: 100%; + } +} diff --git a/src/apps/onboarding/src/components/progress-bar/index.tsx b/src/apps/onboarding/src/components/progress-bar/index.tsx index e0672f03b..ed32ae048 100644 --- a/src/apps/onboarding/src/components/progress-bar/index.tsx +++ b/src/apps/onboarding/src/components/progress-bar/index.tsx @@ -1,3 +1,5 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ import React, { FC } from 'react' import styles from './styles.module.scss' diff --git a/src/apps/onboarding/src/components/skill-tag/index.tsx b/src/apps/onboarding/src/components/skill-tag/index.tsx index 271bc9314..b31b377f8 100644 --- a/src/apps/onboarding/src/components/skill-tag/index.tsx +++ b/src/apps/onboarding/src/components/skill-tag/index.tsx @@ -1,3 +1,5 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ import React, { FC } from 'react' import classNames from 'classnames' diff --git a/src/apps/onboarding/src/components/skill-tag/styles.module.scss b/src/apps/onboarding/src/components/skill-tag/styles.module.scss index 2af4e9a89..eabe14698 100644 --- a/src/apps/onboarding/src/components/skill-tag/styles.module.scss +++ b/src/apps/onboarding/src/components/skill-tag/styles.module.scss @@ -1,19 +1,19 @@ .container { - background-color: gray; - color: white; - border-radius: 3px; - padding: 0 10px; - font-size: 14px; - gap: 5px; + background-color: gray; + color: white; + border-radius: 3px; + padding: 0 10px; + font-size: 14px; + gap: 5px; } .btnDelete { - display: flex; - align-items: center; - justify-content: center; - background-color: white; - border-radius: 100%; - flex-shrink: 0; - width: 15px; - height: 15px; + display: flex; + align-items: center; + justify-content: center; + background-color: white; + border-radius: 100%; + flex-shrink: 0; + width: 15px; + height: 15px; } \ No newline at end of file diff --git a/src/apps/onboarding/src/config/index.ts b/src/apps/onboarding/src/config/index.ts index 100565d55..d9e7e1873 100644 --- a/src/apps/onboarding/src/config/index.ts +++ b/src/apps/onboarding/src/config/index.ts @@ -1,11 +1,29 @@ +/* eslint-disable sort-keys */ export const ACTIONS: { MEMBER: { GET_MEMBER: string; UPDATE_MEMBER_SKILLS: string; + SET_WORKS: string; + SET_EDUCATIONS: string; }; } = { MEMBER: { GET_MEMBER: 'GET_MEMBER', UPDATE_MEMBER_SKILLS: 'UPDATE_MEMBER_SKILLS', + SET_WORKS: 'SET_WORKS', + SET_EDUCATIONS: 'SET_EDUCATIONS', }, } + +export const INDUSTRIES_OPTIONS: string[] = [ + 'Banking', + 'Consumer goods', + 'Energy', + 'Entertainment', + 'Healthcare', + 'Pharma', + 'Tech & technology services', + 'Telecoms', + 'Public sector', + 'Travel & hospitality', +] diff --git a/src/apps/onboarding/src/models/EducationInfo.ts b/src/apps/onboarding/src/models/EducationInfo.ts new file mode 100644 index 000000000..c43ccecf0 --- /dev/null +++ b/src/apps/onboarding/src/models/EducationInfo.ts @@ -0,0 +1,20 @@ +/* eslint-disable sort-keys */ +export default interface EducationInfo { + collegeName: string + major: string + dateDescription: string + startDate?: Date + endDate?: Date + graduated?: boolean + id: number +} + +export const emptyEducationInfo: () => EducationInfo = () => ({ + collegeName: '', + major: '', + dateDescription: '', + startDate: undefined, + endDate: undefined, + graduated: false, + id: 0, +}) diff --git a/src/apps/onboarding/src/models/SkillInfo.ts b/src/apps/onboarding/src/models/SkillInfo.ts index ea886ae37..cbfb4a167 100644 --- a/src/apps/onboarding/src/models/SkillInfo.ts +++ b/src/apps/onboarding/src/models/SkillInfo.ts @@ -1,7 +1,7 @@ export default interface SkillInfo { - category: string; - emsiId: string; - name: string; - skillSources?: string[]; - subCategory: string; + category: string + emsiId: string + name: string + skillSources?: string[] + subCategory: string } diff --git a/src/apps/onboarding/src/models/WorkInfo.ts b/src/apps/onboarding/src/models/WorkInfo.ts new file mode 100644 index 000000000..6342a9681 --- /dev/null +++ b/src/apps/onboarding/src/models/WorkInfo.ts @@ -0,0 +1,26 @@ +/* eslint-disable sort-keys */ +export default interface WorkInfo { + company?: string + position?: string + industry?: string + city?: string + startDate?: Date + dateDescription?: string + description?: string + endDate?: Date + currentlyWorking?: boolean + id: number +} + +export const emptyWorkInfo: () => WorkInfo = () => ({ + company: '', + position: '', + industry: '', + city: '', + startDate: undefined, + dateDescription: '', + endDate: undefined, + description: '', + currentlyWorking: false, + id: 0, +}) diff --git a/src/apps/onboarding/src/onboarding.routes.tsx b/src/apps/onboarding/src/onboarding.routes.tsx index 26e5e29f8..0254db598 100644 --- a/src/apps/onboarding/src/onboarding.routes.tsx +++ b/src/apps/onboarding/src/onboarding.routes.tsx @@ -6,6 +6,8 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/c const PageOnboarding: LazyLoadedComponent = lazyLoad(() => import('./pages/onboarding/index'), 'OnboardingWrapper') const PageStart: LazyLoadedComponent = lazyLoad(() => import('./pages/start/index'), 'PageStart') const PageSkills: LazyLoadedComponent = lazyLoad(() => import('./pages/skills/index'), 'PageSkills') +const PageWorks: LazyLoadedComponent = lazyLoad(() => import('./pages/works/index'), 'PageWorks') +const PageEducations: LazyLoadedComponent = lazyLoad(() => import('./pages/educations/index'), 'PageEducations') const toolTitle: string = ToolTitle.onboarding const onboardingRootRoute: string = '/onboarding' @@ -22,12 +24,18 @@ export const onboardingRoutes: ReadonlyArray = [ { element: , route: '/start', - title: toolTitle, }, { element: , route: '/skills', - title: toolTitle, + }, + { + element: , + route: '/works', + }, + { + element: , + route: '/educations', }, ], element: , diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx new file mode 100644 index 000000000..871da4ae4 --- /dev/null +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -0,0 +1,168 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable unicorn/no-null */ +import { FC, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import classNames from 'classnames' +import _ from 'lodash' +import { connect } from 'react-redux' + +import { Button, PageDivider } from '~/libs/ui' + +import { ProgressBar } from '../../components/progress-bar' +import styles from './styles.module.scss' +import IconEdit from '../../assets/images/edit.svg' +import IconTrash from '../../assets/images/trash.svg' +import EducationInfo from '../../models/EducationInfo' +import ModalAddEducation from '../../components/modal-add-education' +import { updateMemberEducations, createMemberEducations } from '../../redux/actions/member' + +export const PageEducationsContent: FC<{ + reduxEducations: EducationInfo[] | null + updateMemberEducations: (educations: EducationInfo[]) => void + createMemberEducations: (educations: EducationInfo[]) => void +}> = props => { + const navigate: any = useNavigate() + const [editingEducation, setEditingEducation] = useState(null) + const [educations, setEducations] = useState(null) + const [educationId, setEducationId] = useState(10) + const [showAddEducationModal, setShowAddEducationModal] = useState(false) + const [loading, setLoading] = useState(false) + useEffect(() => { + if (!educations && props.reduxEducations) { + setEducations(props.reduxEducations) + setEducationId(props.reduxEducations[props.reduxEducations.length - 1].id + 1) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [props.reduxEducations]) + + return ( +
+

Add your education information

+ + +
+
+ {(educations || []).map(education => ( +
+
+ {education.collegeName} + + +
+ {education.major} + {education.dateDescription ? ({education.dateDescription}) : null} + +
+ ))} +
+ + + {(!educations || educations.length === 0) ? ( + + Relevant education details will help make your + profile more valuable to potential employers, add it here! + + ) : null} +
+ + + +
+ + +
+ {showAddEducationModal ? ( + { + setShowAddEducationModal(false) + setEditingEducation(null) + }} + onAdd={newEducation => { + setEducations([...(educations || []), { + ...newEducation, + id: educationId + 1, + }]) + setEducationId(educationId + 1) + }} + onEdit={editEducation => { + setEducations((educations || []).map(w => (w.id !== editEducation.id ? w : editEducation))) + }} + /> + ) : null} +
+ ) +} + +const mapStateToProps: any = (state: any) => { + const { + educations, + }: any = state.member + + return { + reduxEducations: educations, + } +} + +const mapDispatchToProps: any = { + createMemberEducations, + updateMemberEducations, +} + +export const PageEducations: any = connect(mapStateToProps, mapDispatchToProps)(PageEducationsContent) + +export default PageEducations diff --git a/src/apps/onboarding/src/pages/educations/styles.module.scss b/src/apps/onboarding/src/pages/educations/styles.module.scss new file mode 100644 index 000000000..70463203a --- /dev/null +++ b/src/apps/onboarding/src/pages/educations/styles.module.scss @@ -0,0 +1,12 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 20px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; +} diff --git a/src/apps/onboarding/src/pages/onboarding/index.tsx b/src/apps/onboarding/src/pages/onboarding/index.tsx index 96bd37517..5592b4025 100644 --- a/src/apps/onboarding/src/pages/onboarding/index.tsx +++ b/src/apps/onboarding/src/pages/onboarding/index.tsx @@ -7,16 +7,20 @@ import { ContentLayout } from '~/libs/ui' import { routerContext, RouterContextData } from '~/libs/core' import { onboardRouteId } from '../../onboarding.routes' -import { fetchMemberInfo } from '../../redux/actions/member' +import { fetchMemberInfo, fetchMemberTraits } from '../../redux/actions/member' import store from '../../redux/store' import '../../styles/global/_index.scss' import styles from './styles.module.scss' -const OnboardingContent: FC<{ fetchMemberInfo: () => void }> = props => { +const OnboardingContent: FC<{ + fetchMemberInfo: () => void; + fetchMemberTraits: () => void; +}> = props => { const { getChildRoutes }: RouterContextData = useContext(routerContext) useEffect(() => { props.fetchMemberInfo() + props.fetchMemberTraits() /* eslint-disable react-hooks/exhaustive-deps */ }, []) @@ -26,12 +30,14 @@ const OnboardingContent: FC<{ fetchMemberInfo: () => void }> = props => { {getChildRoutes(onboardRouteId)} +
) } const mapDispatchToProps: any = { fetchMemberInfo, + fetchMemberTraits, } const Onboarding: any = connect(undefined, mapDispatchToProps)(OnboardingContent) diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 1a874eaa3..9eb871e38 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -18,19 +18,21 @@ import SkillInfo from '../../models/SkillInfo' import SkillTag from '../../components/skill-tag' import styles from './styles.module.scss' +import ConnectLinkedIn from '../../components/connect-linked-in' const PageSkillsContent: FC<{ memberInfo?: Member, updateMemberSkills: (skills: SkillInfo[]) => void }> = props => { const navigate: any = useNavigate() - const [skillsFilter, setSkillsFilter] = useState>([]) + const [skillsFilter, setSkillsFilter] = useState | null>(null) const [loading, setLoading] = useState(false) useEffect(() => { - if (!skillsFilter.length) { + if (!skillsFilter && props.memberInfo) { setSkillsFilter(props.memberInfo?.emsiSkills || []) } - }, [props.memberInfo, skillsFilter]) + /* eslint-disable react-hooks/exhaustive-deps */ + }, [props.memberInfo]) return (
@@ -39,13 +41,12 @@ const PageSkillsContent: FC<{

Select your skills

-
- + Add industry standard skills to your profile to let employers search and find you for opportunities that fit your capabilities.
- {(skillsFilter.length > 0) ? ( + {(skillsFilter && skillsFilter.length > 0) ? (
- {skillsFilter.map(skillItem => ( + {(skillsFilter || []).map(skillItem => ( skill.emsiId} onChange={(options: readonly SkillInfo[]) => { if (options.length > 0) { - const newSkillFilter: SkillInfo[] = _.uniqBy([...skillsFilter, ...options], 'name') + const newSkillFilter: SkillInfo[] = _.uniqBy( + [...(skillsFilter || []), ...options], + 'name', + ) _.forEach(options, option => { const matchSkill: SkillInfo | undefined = _.find( newSkillFilter, @@ -105,17 +109,8 @@ const PageSkillsContent: FC<{ />
-
- Wait! Get my skills from LinkedIn instead... - -
+ +
{ setLoading(true) - await props.updateMemberSkills([...skillsFilter]) + if (!_.isEqual(props.memberInfo?.emsiSkills, skillsFilter)) { + await props.updateMemberSkills([...(skillsFilter || [])]) + } + setLoading(false) + navigate('../works') }} > next diff --git a/src/apps/onboarding/src/pages/start/index.tsx b/src/apps/onboarding/src/pages/start/index.tsx index add10ccb9..bfe66debd 100644 --- a/src/apps/onboarding/src/pages/start/index.tsx +++ b/src/apps/onboarding/src/pages/start/index.tsx @@ -21,11 +21,9 @@ export const PageStart: FC<{}> = () => { help put your best foot forward. Let potential employers and others in our network see your exceptional talent! -
-

How would you like to share your skills and experience?

-
+

How would you like to share your skills and experience?

-
+
We can extract data from your LinkedIn profile or a digital version of your resume.
diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx new file mode 100644 index 000000000..b13858074 --- /dev/null +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -0,0 +1,183 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable unicorn/no-null */ +import { FC, useEffect, useState } from 'react' +import { connect } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import classNames from 'classnames' +import _ from 'lodash' + +import { Button, PageDivider } from '~/libs/ui' + +import { ProgressBar } from '../../components/progress-bar' +import styles from './styles.module.scss' +import ConnectLinkedIn from '../../components/connect-linked-in' +import WorkInfo from '../../models/WorkInfo' +import ModalAddWork from '../../components/modal-add-work' +import IconEdit from '../../assets/images/edit.svg' +import IconTrash from '../../assets/images/trash.svg' +import { createMemberWorks, updateMemberWorks } from '../../redux/actions/member' + +export const PageWorksContent: FC<{ + reduxWorks: WorkInfo[] | null + updateMemberWorks: (works: WorkInfo[]) => void + createMemberWorks: (works: WorkInfo[]) => void +}> = props => { + const navigate: any = useNavigate() + const [editingWork, setEditingWork] = useState(null) + const [works, setWorks] = useState(null) + const [workId, setWorkId] = useState(10) + const [showAddWorkModal, setShowAddWorkModal] = useState(false) + const [loading, setLoading] = useState(false) + useEffect(() => { + if (!works && props.reduxWorks) { + setWorks(props.reduxWorks) + setWorkId(props.reduxWorks[props.reduxWorks.length - 1].id + 1) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [props.reduxWorks]) + + return ( +
+

Add your work experience here

+ + +
+
+
+ {(works || []).map(work => ( +
+
+ {work.company} + + +
+ {work.city ? ({work.city}) : null} + {work.position} + {work.dateDescription ? ({work.dateDescription}) : null} + {work.description ? ( + + {work.description} + + ) : null} + +
+ ))} +
+ + + {(!works || works.length === 0) ? ( + + You will be able to add details for each of the work experiences + that you think will demonstrate your abilities. + + ) : null} +
+ +
+ + + +
+ + +
+ {showAddWorkModal ? ( + { + setShowAddWorkModal(false) + setEditingWork(null) + }} + onAdd={newWork => { + setWorks([...(works || []), { + ...newWork, + id: workId + 1, + }]) + setWorkId(workId + 1) + }} + onEdit={editWork => { + setWorks( + (works || []).map(w => (w.id !== editWork.id ? w : editWork)), + ) + }} + /> + ) : null} +
+ ) +} + +const mapStateToProps: any = (state: any) => { + const { + works, + }: any = state.member + + return { + reduxWorks: works, + } +} + +const mapDispatchToProps: any = { + createMemberWorks, + updateMemberWorks, +} + +export const PageWorks: any = connect(mapStateToProps, mapDispatchToProps)(PageWorksContent) + +export default PageWorks diff --git a/src/apps/onboarding/src/pages/works/styles.module.scss b/src/apps/onboarding/src/pages/works/styles.module.scss new file mode 100644 index 000000000..ecb8d6941 --- /dev/null +++ b/src/apps/onboarding/src/pages/works/styles.module.scss @@ -0,0 +1,20 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 20px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; +} + +.textDescription { + white-space: pre; +} + +.textPosition { + font-weight: 700; +} \ No newline at end of file diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index 713ab79e3..c1eb93731 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -1,28 +1,232 @@ /* eslint-disable ordered-imports/ordered-imports */ /* eslint-disable react/jsx-no-bind */ /* eslint-disable sort-keys */ +import moment from 'moment' import { Member } from '~/apps/talent-search/src/lib/models' -import { getAsync, putAsync } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' import { getAsync as getAsyncToken } from '~/libs/core/lib/auth/token-functions/token.functions' -import { profile } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config' import { TokenModel } from '~/libs/core' +import { + createMemberTraits, + updateMemberTraits, +} from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' + import { ACTIONS } from '../../config' import SkillInfo from '../../models/SkillInfo' +import { getMemberInfo, getMemberTraits, putMemberInfo } from '../../services/members' +import WorkInfo from '../../models/WorkInfo' +import EducationInfo from '../../models/EducationInfo' export const updateMemberInfo: any = (memberInfo: Member) => ({ type: ACTIONS.MEMBER.GET_MEMBER, payload: memberInfo, }) +export const updateWorks: any = (works: WorkInfo[]) => ({ + type: ACTIONS.MEMBER.SET_WORKS, + payload: works, +}) + +export const updateEducations: any = (educations: EducationInfo[]) => ({ + type: ACTIONS.MEMBER.SET_EDUCATIONS, + payload: educations, +}) + export const fetchMemberInfo: any = () => async (dispatch: any) => { try { const tokenInfo: TokenModel = await getAsyncToken() - const memberInfo: Member = await getAsync(profile(tokenInfo.handle || '')) + const memberInfo: Member = await getMemberInfo(tokenInfo.handle || '') dispatch(updateMemberInfo(memberInfo)) } catch (error) { } } +const dateTimeToDate: any = (s: string) => (s ? new Date(s) : undefined) +export const fetchMemberTraits: any = () => async (dispatch: any) => { + const tokenInfo: TokenModel = await getAsyncToken() + let memberTraits: any = [] + try { + memberTraits = await getMemberTraits(tokenInfo.handle || '') + } catch (error) { + } + + const workExp: any = memberTraits.find((t: any) => t.traitId === 'work') + const workExpValue: any = workExp?.traits?.data + if (workExpValue) { + // workExpValue is array of works. fill it to state + const works: WorkInfo[] = workExpValue.map((j: any, index: number) => { + const startDate: Date | undefined = dateTimeToDate(j.timePeriodFrom) + const endDate: Date | undefined = dateTimeToDate(j.timePeriodTo) + let endDateString: string = endDate ? moment(endDate) + .format('YYYY') : '' + if (j.working) { + endDateString = 'current' + } + + let startDateString: string = startDate ? moment(startDate) + .format('YYYY') : '' + if (startDateString) { + startDateString += '-' + } + + const dateDescription: string = ( + startDate || endDate + ) ? `${startDateString}${endDateString}` : '' + return ({ + company: j.company, + position: j.position, + industry: j.industry, + city: j.cityTown, + startDate, + endDate, + currentlyWorking: j.working, + description: j.description, + dateDescription, + id: index + 1, + }) + }) + dispatch(updateWorks(works)) + } + + const educationExp: any = memberTraits.find( + (t: any) => t.traitId === 'education', + ) + const educationExpValue: any = educationExp?.traits?.data + if (educationExpValue) { + // educationExpValue is array of educations. fill it to state + const educations: EducationInfo[] = educationExpValue.map((e: any, index: number) => { + const startDate: Date | undefined = dateTimeToDate(e.timePeriodFrom) + const endDate: Date | undefined = dateTimeToDate(e.timePeriodTo) + let endDateString: string = endDate ? moment(endDate) + .format('YYYY') : '' + if (!e.graduated) { + endDateString = 'current' + } + + let startDateString: string = startDate ? moment(startDate) + .format('YYYY') : '' + if (startDateString) { + startDateString += '-' + } + + const dateDescription: string = ( + startDate || endDate + ) ? `${startDateString}${endDateString}` : '' + return ({ + collegeName: e.schoolCollegeName, + major: e.major, + startDate, + endDate, + graduated: e.graduated, + dateDescription, + id: index + 1, + }) + }) + dispatch(updateEducations(educations)) + } +} + +const createWorksPayloadData: any = (works: WorkInfo[]) => { + const data: any = works.map(work => { + const { + company, + position, + industry, + city, + startDate, + endDate, + currentlyWorking, + description, + }: any = work + return { + company, + industry, + position, + cityTown: city, + description, + timePeriodFrom: startDate ? startDate.toISOString() : '', + timePeriodTo: endDate ? endDate.toISOString() : '', + working: currentlyWorking, + } + }) + + const payload: any = { + categoryName: 'Work', + traitId: 'work', + traits: { + data, + }, + } + return [payload] +} + +export const updateMemberWorks: any = (works: WorkInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await updateMemberTraits(tokenInfo.handle || '', createWorksPayloadData(works)) + dispatch(updateWorks(works)) + } catch (error) { + } +} + +export const createMemberWorks: any = (works: WorkInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await createMemberTraits(tokenInfo.handle || '', createWorksPayloadData(works)) + dispatch(updateWorks(works)) + } catch (error) { + } +} + +const createEducationsPayloadData: any = (educations: EducationInfo[]) => { + const data: any = educations.map(education => { + const { + collegeName, + major, + startDate, + endDate, + graduated, + }: any = education + return { + schoolCollegeName: collegeName, + major, + timePeriodFrom: startDate ? startDate.toISOString() : '', + timePeriodTo: endDate ? endDate.toISOString() : '', + graduated, + } + }) + + const payload: any = { + categoryName: 'Education', + traitId: 'education', + traits: { + data, + }, + } + return [payload] +} + +export const updateMemberEducations: any = (educations: EducationInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await updateMemberTraits(tokenInfo.handle || '', createEducationsPayloadData(educations)) + dispatch(updateEducations(educations)) + } catch (error) { + } +} + +export const createMemberEducations: any = (educations: EducationInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await createMemberTraits(tokenInfo.handle || '', createEducationsPayloadData(educations)) + dispatch(updateEducations(educations)) + } catch (error) { + } +} + export const setMemberSkills: any = (skills: SkillInfo[]) => ({ type: ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS, payload: skills, @@ -31,7 +235,7 @@ export const setMemberSkills: any = (skills: SkillInfo[]) => ({ export const updateMemberSkills: any = (skills: SkillInfo[]) => async (dispatch: any) => { try { const tokenInfo: TokenModel = await getAsyncToken() - await putAsync(profile(tokenInfo.handle || ''), { + await putMemberInfo(tokenInfo.handle || '', { emsiSkills: skills.map(skill => ({ skillSources: skill.skillSources, subCategory: skill.subCategory, diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index 666b4ddb1..36a482604 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -3,9 +3,13 @@ /* eslint-disable default-param-last */ import { Member } from '~/apps/talent-search/src/lib/models' import { ACTIONS } from '../../config' +import WorkInfo from '../../models/WorkInfo' +import EducationInfo from '../../models/EducationInfo' const initialState: { memberInfo?: Member + works?: WorkInfo[] + educations?: EducationInfo[] } = { } @@ -16,6 +20,16 @@ const memberReducer: any = (state = initialState, action: { type: any; payload: ...state, memberInfo: action.payload, } + case ACTIONS.MEMBER.SET_WORKS: + return { + ...state, + works: action.payload, + } + case ACTIONS.MEMBER.SET_EDUCATIONS: + return { + ...state, + educations: action.payload, + } case ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS: { if (!state.memberInfo) { return state diff --git a/src/apps/onboarding/src/services/members.ts b/src/apps/onboarding/src/services/members.ts new file mode 100644 index 000000000..c35aa9548 --- /dev/null +++ b/src/apps/onboarding/src/services/members.ts @@ -0,0 +1,15 @@ +import { getAsync, putAsync } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' +import { profile } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config' +import { Member } from '~/apps/talent-search/src/lib/models' + +export async function getMemberInfo(handle: string): Promise { + return getAsync(profile(handle)) +} + +export async function getMemberTraits(handle: string): Promise { + return getAsync(`${profile(handle)}/traits`) +} + +export async function putMemberInfo(handle: string, data: any): Promise { + return putAsync(profile(handle || ''), data) +} diff --git a/src/apps/onboarding/src/styles/global/_flex.scss b/src/apps/onboarding/src/styles/global/_flex.scss index 14274549e..b4d3ec1a5 100644 --- a/src/apps/onboarding/src/styles/global/_flex.scss +++ b/src/apps/onboarding/src/styles/global/_flex.scss @@ -4,6 +4,12 @@ .flex-none { flex: none; } +.gap-20 { + gap: 20px; +} +.gap-50 { + gap: 50px; +} .d-flex { display: flex; diff --git a/src/apps/onboarding/src/styles/global/_index.scss b/src/apps/onboarding/src/styles/global/_index.scss index 31505310d..e89490574 100644 --- a/src/apps/onboarding/src/styles/global/_index.scss +++ b/src/apps/onboarding/src/styles/global/_index.scss @@ -1,2 +1,10 @@ @import "flex"; @import "layout"; + +.react-datepicker-popper { + z-index: 9999 !important; +} + +.full-width { + width: 100%; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index cef4eff49..971508b90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1504,6 +1504,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.13.8", "@babel/runtime@^7.6.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" + integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.17.8", "@babel/runtime@^7.7.6": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" @@ -2721,11 +2728,23 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@popperjs/core@^2.11.6", "@popperjs/core@^2.9.2": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@remix-run/router@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.5.tgz#d5c65626add4c3c185a89aa5bd38b1e42daec075" integrity sha512-my0Mycd+jruq/1lQuO5LBB6WTlL/e8DTCYWp44DfMTDcXz8DcTlgF0ISaLsGewt+ctHN+yA8xMq3q/N7uWJPug== +"@restart/hooks@^0.4.7": + version "0.4.9" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.9.tgz#ad858fb39d99e252cccce19416adc18fc3f18fcb" + integrity sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ== + dependencies: + dequal "^2.0.2" + "@rollup/plugin-babel@^5.2.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" @@ -4435,6 +4454,16 @@ dependencies: "@types/react" "*" +"@types/react-datepicker@^4.11.2": + version "4.11.2" + resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.11.2.tgz#44abfc6379faa58d28eb5730a058d6a389d40eac" + integrity sha512-ELYyX3lb3K1WltqdlF1hbnaDGgzlF6PIR5T4W38cSEcfrQDIrPE+Ioq5pwRe/KEJ+ihHMjvTVZQkwJx0pWMNHQ== + dependencies: + "@popperjs/core" "^2.9.2" + "@types/react" "*" + date-fns "^2.0.1" + react-popper "^2.2.5" + "@types/react-dom@^18.0.0", "@types/react-dom@^18.0.6": version "18.0.9" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.9.tgz#ffee5e4bfc2a2f8774b15496474f8e7fe8d0b504" @@ -4504,6 +4533,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@>=16.9.11": + version "18.2.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.14.tgz#fa7a6fecf1ce35ca94e74874f70c56ce88f7a127" + integrity sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/redux-actions@2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.2.tgz#5956d9e7b9a644358e2c0610f47b1fa3060edc21" @@ -4648,6 +4686,11 @@ resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.10.tgz#f9763dc0933f8324920afa9c0790308eedf55ca7" integrity sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ== +"@types/warning@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" + integrity sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA== + "@types/ws@^8.5.1": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -7374,18 +7417,18 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -date-fns@^2.29.1: - version "2.29.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" - integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== - -date-fns@^2.30.0: +date-fns@^2.0.1, date-fns@^2.24.0, date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== dependencies: "@babel/runtime" "^7.21.0" +date-fns@^2.29.1: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + date-format@4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.3.tgz#f63de5dc08dc02efd8ef32bf2a6918e486f35873" @@ -7718,7 +7761,7 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" -dom-helpers@^5.0.1: +dom-helpers@^5.0.1, dom-helpers@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== @@ -15113,6 +15156,18 @@ react-date-range@^1.1.3: react-list "^0.8.13" shallow-equal "^1.2.1" +react-datepicker@^4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.14.1.tgz#f9f7925d0a40e3195048bbd22a5faa635765163d" + integrity sha512-uiPfjD+25KI5WOfCAXlzQgSLyksTagk3wwKn1KGBdF19YtybFDregRmcoNNGveQHAbT10SJZdCvk/8pbc7zxJg== + dependencies: + "@popperjs/core" "^2.9.2" + classnames "^2.2.6" + date-fns "^2.24.0" + prop-types "^15.7.2" + react-onclickoutside "^6.12.2" + react-popper "^2.3.0" + react-dev-utils@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" @@ -15205,6 +15260,11 @@ react-error-overlay@^6.0.11: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-fast-compare@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + react-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -15304,11 +15364,38 @@ react-markdown@8.0.6: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-onclickoutside@^6.12.2: + version "6.13.0" + resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" + integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A== + react-only-when@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-only-when/-/react-only-when-1.0.2.tgz#a8a79b48dd6cfbd91ddc710674a94153e88039d3" integrity sha512-agE6l3L6bqaVuwNtjihTQ36M+VBfPS63KOzcNL4ZTmlwSxQPvhzIqmBWfiol0/wLYmKxCcBqgXkEJpvj5Kob8Q== +react-overlays@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.2.1.tgz#49dc007321adb6784e1f212403f0fb37a74ab86b" + integrity sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA== + dependencies: + "@babel/runtime" "^7.13.8" + "@popperjs/core" "^2.11.6" + "@restart/hooks" "^0.4.7" + "@types/warning" "^3.0.0" + dom-helpers "^5.2.0" + prop-types "^15.7.2" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-popper@^2.2.5, react-popper@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba" + integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-redux-toastr@^7.6.10: version "7.6.10" resolved "https://registry.yarnpkg.com/react-redux-toastr/-/react-redux-toastr-7.6.10.tgz#e76c6a7a19162584494b878173fb97d0b557c45a" @@ -17952,6 +18039,16 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +uncontrollable@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" + integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== + dependencies: + "@babel/runtime" "^7.6.3" + "@types/react" ">=16.9.11" + invariant "^2.2.4" + react-lifecycles-compat "^3.0.4" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -18387,6 +18484,13 @@ walker@^1.0.7, walker@^1.0.8: dependencies: makeerror "1.0.12" +warning@^4.0.2, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^2.2.0, watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" From d262ebad23a4d7b8b6424a0d5d444f54bea7bccb Mon Sep 17 00:00:00 2001 From: dat Date: Sat, 24 Jun 2023 19:15:55 +0700 Subject: [PATCH 06/25] fix date picker placeholder color --- .../onboarding/src/components/DateInput/styles.module.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/onboarding/src/components/DateInput/styles.module.scss b/src/apps/onboarding/src/components/DateInput/styles.module.scss index 1d1ff7e09..7e82dc04f 100644 --- a/src/apps/onboarding/src/components/DateInput/styles.module.scss +++ b/src/apps/onboarding/src/components/DateInput/styles.module.scss @@ -61,10 +61,11 @@ font-weight: 400; &::placeholder { - color: #aaa; + color: #7F7F7F; font-size: 14px; font-weight: 400; text-transform: none !important; + opacity: 1; } } } From e1618f2a62f52f63c500a8cfbc4914fb9d3929e8 Mon Sep 17 00:00:00 2001 From: dat Date: Mon, 26 Jun 2023 12:08:53 +0700 Subject: [PATCH 07/25] onboarding: "+" button is disable for new user --- src/apps/onboarding/src/config/index.ts | 2 ++ src/apps/onboarding/src/pages/educations/index.tsx | 7 +++++-- src/apps/onboarding/src/pages/works/index.tsx | 7 +++++-- src/apps/onboarding/src/redux/actions/member.ts | 8 ++++++++ src/apps/onboarding/src/redux/reducers/member.ts | 5 +++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/apps/onboarding/src/config/index.ts b/src/apps/onboarding/src/config/index.ts index d9e7e1873..9484612cf 100644 --- a/src/apps/onboarding/src/config/index.ts +++ b/src/apps/onboarding/src/config/index.ts @@ -5,6 +5,7 @@ export const ACTIONS: { UPDATE_MEMBER_SKILLS: string; SET_WORKS: string; SET_EDUCATIONS: string; + SET_LOADING_MEMBER_TRAITS: string; }; } = { MEMBER: { @@ -12,6 +13,7 @@ export const ACTIONS: { UPDATE_MEMBER_SKILLS: 'UPDATE_MEMBER_SKILLS', SET_WORKS: 'SET_WORKS', SET_EDUCATIONS: 'SET_EDUCATIONS', + SET_LOADING_MEMBER_TRAITS: 'SET_LOADING_MEMBER_TRAITS', }, } diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 871da4ae4..9457d96c2 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -21,6 +21,7 @@ export const PageEducationsContent: FC<{ reduxEducations: EducationInfo[] | null updateMemberEducations: (educations: EducationInfo[]) => void createMemberEducations: (educations: EducationInfo[]) => void + loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() const [editingEducation, setEditingEducation] = useState(null) @@ -77,7 +78,7 @@ export const PageEducationsContent: FC<{ secondary iconToLeft onClick={() => setShowAddEducationModal(true)} - disabled={!educations} + disabled={props.loadingMemberTraits} > + add education @@ -108,7 +109,7 @@ export const PageEducationsContent: FC<{ size='lg' primary iconToLeft - disabled={loading || !educations} + disabled={loading || props.loadingMemberTraits} onClick={async () => { setLoading(true) if (!_.isEqual(props.reduxEducations, educations)) { @@ -151,9 +152,11 @@ export const PageEducationsContent: FC<{ const mapStateToProps: any = (state: any) => { const { educations, + loadingMemberTraits, }: any = state.member return { + loadingMemberTraits, reduxEducations: educations, } } diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index b13858074..931a10ccd 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -22,6 +22,7 @@ export const PageWorksContent: FC<{ reduxWorks: WorkInfo[] | null updateMemberWorks: (works: WorkInfo[]) => void createMemberWorks: (works: WorkInfo[]) => void + loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() const [editingWork, setEditingWork] = useState(null) @@ -87,7 +88,7 @@ export const PageWorksContent: FC<{ secondary iconToLeft onClick={() => setShowAddWorkModal(true)} - disabled={!works} + disabled={props.loadingMemberTraits} > + add work experience @@ -120,7 +121,7 @@ export const PageWorksContent: FC<{ size='lg' primary iconToLeft - disabled={loading || !works} + disabled={loading || props.loadingMemberTraits} onClick={async () => { setLoading(true) if (!_.isEqual(props.reduxWorks, works)) { @@ -165,10 +166,12 @@ export const PageWorksContent: FC<{ const mapStateToProps: any = (state: any) => { const { + loadingMemberTraits, works, }: any = state.member return { + loadingMemberTraits, reduxWorks: works, } } diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index c1eb93731..4eeafa6db 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -31,6 +31,11 @@ export const updateEducations: any = (educations: EducationInfo[]) => ({ payload: educations, }) +export const updateLoadingMemberTraits: any = (loading: boolean) => ({ + type: ACTIONS.MEMBER.SET_LOADING_MEMBER_TRAITS, + payload: loading, +}) + export const fetchMemberInfo: any = () => async (dispatch: any) => { try { const tokenInfo: TokenModel = await getAsyncToken() @@ -44,11 +49,14 @@ const dateTimeToDate: any = (s: string) => (s ? new Date(s) : undefined) export const fetchMemberTraits: any = () => async (dispatch: any) => { const tokenInfo: TokenModel = await getAsyncToken() let memberTraits: any = [] + dispatch(updateLoadingMemberTraits(true)) try { memberTraits = await getMemberTraits(tokenInfo.handle || '') } catch (error) { } + dispatch(updateLoadingMemberTraits(false)) + const workExp: any = memberTraits.find((t: any) => t.traitId === 'work') const workExpValue: any = workExp?.traits?.data if (workExpValue) { diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index 36a482604..18e173bd3 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -25,6 +25,11 @@ const memberReducer: any = (state = initialState, action: { type: any; payload: ...state, works: action.payload, } + case ACTIONS.MEMBER.SET_LOADING_MEMBER_TRAITS: + return { + ...state, + loadingMemberTraits: action.payload, + } case ACTIONS.MEMBER.SET_EDUCATIONS: return { ...state, From 9ccefd9cbb56c51096092d09aded4ec1757263d7 Mon Sep 17 00:00:00 2001 From: dat Date: Tue, 27 Jun 2023 00:57:14 +0700 Subject: [PATCH 08/25] - Remove Linked In buttons and references - Next button on the Onboarding step 1 is not working --- .../components/connect-linked-in/index.tsx | 21 ------------------- .../onboarding/src/pages/educations/index.tsx | 4 +++- .../onboarding/src/pages/skills/index.tsx | 3 --- src/apps/onboarding/src/pages/start/index.tsx | 10 ++------- src/apps/onboarding/src/pages/works/index.tsx | 6 +++--- 5 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 src/apps/onboarding/src/components/connect-linked-in/index.tsx diff --git a/src/apps/onboarding/src/components/connect-linked-in/index.tsx b/src/apps/onboarding/src/components/connect-linked-in/index.tsx deleted file mode 100644 index ddff9dc93..000000000 --- a/src/apps/onboarding/src/components/connect-linked-in/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -import React, { FC } from 'react' - -import { Button } from '~/libs/ui' - -const ConnectLinkedIn: FC<{}> = () => ( -
- Wait! Get my skills from LinkedIn instead... - -
-) - -export default ConnectLinkedIn diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 9457d96c2..09cbaacf7 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -32,7 +32,9 @@ export const PageEducationsContent: FC<{ useEffect(() => { if (!educations && props.reduxEducations) { setEducations(props.reduxEducations) - setEducationId(props.reduxEducations[props.reduxEducations.length - 1].id + 1) + if (props.reduxEducations.length > 0) { + setEducationId(props.reduxEducations[props.reduxEducations.length - 1].id + 1) + } } /* eslint-disable react-hooks/exhaustive-deps */ }, [props.reduxEducations]) diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 9eb871e38..6f1372ac2 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -18,7 +18,6 @@ import SkillInfo from '../../models/SkillInfo' import SkillTag from '../../components/skill-tag' import styles from './styles.module.scss' -import ConnectLinkedIn from '../../components/connect-linked-in' const PageSkillsContent: FC<{ memberInfo?: Member, @@ -109,8 +108,6 @@ const PageSkillsContent: FC<{ />
- -
= () => {
- We can extract data from your LinkedIn profile or a digital version of your resume. + We can extract data from a digital version of your resume.
- diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index 931a10ccd..de933170a 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -11,7 +11,6 @@ import { Button, PageDivider } from '~/libs/ui' import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' -import ConnectLinkedIn from '../../components/connect-linked-in' import WorkInfo from '../../models/WorkInfo' import ModalAddWork from '../../components/modal-add-work' import IconEdit from '../../assets/images/edit.svg' @@ -33,7 +32,9 @@ export const PageWorksContent: FC<{ useEffect(() => { if (!works && props.reduxWorks) { setWorks(props.reduxWorks) - setWorkId(props.reduxWorks[props.reduxWorks.length - 1].id + 1) + if (props.reduxWorks.length > 0) { + setWorkId(props.reduxWorks[props.reduxWorks.length - 1].id + 1) + } } /* eslint-disable react-hooks/exhaustive-deps */ }, [props.reduxWorks]) @@ -99,7 +100,6 @@ export const PageWorksContent: FC<{ ) : null}
-
Date: Tue, 27 Jun 2023 15:40:08 +0700 Subject: [PATCH 09/25] The entered data is not saved if the user clicks Back button in any step --- .../src/components/skill-tag/index.tsx | 8 +++- .../components/skill-tag/styles.module.scss | 4 ++ .../onboarding/src/pages/educations/index.tsx | 37 ++++++++++------ .../src/pages/educations/styles.module.scss | 4 ++ .../onboarding/src/pages/skills/index.tsx | 26 ++++++++---- src/apps/onboarding/src/pages/works/index.tsx | 42 ++++++++++++------- .../src/pages/works/styles.module.scss | 6 ++- 7 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/apps/onboarding/src/components/skill-tag/index.tsx b/src/apps/onboarding/src/components/skill-tag/index.tsx index b31b377f8..4b24d1934 100644 --- a/src/apps/onboarding/src/components/skill-tag/index.tsx +++ b/src/apps/onboarding/src/components/skill-tag/index.tsx @@ -12,12 +12,18 @@ import styles from './styles.module.scss' interface SkillTagProps { skill: Skill onDelete?: () => void + disabled?: boolean } const SkillTag: FC = (props: SkillTagProps) => (
{props.skill.name} -
diff --git a/src/apps/onboarding/src/components/skill-tag/styles.module.scss b/src/apps/onboarding/src/components/skill-tag/styles.module.scss index eabe14698..7f91068f5 100644 --- a/src/apps/onboarding/src/components/skill-tag/styles.module.scss +++ b/src/apps/onboarding/src/components/skill-tag/styles.module.scss @@ -16,4 +16,8 @@ flex-shrink: 0; width: 15px; height: 15px; + + &:disabled { + opacity: 0.2; + } } \ No newline at end of file diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 09cbaacf7..872119d00 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -39,6 +39,25 @@ export const PageEducationsContent: FC<{ /* eslint-disable react-hooks/exhaustive-deps */ }, [props.reduxEducations]) + useEffect(() => { + const saveData: any = async () => { + setLoading(true) + if (!props.reduxEducations) { + await props.createMemberEducations(educations || []) + } else { + await props.updateMemberEducations(educations || []) + } + + setLoading(false) + } + + if (!!educations && !_.isEqual(props.reduxEducations, educations)) { + saveData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [educations]) + return (

Add your education information

@@ -57,6 +76,8 @@ export const PageEducationsContent: FC<{ setEditingEducation(education) setShowAddEducationModal(true) }} + disabled={loading} + className={styles.btn} > @@ -64,6 +85,8 @@ export const PageEducationsContent: FC<{ aria-label='edit' type='button' onClick={() => setEducations(_.filter(educations, w => w.id !== education.id))} + disabled={loading} + className={styles.btn} > @@ -111,19 +134,7 @@ export const PageEducationsContent: FC<{ size='lg' primary iconToLeft - disabled={loading || props.loadingMemberTraits} - onClick={async () => { - setLoading(true) - if (!_.isEqual(props.reduxEducations, educations)) { - if (!props.reduxEducations) { - await props.createMemberEducations(educations || []) - } else { - await props.updateMemberEducations(educations || []) - } - } - - setLoading(false) - }} + disabled={loading} > next diff --git a/src/apps/onboarding/src/pages/educations/styles.module.scss b/src/apps/onboarding/src/pages/educations/styles.module.scss index 70463203a..2389438b5 100644 --- a/src/apps/onboarding/src/pages/educations/styles.module.scss +++ b/src/apps/onboarding/src/pages/educations/styles.module.scss @@ -10,3 +10,7 @@ margin-top: auto; margin-bottom: 32px; } + +.btn:disabled { + opacity: 0.2; +} diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 6f1372ac2..429b4e44a 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -33,6 +33,20 @@ const PageSkillsContent: FC<{ /* eslint-disable react-hooks/exhaustive-deps */ }, [props.memberInfo]) + useEffect(() => { + const saveData: any = async () => { + setLoading(true) + await props.updateMemberSkills([...(skillsFilter || [])]) + setLoading(false) + } + + if (!!skillsFilter && !_.isEqual(props.memberInfo?.emsiSkills, skillsFilter)) { + saveData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [skillsFilter]) + return (

What skills do you have?

@@ -58,6 +72,7 @@ const PageSkillsContent: FC<{ { setSkillsFilter( _.filter(skillsFilter, skill => skill.name !== skillItem.name), @@ -121,6 +136,7 @@ const PageSkillsContent: FC<{ size='lg' primary iconToLeft + disabled={loading} onClick={() => navigate('../start')} > back @@ -130,15 +146,7 @@ const PageSkillsContent: FC<{ primary iconToLeft disabled={loading} - onClick={async () => { - setLoading(true) - if (!_.isEqual(props.memberInfo?.emsiSkills, skillsFilter)) { - await props.updateMemberSkills([...(skillsFilter || [])]) - } - - setLoading(false) - navigate('../works') - }} + onClick={() => navigate('../works')} > next diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index de933170a..9348be2e2 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -39,6 +39,25 @@ export const PageWorksContent: FC<{ /* eslint-disable react-hooks/exhaustive-deps */ }, [props.reduxWorks]) + useEffect(() => { + const saveData: any = async () => { + setLoading(true) + if (!props.reduxWorks) { + await props.createMemberWorks(works || []) + } else { + await props.updateMemberWorks(works || []) + } + + setLoading(false) + } + + if (!!works && !_.isEqual(props.reduxWorks, works)) { + saveData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [works]) + return (

Add your work experience here

@@ -58,6 +77,8 @@ export const PageWorksContent: FC<{ setEditingWork(work) setShowAddWorkModal(true) }} + disabled={loading} + className={styles.btn} > @@ -65,6 +86,8 @@ export const PageWorksContent: FC<{ aria-label='delete' type='button' onClick={() => setWorks(_.filter(works, w => w.id !== work.id))} + disabled={loading} + className={styles.btn} > @@ -89,7 +112,7 @@ export const PageWorksContent: FC<{ secondary iconToLeft onClick={() => setShowAddWorkModal(true)} - disabled={props.loadingMemberTraits} + disabled={props.loadingMemberTraits || loading} > + add work experience @@ -113,6 +136,7 @@ export const PageWorksContent: FC<{ size='lg' primary iconToLeft + disabled={loading} onClick={() => navigate('../skills')} > back @@ -121,20 +145,8 @@ export const PageWorksContent: FC<{ size='lg' primary iconToLeft - disabled={loading || props.loadingMemberTraits} - onClick={async () => { - setLoading(true) - if (!_.isEqual(props.reduxWorks, works)) { - if (!props.reduxWorks) { - await props.createMemberWorks(works || []) - } else { - await props.updateMemberWorks(works || []) - } - } - - setLoading(false) - navigate('../educations') - }} + disabled={loading} + onClick={() => navigate('../educations')} > next diff --git a/src/apps/onboarding/src/pages/works/styles.module.scss b/src/apps/onboarding/src/pages/works/styles.module.scss index ecb8d6941..7d4bb91c3 100644 --- a/src/apps/onboarding/src/pages/works/styles.module.scss +++ b/src/apps/onboarding/src/pages/works/styles.module.scss @@ -17,4 +17,8 @@ .textPosition { font-weight: 700; -} \ No newline at end of file +} + +.btn:disabled { + opacity: 0.2; +} From 5542a84cd811d1766a59ace9e701edc4d381ca60 Mon Sep 17 00:00:00 2001 From: dat Date: Tue, 27 Jun 2023 22:10:10 +0700 Subject: [PATCH 10/25] Onboarding - Input a bio and profile picture --- .../src/components/FieldAvatar/index.tsx | 54 +++++++ .../components/FieldAvatar/styles.module.scss | 10 ++ .../components/InputTextAutoSave/index.tsx | 22 +++ .../InputTextAutoSave/styles.module.scss | 2 + .../InputTextareaAutoSave/index.tsx | 38 +++++ .../InputTextareaAutoSave/styles.module.scss | 2 + .../src/models/AccountDetailInfo.ts | 20 +++ .../src/models/PersonalizationInfo.ts | 12 ++ src/apps/onboarding/src/onboarding.routes.tsx | 10 ++ .../src/pages/account-details/index.tsx | 68 +++++++++ .../pages/account-details/styles.module.scss | 12 ++ .../onboarding/src/pages/educations/index.tsx | 1 + .../src/pages/personalization/index.tsx | 143 ++++++++++++++++++ .../pages/personalization/styles.module.scss | 12 ++ .../components/radio-button/RadioButton.jsx | 5 +- .../radio-button/styles.module.scss | 6 + 16 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/apps/onboarding/src/components/FieldAvatar/index.tsx create mode 100644 src/apps/onboarding/src/components/FieldAvatar/styles.module.scss create mode 100644 src/apps/onboarding/src/components/InputTextAutoSave/index.tsx create mode 100644 src/apps/onboarding/src/components/InputTextAutoSave/styles.module.scss create mode 100644 src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx create mode 100644 src/apps/onboarding/src/components/InputTextareaAutoSave/styles.module.scss create mode 100644 src/apps/onboarding/src/models/AccountDetailInfo.ts create mode 100644 src/apps/onboarding/src/models/PersonalizationInfo.ts create mode 100644 src/apps/onboarding/src/pages/account-details/index.tsx create mode 100644 src/apps/onboarding/src/pages/account-details/styles.module.scss create mode 100644 src/apps/onboarding/src/pages/personalization/index.tsx create mode 100644 src/apps/onboarding/src/pages/personalization/styles.module.scss diff --git a/src/apps/onboarding/src/components/FieldAvatar/index.tsx b/src/apps/onboarding/src/components/FieldAvatar/index.tsx new file mode 100644 index 000000000..53adb74fa --- /dev/null +++ b/src/apps/onboarding/src/components/FieldAvatar/index.tsx @@ -0,0 +1,54 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable unicorn/no-null */ +/** + * FieldAvatar + * + * A Form Field Is a wrapper for input to add the label to it + */ +import React, { FC } from 'react' +import classNames from 'classnames' + +import styles from './styles.module.scss' +import { Button, IconOutline } from '~/libs/ui' + +interface FieldAvatarProps { + className?: string +} + +const FieldAvatar: FC = ({ + className, + ...props +}: FieldAvatarProps) => { + return ( +
+

A picture can speek a thousand words

+
+
+ +
+
+ Requirements: +
    +
  • PNG or JPG format.
  • +
  • Maximum size: 2MB.
  • +
+
+
+ + +
+ ) +} + +export default FieldAvatar diff --git a/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss b/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss new file mode 100644 index 000000000..89eae3a2a --- /dev/null +++ b/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss @@ -0,0 +1,10 @@ +.container { + strong { + font-weight: bold; + } + + ul { + list-style: unset; + padding-left: 30px; + } +} \ No newline at end of file diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx new file mode 100644 index 000000000..c4c37878d --- /dev/null +++ b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx @@ -0,0 +1,22 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable unicorn/no-null */ +/** + * InputTextAutoSave + * + * A Form Field Is a wrapper for input to add the label to it + */ +import React, { FC } from 'react' +import { InputText } from '~/libs/ui' +import { InputTextProps } from '~/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText' + +const InputTextAutoSave: FC = (props: InputTextProps) => { + return ( + + ) +} + +export default InputTextAutoSave diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/styles.module.scss b/src/apps/onboarding/src/components/InputTextAutoSave/styles.module.scss new file mode 100644 index 000000000..204049cc2 --- /dev/null +++ b/src/apps/onboarding/src/components/InputTextAutoSave/styles.module.scss @@ -0,0 +1,2 @@ +.container { +} \ No newline at end of file diff --git a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx new file mode 100644 index 000000000..ad26c3dd8 --- /dev/null +++ b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx @@ -0,0 +1,38 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable unicorn/no-null */ +/** + * InputTextareaAutoSave + * + * A Form Field Is a wrapper for input to add the label to it + */ +import React, { FC, FocusEvent } from 'react' +import { InputTextarea } from '~/libs/ui' + + +export interface InputTextareaProps { + readonly dirty?: boolean + readonly disabled?: boolean + readonly error?: string + readonly hideInlineErrors?: boolean + readonly hint?: string + readonly label?: string + readonly name: string + readonly onChange: (event: FocusEvent) => void + readonly placeholder?: string + readonly spellCheck?: boolean + readonly tabIndex: number + readonly value?: string | number +} + +const InputTextareaAutoSave: FC = (props: InputTextareaProps) => { + return ( + { }} + /> + ) +} + +export default InputTextareaAutoSave diff --git a/src/apps/onboarding/src/components/InputTextareaAutoSave/styles.module.scss b/src/apps/onboarding/src/components/InputTextareaAutoSave/styles.module.scss new file mode 100644 index 000000000..204049cc2 --- /dev/null +++ b/src/apps/onboarding/src/components/InputTextareaAutoSave/styles.module.scss @@ -0,0 +1,2 @@ +.container { +} \ No newline at end of file diff --git a/src/apps/onboarding/src/models/AccountDetailInfo.ts b/src/apps/onboarding/src/models/AccountDetailInfo.ts new file mode 100644 index 000000000..4308e7748 --- /dev/null +++ b/src/apps/onboarding/src/models/AccountDetailInfo.ts @@ -0,0 +1,20 @@ +/* eslint-disable sort-keys */ +export default interface AccountDetailInfo { + streetAddr1: string + streetAddr2: string + city: string + stateCode: string + zip: string + country: string + phoneNumber: string +} + +export const emptyAccountDetailInfo: () => AccountDetailInfo = () => ({ + streetAddr1: '', + streetAddr2: '', + city: '', + stateCode: '', + zip: '', + country: '', + phoneNumber: '', +}) diff --git a/src/apps/onboarding/src/models/PersonalizationInfo.ts b/src/apps/onboarding/src/models/PersonalizationInfo.ts new file mode 100644 index 000000000..6f68bbfba --- /dev/null +++ b/src/apps/onboarding/src/models/PersonalizationInfo.ts @@ -0,0 +1,12 @@ +/* eslint-disable sort-keys */ +export default interface PersonalizationInfo { + referAs: string + profileSelfTitle: string + shortBio: string +} + +export const emptyPersonalizationInfo: () => PersonalizationInfo = () => ({ + referAs: '', + profileSelfTitle: '', + shortBio: '', +}) diff --git a/src/apps/onboarding/src/onboarding.routes.tsx b/src/apps/onboarding/src/onboarding.routes.tsx index 0254db598..052b8ee3a 100644 --- a/src/apps/onboarding/src/onboarding.routes.tsx +++ b/src/apps/onboarding/src/onboarding.routes.tsx @@ -8,6 +8,8 @@ const PageStart: LazyLoadedComponent = lazyLoad(() => import('./pages/start/inde const PageSkills: LazyLoadedComponent = lazyLoad(() => import('./pages/skills/index'), 'PageSkills') const PageWorks: LazyLoadedComponent = lazyLoad(() => import('./pages/works/index'), 'PageWorks') const PageEducations: LazyLoadedComponent = lazyLoad(() => import('./pages/educations/index'), 'PageEducations') +const PagePersonalization: LazyLoadedComponent = lazyLoad(() => import('./pages/personalization/index'), 'PagePersonalization') +const PageAccountDetails: LazyLoadedComponent = lazyLoad(() => import('./pages/account-details/index'), 'PageAccountDetails') const toolTitle: string = ToolTitle.onboarding const onboardingRootRoute: string = '/onboarding' @@ -37,6 +39,14 @@ export const onboardingRoutes: ReadonlyArray = [ element: , route: '/educations', }, + { + element: , + route: '/personalization', + }, + { + element: , + route: '/account-details', + }, ], element: , id: onboardRouteId, diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx new file mode 100644 index 000000000..22861c992 --- /dev/null +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -0,0 +1,68 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable unicorn/no-null */ +import { useNavigate } from 'react-router-dom' +import { connect } from 'react-redux' +import { FC, useState } from 'react' +import _ from 'lodash' +import classNames from 'classnames' + +import { Button, PageDivider } from '~/libs/ui' + +import { ProgressBar } from '../../components/progress-bar' + +import styles from './styles.module.scss' +import AccountDetailInfo from '../../models/AccountDetailInfo' + +const PageAccountDetailsContent: FC<{ +}> = props => { + const navigate: any = useNavigate() + const [loading, setLoading] = useState(false) + const [accountDetailInfo, setAccountDetailInfo] = useState(null) + + return ( +
+

Final account details

+ +
+

Account mailing address

+
+ + + +
+ + +
+
+ ) +} + +const mapStateToProps: any = (state: any) => { +} + +const mapDispatchToProps: any = { +} + +export const PageAccountDetails: any = connect(mapStateToProps, mapDispatchToProps)(PageAccountDetailsContent) + +export default PageAccountDetails diff --git a/src/apps/onboarding/src/pages/account-details/styles.module.scss b/src/apps/onboarding/src/pages/account-details/styles.module.scss new file mode 100644 index 000000000..70463203a --- /dev/null +++ b/src/apps/onboarding/src/pages/account-details/styles.module.scss @@ -0,0 +1,12 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 20px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; +} diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 872119d00..e2d3fadd7 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -135,6 +135,7 @@ export const PageEducationsContent: FC<{ primary iconToLeft disabled={loading} + onClick={() => navigate('../personalization')} > next diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx new file mode 100644 index 000000000..0163eaf91 --- /dev/null +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -0,0 +1,143 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable unicorn/no-null */ +import { useNavigate } from 'react-router-dom' +import { connect } from 'react-redux' +import { FC, useState } from 'react' +import _ from 'lodash' +import classNames from 'classnames' + +import { Button, PageDivider } from '~/libs/ui' + +import { ProgressBar } from '../../components/progress-bar' + +import styles from './styles.module.scss' +import FieldAvatar from '../../components/FieldAvatar' +import { RadioButton } from '~/apps/self-service/src/components/radio-button' +import InputTextAutoSave from '../../components/InputTextAutoSave' +import PersonalizationInfo, { emptyPersonalizationInfo } from '../../models/PersonalizationInfo' +import InputTextareaAutoSave from '../../components/InputTextareaAutoSave' + +const RadioButtonTypescript: any = RadioButton + +const referAsOptions = [ + { + label: 'Only show handle instead of name', + key: 'handle', + value: false, + }, + { + label: 'Show first name, last name and handle', + key: 'name', + value: false, + }, +] + +const blankPersonalizationInfo: PersonalizationInfo = emptyPersonalizationInfo() + +const PagePersonalizationContent: FC<{ +}> = props => { + const navigate: any = useNavigate() + const [loading, setLoading] = useState(false) + const [personalizationInfo, setPersonalizationInfo] = useState(null) + + return ( +
+

Show us your personality!

+ +
+ When members personalize theirs profiles with a photo and description, + they are more likely to get notices by our customers for work and opportunities. + +
+ + +
+

Would you like to add a "handle" to use in community communications?

+ Some of our members prefer to be known within our community with a "handle" or display name that is not their official name. + for example: DannyCoder or ZiggyZ123. You will have an opportunity to set preference for how this is used in your profile. + + + { + console.log('totest newValues', newValues) + }} + disabled={loading} + /> +
+
+ +

Give yourself a professional title

+ An industry standard title will help employers know your general area of expertise. + { + setPersonalizationInfo({ + ...(personalizationInfo || blankPersonalizationInfo), + profileSelfTitle: event.target.value, + }) + }} + placeholder='Company Name' + tabIndex={0} + type='text' + /> + +

Write a short biography

+ Describe yourself in your own words. A short bio will give potential customers a sense of who you are. + { + setPersonalizationInfo({ + ...(personalizationInfo || blankPersonalizationInfo), + profileSelfTitle: event.target.value, + }) + }} + placeholder='Describe yourself and your work' + tabIndex={0} + /> +
+ + + +
+ + +
+
+ ) +} + +const mapStateToProps: any = (state: any) => { +} + +const mapDispatchToProps: any = { +} + +export const PagePersonalization: any = connect(mapStateToProps, mapDispatchToProps)(PagePersonalizationContent) + +export default PagePersonalization diff --git a/src/apps/onboarding/src/pages/personalization/styles.module.scss b/src/apps/onboarding/src/pages/personalization/styles.module.scss new file mode 100644 index 000000000..70463203a --- /dev/null +++ b/src/apps/onboarding/src/pages/personalization/styles.module.scss @@ -0,0 +1,12 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 20px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; +} diff --git a/src/apps/self-service/src/components/radio-button/RadioButton.jsx b/src/apps/self-service/src/components/radio-button/RadioButton.jsx index 8cfa5c3b7..4325b7196 100644 --- a/src/apps/self-service/src/components/radio-button/RadioButton.jsx +++ b/src/apps/self-service/src/components/radio-button/RadioButton.jsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from "react"; import styles from "./styles.module.scss"; import classNames from "classnames"; -function RadioButton({ options, onChange, size, errorMsg }) { +function RadioButton({ options, onChange, size, errorMsg, disabled }) { const [internalOptions, setInternalOptions] = useState( (options || []).map((o, i) => ({ ...o, key: i })) ); @@ -57,6 +57,7 @@ function RadioButton({ options, onChange, size, errorMsg }) { { const newOptions = internalOptions.map((oWithKeyTmp) => ({ ...oWithKeyTmp, @@ -81,6 +82,7 @@ RadioButton.defaultProps = { onChange: () => { }, size: "sm", errorMsg: "", + disabled: false, }; RadioButton.propTypes = { @@ -93,6 +95,7 @@ RadioButton.propTypes = { onChange: PT.func, size: PT.oneOf(["xs", "sm", "lg"]), errorMsg: PT.string, + disabled: PT.bool, }; export default RadioButton; diff --git a/src/apps/self-service/src/components/radio-button/styles.module.scss b/src/apps/self-service/src/components/radio-button/styles.module.scss index 6621e1400..0db4319c5 100644 --- a/src/apps/self-service/src/components/radio-button/styles.module.scss +++ b/src/apps/self-service/src/components/radio-button/styles.module.scss @@ -62,6 +62,12 @@ opacity: 0; cursor: pointer; + &:disabled { + &~.checkmark { + opacity: 0.2; + } + } + /* When the radio button is checked, add a blue background */ &:checked~.checkmark { background-color: $gui-kit-level-2; From 1b48f50f9126734c957b1f1417b4ab0790db2d5b Mon Sep 17 00:00:00 2001 From: dat Date: Wed, 28 Jun 2023 09:10:04 +0700 Subject: [PATCH 11/25] Onboarding: page 5,6/7 --- .../src/components/FieldAvatar/index.tsx | 6 +- .../components/InputTextAutoSave/index.tsx | 36 ++- .../InputTextareaAutoSave/index.tsx | 15 +- src/apps/onboarding/src/config/index.ts | 8 + src/apps/onboarding/src/models/ConnectInfo.ts | 10 + ...{AccountDetailInfo.ts => MemberAddress.ts} | 8 +- src/apps/onboarding/src/models/MemberInfo.ts | 22 ++ .../src/pages/account-details/index.tsx | 306 +++++++++++++++++- .../pages/account-details/styles.module.scss | 5 + .../src/pages/personalization/index.tsx | 128 ++++++-- .../pages/personalization/styles.module.scss | 4 + .../onboarding/src/pages/skills/index.tsx | 4 +- .../onboarding/src/redux/actions/member.ts | 194 ++++++++++- .../onboarding/src/redux/reducers/member.ts | 34 +- src/apps/onboarding/src/services/members.ts | 6 +- .../onboarding/src/styles/global/_flex.scss | 6 + src/apps/onboarding/src/utils/validation.ts | 11 + 17 files changed, 744 insertions(+), 59 deletions(-) create mode 100644 src/apps/onboarding/src/models/ConnectInfo.ts rename src/apps/onboarding/src/models/{AccountDetailInfo.ts => MemberAddress.ts} (53%) create mode 100644 src/apps/onboarding/src/models/MemberInfo.ts create mode 100644 src/apps/onboarding/src/utils/validation.ts diff --git a/src/apps/onboarding/src/components/FieldAvatar/index.tsx b/src/apps/onboarding/src/components/FieldAvatar/index.tsx index 53adb74fa..d65383d12 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/index.tsx +++ b/src/apps/onboarding/src/components/FieldAvatar/index.tsx @@ -23,7 +23,7 @@ const FieldAvatar: FC = ({ }: FieldAvatarProps) => { return (

A picture can speek a thousand words

@@ -33,8 +33,8 @@ const FieldAvatar: FC = ({
Requirements:
    -
  • PNG or JPG format.
  • -
  • Maximum size: 2MB.
  • +
  • PNG or JPG format.
  • +
  • Maximum size: 2MB.
diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx index c4c37878d..f704a7169 100644 --- a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx @@ -7,14 +7,44 @@ * * A Form Field Is a wrapper for input to add the label to it */ -import React, { FC } from 'react' -import { InputText } from '~/libs/ui' -import { InputTextProps } from '~/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText' +import React, { FC, useEffect, useState } from 'react' +import { InputText, InputValue } from '~/libs/ui' +import { InputTextTypes } from '~/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText' + + +export interface InputTextProps { + readonly checked?: boolean + readonly className?: string + readonly dirty?: boolean + readonly disabled?: boolean + readonly error?: string + readonly hideInlineErrors?: boolean + readonly hint?: string + readonly label?: string | JSX.Element + readonly name: string + readonly onChange: (value?: string) => void + readonly placeholder?: string + readonly readonly?: boolean + readonly spellCheck?: boolean + readonly tabIndex: number + readonly value?: InputValue + readonly type: InputTextTypes +} const InputTextAutoSave: FC = (props: InputTextProps) => { + const [value, setValue] = useState('') + useEffect(() => { + setValue(props.value) + }, [props.value]) + return ( { + setValue(event.target.value) + }} + onBlur={() => props.onChange(`${value}`)} /> ) } diff --git a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx index ad26c3dd8..a60147448 100644 --- a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx @@ -7,7 +7,7 @@ * * A Form Field Is a wrapper for input to add the label to it */ -import React, { FC, FocusEvent } from 'react' +import React, { FC, useEffect, useState } from 'react' import { InputTextarea } from '~/libs/ui' @@ -19,7 +19,7 @@ export interface InputTextareaProps { readonly hint?: string readonly label?: string readonly name: string - readonly onChange: (event: FocusEvent) => void + readonly onChange: (value?: string) => void readonly placeholder?: string readonly spellCheck?: boolean readonly tabIndex: number @@ -27,10 +27,19 @@ export interface InputTextareaProps { } const InputTextareaAutoSave: FC = (props: InputTextareaProps) => { + const [value, setValue] = useState('') + useEffect(() => { + setValue(props.value) + }, [props.value]) + return ( { }} + value={value} + onChange={event => { + setValue(event.target.value) + }} + onBlur={() => props.onChange(`${value}`)} /> ) } diff --git a/src/apps/onboarding/src/config/index.ts b/src/apps/onboarding/src/config/index.ts index 9484612cf..cee1fffd3 100644 --- a/src/apps/onboarding/src/config/index.ts +++ b/src/apps/onboarding/src/config/index.ts @@ -5,7 +5,11 @@ export const ACTIONS: { UPDATE_MEMBER_SKILLS: string; SET_WORKS: string; SET_EDUCATIONS: string; + SET_PERSONALIZATION: string; + SET_ADDRESS: string; + SET_CONNECT_INFO: string; SET_LOADING_MEMBER_TRAITS: string; + SET_LOADING_MEMBER_INFO: string; }; } = { MEMBER: { @@ -13,7 +17,11 @@ export const ACTIONS: { UPDATE_MEMBER_SKILLS: 'UPDATE_MEMBER_SKILLS', SET_WORKS: 'SET_WORKS', SET_EDUCATIONS: 'SET_EDUCATIONS', + SET_PERSONALIZATION: 'SET_PERSONALIZATION', + SET_ADDRESS: 'SET_ADDRESS', + SET_CONNECT_INFO: 'SET_CONNECT_INFO', SET_LOADING_MEMBER_TRAITS: 'SET_LOADING_MEMBER_TRAITS', + SET_LOADING_MEMBER_INFO: 'SET_LOADING_MEMBER_INFO', }, } diff --git a/src/apps/onboarding/src/models/ConnectInfo.ts b/src/apps/onboarding/src/models/ConnectInfo.ts new file mode 100644 index 000000000..e7f80beec --- /dev/null +++ b/src/apps/onboarding/src/models/ConnectInfo.ts @@ -0,0 +1,10 @@ +/* eslint-disable sort-keys */ +export default interface ConnectInfo { + country: string + phoneNumber: string +} + +export const emptyConnectInfo: () => ConnectInfo = () => ({ + country: '', + phoneNumber: '', +}) diff --git a/src/apps/onboarding/src/models/AccountDetailInfo.ts b/src/apps/onboarding/src/models/MemberAddress.ts similarity index 53% rename from src/apps/onboarding/src/models/AccountDetailInfo.ts rename to src/apps/onboarding/src/models/MemberAddress.ts index 4308e7748..4c5eb8ba4 100644 --- a/src/apps/onboarding/src/models/AccountDetailInfo.ts +++ b/src/apps/onboarding/src/models/MemberAddress.ts @@ -1,20 +1,16 @@ /* eslint-disable sort-keys */ -export default interface AccountDetailInfo { +export default interface MemberAddress { streetAddr1: string streetAddr2: string city: string stateCode: string zip: string - country: string - phoneNumber: string } -export const emptyAccountDetailInfo: () => AccountDetailInfo = () => ({ +export const emptyMemberAddress: () => MemberAddress = () => ({ streetAddr1: '', streetAddr2: '', city: '', stateCode: '', zip: '', - country: '', - phoneNumber: '', }) diff --git a/src/apps/onboarding/src/models/MemberInfo.ts b/src/apps/onboarding/src/models/MemberInfo.ts new file mode 100644 index 000000000..aff63d0f2 --- /dev/null +++ b/src/apps/onboarding/src/models/MemberInfo.ts @@ -0,0 +1,22 @@ +import { MemberMaxRating, MemberEmsiSkill } from "~/apps/talent-search/src/lib/models" +import { MemberStats } from "~/libs/core" +import MemberAddress from "./MemberAddress" + + +export default interface MemberInfo { + userId: number + handle: string + status: string + firstName: string + lastName: string + competitionCountryCode: string + email: string + accountAge: number + maxRating: MemberMaxRating + emsiSkills: Array + stats: Array + addresses?: MemberAddress[] + country: string + photoURL: string + createdAt: number +} diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx index 22861c992..bf9048068 100644 --- a/src/apps/onboarding/src/pages/account-details/index.tsx +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -3,29 +3,295 @@ /* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' import { connect } from 'react-redux' -import { FC, useState } from 'react' +import { FC, useEffect, useRef, useState } from 'react' import _ from 'lodash' import classNames from 'classnames' -import { Button, PageDivider } from '~/libs/ui' +import { Button, InputSelect, PageDivider } from '~/libs/ui' import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' -import AccountDetailInfo from '../../models/AccountDetailInfo' +import InputTextAutoSave from '../../components/InputTextAutoSave' +import { validatePhonenumber } from '../../utils/validation' +import { getCountryLookup } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' +import { createMemberConnectInfos, updateMemberConnectInfos, updateMemberHomeAddresss } from '../../redux/actions/member' +import MemberAddress, { emptyMemberAddress } from '../../models/MemberAddress' +import ConnectInfo, { emptyConnectInfo } from '../../models/ConnectInfo' + +const blankMemberAddress: MemberAddress = emptyMemberAddress() +const blankConnectInfo: ConnectInfo = emptyConnectInfo() const PageAccountDetailsContent: FC<{ + reduxAddress: MemberAddress | null + reduxConnectInfo: ConnectInfo | null + updateMemberConnectInfos: (infos: ConnectInfo[]) => void + createMemberConnectInfos: (infos: ConnectInfo[]) => void + updateMemberHomeAddresss: (infos: MemberAddress[]) => void + loadingMemberInfo: boolean + loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() - const [loading, setLoading] = useState(false) - const [accountDetailInfo, setAccountDetailInfo] = useState(null) + const [loadingAddress, setLoadingAddress] = useState(false) + const [loadingConnectInfo, setLoadingConnectInfo] = useState(false) + const [memberAddress, setMemberAddress] = useState(null) + const [connectInfo, setConnectInfo] = useState(null) + const [formErrors, setFormErrors] = useState({}) + const [countryOptions, setCountryOptions] = useState<{ + label: string + value: string + }[]>([]); + const shouldSavingAddressData = useRef(false) + const shouldSavingConnectInfoData = useRef(false) + const shouldNavigateTo = useRef('') + + const validateField: any = () => { + const errorTmp: any = {} + if (!validatePhonenumber(connectInfo?.phoneNumber || '')) { + errorTmp.phoneNumber = 'Invalid phone number' + } + + setFormErrors(errorTmp) + return _.isEmpty(errorTmp) + } + + const saveConnectInfoData: any = async () => { + if (!connectInfo || !validateField()) { + return + } + setLoadingConnectInfo(true) + if (!props.reduxConnectInfo) { + await props.createMemberConnectInfos([connectInfo]) + } else { + await props.updateMemberConnectInfos([connectInfo]) + } + + setLoadingConnectInfo(false) + } + + useEffect(() => { + if (!!connectInfo && validateField() && !_.isEqual(props.reduxConnectInfo, connectInfo)) { + if (loadingConnectInfo) { + shouldSavingConnectInfoData.current = true + return + } + saveConnectInfoData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [connectInfo]) + + const saveAddressData: any = async () => { + if (!memberAddress) { + return + } + setLoadingAddress(true) + await props.updateMemberHomeAddresss([memberAddress]) + + setLoadingAddress(false) + } + + useEffect(() => { + if (!!memberAddress && !_.isEqual(props.reduxAddress, memberAddress)) { + if (loadingAddress) { + shouldSavingAddressData.current = true + return + } + saveAddressData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [memberAddress]) + + useEffect(() => { + const doSaveAddress = !loadingAddress && shouldSavingAddressData.current + if (doSaveAddress) { + shouldSavingAddressData.current = false + saveAddressData() + .then(_.noop) + } + const doSaveConnectData = !loadingConnectInfo && shouldSavingConnectInfoData.current + if (doSaveConnectData) { + shouldSavingConnectInfoData.current = false + saveConnectInfoData() + .then(_.noop) + } + + if ( + !shouldSavingAddressData.current + && !shouldSavingConnectInfoData.current + && !loadingAddress + && !loadingConnectInfo + && shouldNavigateTo.current) { + navigate(shouldNavigateTo.current) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [loadingAddress, loadingConnectInfo]) + + // Get all countries + useEffect(() => { + getCountryLookup() + .then((results) => { + if (results) { + setCountryOptions(_.sortBy(results, 'country').map((country: any) => ({ + value: country.country, + label: country.country, + }))); + } + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.log(e); + }); + }, []); + + useEffect(() => { + if (!memberAddress && props.reduxAddress) { + setMemberAddress(props.reduxAddress) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [props.reduxAddress]) + + useEffect(() => { + if (!connectInfo && props.reduxConnectInfo) { + setConnectInfo(props.reduxConnectInfo) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [props.reduxConnectInfo]) + return (

Final account details

-
+

Account mailing address

+ Your mailing address is required for account activation and verification to do work with Topcoder or Topcoder customers. This information will not be displayed on your profile nor to anyone visiting the Topcoder site. + + { + setMemberAddress({ + ...(memberAddress || blankMemberAddress), + streetAddr1: event || '', + }) + }} + placeholder='Address 1' + tabIndex={0} + type='text' + disabled={props.loadingMemberInfo} + /> + { + setMemberAddress({ + ...(memberAddress || blankMemberAddress), + streetAddr2: event || '', + }) + }} + placeholder='Address 2' + tabIndex={0} + type='text' + disabled={props.loadingMemberInfo} + /> +
+
+ { + setMemberAddress({ + ...(memberAddress || blankMemberAddress), + city: event || '', + }) + }} + placeholder='City' + tabIndex={0} + type='text' + disabled={props.loadingMemberInfo} + /> +
+
+ { + setMemberAddress({ + ...(memberAddress || blankMemberAddress), + stateCode: event || '', + }) + }} + placeholder='State / Province' + tabIndex={0} + type='text' + className='flex-1' + disabled={props.loadingMemberInfo} + /> +
+
+ { + setMemberAddress({ + ...(memberAddress || blankMemberAddress), + zip: event || '', + }) + }} + placeholder='Zip / Postal Code' + tabIndex={0} + type='text' + className='flex-1' + disabled={props.loadingMemberInfo} + /> +
+
+ { + setConnectInfo({ + ...(connectInfo || blankConnectInfo), + country: event.target.value, + }) + }} + name='country' + label='Country' + placeholder='Country' + disabled={props.loadingMemberTraits} + /> +

Valid mobile phone number

+ Your phone number may be necessary to validate your account and verify your identity. Topcoder will not share this information with any other parties or use this to contact you for any other reason. It will not display on your public profile nor to anyone visiting the Topcoder site. + { + setConnectInfo({ + ...(connectInfo || blankConnectInfo), + phoneNumber: event || '', + }) + }} + placeholder='Phone Number' + tabIndex={0} + type='text' + dirty + error={formErrors.phoneNumber} + disabled={props.loadingMemberTraits} + />
navigate('../personalization')} + disabled={!_.isEmpty(formErrors)} + onClick={() => { + if (loadingAddress || loadingConnectInfo) { + shouldNavigateTo.current = '../personalization' + } else { + navigate('../personalization') + } + }} > back @@ -48,7 +320,7 @@ const PageAccountDetailsContent: FC<{ size='lg' primary iconToLeft - disabled={loading} + disabled={!_.isEmpty(formErrors)} > next @@ -58,9 +330,25 @@ const PageAccountDetailsContent: FC<{ } const mapStateToProps: any = (state: any) => { + const { + loadingMemberTraits, + loadingMemberInfo, + address, + connectInfo, + }: any = state.member + + return { + loadingMemberInfo, + loadingMemberTraits, + reduxAddress: address, + reduxConnectInfo: connectInfo, + } } const mapDispatchToProps: any = { + updateMemberHomeAddresss, + updateMemberConnectInfos, + createMemberConnectInfos, } export const PageAccountDetails: any = connect(mapStateToProps, mapDispatchToProps)(PageAccountDetailsContent) diff --git a/src/apps/onboarding/src/pages/account-details/styles.module.scss b/src/apps/onboarding/src/pages/account-details/styles.module.scss index 70463203a..0c483b495 100644 --- a/src/apps/onboarding/src/pages/account-details/styles.module.scss +++ b/src/apps/onboarding/src/pages/account-details/styles.module.scss @@ -1,9 +1,14 @@ .container { min-height: 100%; + + strong { + font-weight: bold; + } } .blockContent { margin-bottom: 20px; + max-width: 772px; } .ProgressBar { diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index 0163eaf91..86d8cc77a 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -3,7 +3,7 @@ /* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' import { connect } from 'react-redux' -import { FC, useState } from 'react' +import { FC, useEffect, useRef, useState } from 'react' import _ from 'lodash' import classNames from 'classnames' @@ -17,88 +17,151 @@ import { RadioButton } from '~/apps/self-service/src/components/radio-button' import InputTextAutoSave from '../../components/InputTextAutoSave' import PersonalizationInfo, { emptyPersonalizationInfo } from '../../models/PersonalizationInfo' import InputTextareaAutoSave from '../../components/InputTextareaAutoSave' +import { createMemberPersonalizations, updateMemberPersonalizations } from '../../redux/actions/member' const RadioButtonTypescript: any = RadioButton const referAsOptions = [ { label: 'Only show handle instead of name', - key: 'handle', - value: false, + key: 'handle' }, { label: 'Show first name, last name and handle', - key: 'name', - value: false, + key: 'name' }, ] const blankPersonalizationInfo: PersonalizationInfo = emptyPersonalizationInfo() const PagePersonalizationContent: FC<{ + reduxPersonalization: PersonalizationInfo | null + updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void + createMemberPersonalizations: (infos: PersonalizationInfo[]) => void + loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() const [loading, setLoading] = useState(false) const [personalizationInfo, setPersonalizationInfo] = useState(null) + const shouldSavingData = useRef(false) + const shouldNavigateTo = useRef('') + useEffect(() => { + if (!personalizationInfo && props.reduxPersonalization) { + setPersonalizationInfo(props.reduxPersonalization) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [props.reduxPersonalization]) + + const saveData: any = async () => { + if (!personalizationInfo) { + return + } + setLoading(true) + if (!props.reduxPersonalization) { + await props.createMemberPersonalizations([personalizationInfo]) + } else { + await props.updateMemberPersonalizations([personalizationInfo]) + } + + setLoading(false) + } + + useEffect(() => { + if (!!personalizationInfo && !_.isEqual(props.reduxPersonalization, personalizationInfo)) { + if (loading) { + shouldSavingData.current = true + return + } + saveData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [personalizationInfo]) + + useEffect(() => { + if (!loading && shouldSavingData.current) { + shouldSavingData.current = false + saveData() + .then(_.noop) + } else if (!loading && !!shouldNavigateTo.current) { + navigate(shouldNavigateTo.current) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [loading]) + return (

Show us your personality!

-
+
When members personalize theirs profiles with a photo and description, they are more likely to get notices by our customers for work and opportunities. -
+
-
+

Would you like to add a "handle" to use in community communications?

Some of our members prefer to be known within our community with a "handle" or display name that is not their official name. for example: DannyCoder or ZiggyZ123. You will have an opportunity to set preference for how this is used in your profile. option.key !== personalizationInfo?.referAs ? option : ({ + ...option, + value: true, + }))} onChange={(newValues: any) => { - console.log('totest newValues', newValues) + const matchValue = _.find(newValues, { value: true }) + if (matchValue) { + const matchOption = _.find(referAsOptions, { label: matchValue.label }) + if (matchOption) { + setPersonalizationInfo({ + ...(personalizationInfo || blankPersonalizationInfo), + referAs: matchOption.key, + }) + } + } }} - disabled={loading} + disabled={props.loadingMemberTraits} />
-

Give yourself a professional title

+

Give yourself a professional title

An industry standard title will help employers know your general area of expertise. { + onChange={value => { setPersonalizationInfo({ ...(personalizationInfo || blankPersonalizationInfo), - profileSelfTitle: event.target.value, + profileSelfTitle: value || '', }) }} placeholder='Company Name' tabIndex={0} type='text' + disabled={props.loadingMemberTraits} /> -

Write a short biography

+

Write a short biography

Describe yourself in your own words. A short bio will give potential customers a sense of who you are. { + value={personalizationInfo?.shortBio || ''} + onChange={value => { setPersonalizationInfo({ ...(personalizationInfo || blankPersonalizationInfo), - profileSelfTitle: event.target.value, + shortBio: value || '', }) }} placeholder='Describe yourself and your work' tabIndex={0} + disabled={props.loadingMemberTraits} />
@@ -113,8 +176,13 @@ const PagePersonalizationContent: FC<{ size='lg' primary iconToLeft - disabled={loading} - onClick={() => navigate('../educations')} + onClick={() => { + if (loading) { + shouldNavigateTo.current = '../educations' + } else { + navigate('../educations') + } + }} > back @@ -122,8 +190,13 @@ const PagePersonalizationContent: FC<{ size='lg' primary iconToLeft - disabled={loading} - onClick={() => navigate('../account-details')} + onClick={() => { + if (loading) { + shouldNavigateTo.current = '../account-details' + } else { + navigate('../account-details') + } + }} > next @@ -133,9 +206,20 @@ const PagePersonalizationContent: FC<{ } const mapStateToProps: any = (state: any) => { + const { + loadingMemberTraits, + personalization, + }: any = state.member + + return { + loadingMemberTraits, + reduxPersonalization: personalization, + } } const mapDispatchToProps: any = { + createMemberPersonalizations, + updateMemberPersonalizations, } export const PagePersonalization: any = connect(mapStateToProps, mapDispatchToProps)(PagePersonalizationContent) diff --git a/src/apps/onboarding/src/pages/personalization/styles.module.scss b/src/apps/onboarding/src/pages/personalization/styles.module.scss index 70463203a..77bf81c92 100644 --- a/src/apps/onboarding/src/pages/personalization/styles.module.scss +++ b/src/apps/onboarding/src/pages/personalization/styles.module.scss @@ -10,3 +10,7 @@ margin-top: auto; margin-bottom: 32px; } + +.blockHandleSelect { + max-width: 621px; +} diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 429b4e44a..106601ac3 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -8,7 +8,6 @@ import _ from 'lodash' import AsyncSelect from 'react-select/async' import classNames from 'classnames' -import { Member } from '~/apps/talent-search/src/lib/models' import { Button, PageDivider } from '~/libs/ui' import { ProgressBar } from '../../components/progress-bar' @@ -18,9 +17,10 @@ import SkillInfo from '../../models/SkillInfo' import SkillTag from '../../components/skill-tag' import styles from './styles.module.scss' +import MemberInfo from '../../models/MemberInfo' const PageSkillsContent: FC<{ - memberInfo?: Member, + memberInfo?: MemberInfo, updateMemberSkills: (skills: SkillInfo[]) => void }> = props => { const navigate: any = useNavigate() diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index 4eeafa6db..b1a313720 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -2,7 +2,6 @@ /* eslint-disable react/jsx-no-bind */ /* eslint-disable sort-keys */ import moment from 'moment' -import { Member } from '~/apps/talent-search/src/lib/models' import { getAsync as getAsyncToken } from '~/libs/core/lib/auth/token-functions/token.functions' import { TokenModel } from '~/libs/core' import { @@ -15,8 +14,13 @@ import SkillInfo from '../../models/SkillInfo' import { getMemberInfo, getMemberTraits, putMemberInfo } from '../../services/members' import WorkInfo from '../../models/WorkInfo' import EducationInfo from '../../models/EducationInfo' +import PersonalizationInfo from '../../models/PersonalizationInfo' +import MemberAddress from '../../models/MemberAddress' +import MemberInfo from '../../models/MemberInfo' +import ConnectInfo from '../../models/ConnectInfo' +import _ from 'lodash' -export const updateMemberInfo: any = (memberInfo: Member) => ({ +export const updateMemberInfo: any = (memberInfo: MemberInfo) => ({ type: ACTIONS.MEMBER.GET_MEMBER, payload: memberInfo, }) @@ -31,18 +35,60 @@ export const updateEducations: any = (educations: EducationInfo[]) => ({ payload: educations, }) +export const updatePersonalization: any = (personalization: PersonalizationInfo) => ({ + type: ACTIONS.MEMBER.SET_PERSONALIZATION, + payload: personalization, +}) + +export const updateConnectInfo: any = (connectInfo: ConnectInfo) => ({ + type: ACTIONS.MEMBER.SET_CONNECT_INFO, + payload: connectInfo, +}) + +export const updateAddress: any = (address: MemberAddress) => ({ + type: ACTIONS.MEMBER.SET_ADDRESS, + payload: address, +}) + export const updateLoadingMemberTraits: any = (loading: boolean) => ({ type: ACTIONS.MEMBER.SET_LOADING_MEMBER_TRAITS, payload: loading, }) + +export const updateLoadingMemberInfo: any = (loading: boolean) => ({ + type: ACTIONS.MEMBER.SET_LOADING_MEMBER_INFO, + payload: loading, +}) export const fetchMemberInfo: any = () => async (dispatch: any) => { + let tokenInfo: TokenModel + let memberInfo: MemberInfo | null = null + dispatch(updateLoadingMemberInfo(true)) try { - const tokenInfo: TokenModel = await getAsyncToken() - const memberInfo: Member = await getMemberInfo(tokenInfo.handle || '') - dispatch(updateMemberInfo(memberInfo)) + tokenInfo = await getAsyncToken() + memberInfo = await getMemberInfo(tokenInfo.handle || '') } catch (error) { } + + dispatch(updateLoadingMemberInfo(false)) + if (memberInfo) { + dispatch(updateMemberInfo(memberInfo)) + + if (memberInfo.addresses) { + const addresses = memberInfo.addresses.map(address => ({ + ...address, + streetAddr1: address.streetAddr1, + streetAddr2: address.streetAddr2, + city: address.city, + stateCode: address.stateCode, + zip: address.zip, + })) + const matchAddress = _.find(addresses, { type: 'HOME' }) + if (matchAddress) { + dispatch(updateAddress(matchAddress)) + } + } + } } const dateTimeToDate: any = (s: string) => (s ? new Date(s) : undefined) @@ -131,6 +177,37 @@ export const fetchMemberTraits: any = () => async (dispatch: any) => { }) dispatch(updateEducations(educations)) } + + const personalizationExp: any = memberTraits.find( + (t: any) => t.traitId === 'personalization', + ) + const personalizationExpValue: any = personalizationExp?.traits?.data + if (personalizationExpValue) { + const personalizations: PersonalizationInfo[] = personalizationExpValue.map((e: any) => { + return ({ + referAs: e.referAs, + profileSelfTitle: e.profileSelfTitle, + shortBio: e.shortBio, + ...e, + }) + }) + dispatch(updatePersonalization(personalizations[0])) + } + + const connectInfoExp: any = memberTraits.find( + (t: any) => t.traitId === 'connect_info', + ) + const connectInfoExpValue: any = connectInfoExp?.traits?.data + if (connectInfoExpValue) { + const connectInfos: ConnectInfo[] = connectInfoExpValue.map((e: any) => { + return ({ + ...e, + country: e.country, + phoneNumber: e.phoneNumber, + }) + }) + dispatch(updateConnectInfo(connectInfos[0])) + } } const createWorksPayloadData: any = (works: WorkInfo[]) => { @@ -235,6 +312,94 @@ export const createMemberEducations: any = (educations: EducationInfo[]) => asyn } } +const createPersonalizationsPayloadData: any = (personalizations: PersonalizationInfo[]) => { + const data: any = personalizations.map(personalization => { + const { + referAs, + profileSelfTitle, + shortBio, + }: any = personalization + return { + ...personalization, + referAs, + profileSelfTitle, + shortBio, + } + }) + + const payload: any = { + categoryName: 'Personalization', + traitId: 'personalization', + traits: { + data, + }, + } + return [payload] +} + +export const updateMemberPersonalizations: any = (personalizations: PersonalizationInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await updateMemberTraits(tokenInfo.handle || '', createPersonalizationsPayloadData(personalizations)) + dispatch(updatePersonalization(personalizations[0])) + } catch (error) { + } +} + +export const createMemberPersonalizations: any = (personalizations: PersonalizationInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await createMemberTraits(tokenInfo.handle || '', createPersonalizationsPayloadData(personalizations)) + dispatch(updatePersonalization(personalizations[0])) + } catch (error) { + } +} + +const createConnectInfosPayloadData: any = (connectInfos: ConnectInfo[]) => { + const data: any = connectInfos.map(connectInfo => { + const { + country, + phoneNumber, + }: any = connectInfo + return { + ...connectInfo, + country, + phoneNumber, + } + }) + + const payload: any = { + categoryName: 'Connect User Information', + traitId: 'connect_info', + traits: { + data, + }, + } + return [payload] +} + +export const updateMemberConnectInfos: any = (connectInfos: ConnectInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await updateMemberTraits(tokenInfo.handle || '', createConnectInfosPayloadData(connectInfos)) + dispatch(updateConnectInfo(connectInfos[0])) + } catch (error) { + } +} + +export const createMemberConnectInfos: any = (connectInfos: ConnectInfo[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + + await createMemberTraits(tokenInfo.handle || '', createConnectInfosPayloadData(connectInfos)) + dispatch(updateConnectInfo(connectInfos[0])) + } catch (error) { + } +} + export const setMemberSkills: any = (skills: SkillInfo[]) => ({ type: ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS, payload: skills, @@ -256,3 +421,22 @@ export const updateMemberSkills: any = (skills: SkillInfo[]) => async (dispatch: } catch (error) { } } + +export const updateMemberHomeAddresss: any = (addresses: MemberAddress[]) => async (dispatch: any) => { + try { + const tokenInfo: TokenModel = await getAsyncToken() + await putMemberInfo(tokenInfo.handle || '', { + addresses: addresses.map(address => ({ + ...address, + streetAddr1: address.streetAddr1, + streetAddr2: address.streetAddr2, + city: address.city, + stateCode: address.stateCode, + zip: address.zip, + type: 'HOME', + })), + }) + dispatch(updateAddress(addresses[0])) + } catch (error) { + } +} diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index 18e173bd3..081eed3bf 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -5,11 +5,19 @@ import { Member } from '~/apps/talent-search/src/lib/models' import { ACTIONS } from '../../config' import WorkInfo from '../../models/WorkInfo' import EducationInfo from '../../models/EducationInfo' +import PersonalizationInfo from '../../models/PersonalizationInfo' +import MemberAddress from '../../models/MemberAddress' +import ConnectInfo from '../../models/ConnectInfo' const initialState: { - memberInfo?: Member - works?: WorkInfo[] - educations?: EducationInfo[] + memberInfo?: Member + works?: WorkInfo[] + educations?: EducationInfo[] + personalization?: PersonalizationInfo + address?: MemberAddress + connectInfo?: ConnectInfo + loadingMemberTraits?: boolean + loadingMemberInfo?: boolean } = { } @@ -25,11 +33,31 @@ const memberReducer: any = (state = initialState, action: { type: any; payload: ...state, works: action.payload, } + case ACTIONS.MEMBER.SET_PERSONALIZATION: + return { + ...state, + personalization: action.payload, + } + case ACTIONS.MEMBER.SET_ADDRESS: + return { + ...state, + address: action.payload, + } + case ACTIONS.MEMBER.SET_CONNECT_INFO: + return { + ...state, + connectInfo: action.payload, + } case ACTIONS.MEMBER.SET_LOADING_MEMBER_TRAITS: return { ...state, loadingMemberTraits: action.payload, } + case ACTIONS.MEMBER.SET_LOADING_MEMBER_INFO: + return { + ...state, + loadingMemberInfo: action.payload, + } case ACTIONS.MEMBER.SET_EDUCATIONS: return { ...state, diff --git a/src/apps/onboarding/src/services/members.ts b/src/apps/onboarding/src/services/members.ts index c35aa9548..293a439ca 100644 --- a/src/apps/onboarding/src/services/members.ts +++ b/src/apps/onboarding/src/services/members.ts @@ -1,8 +1,8 @@ import { getAsync, putAsync } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' import { profile } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config' -import { Member } from '~/apps/talent-search/src/lib/models' +import MemberInfo from '../models/MemberInfo' -export async function getMemberInfo(handle: string): Promise { +export async function getMemberInfo(handle: string): Promise { return getAsync(profile(handle)) } @@ -10,6 +10,6 @@ export async function getMemberTraits(handle: string): Promise { return getAsync(`${profile(handle)}/traits`) } -export async function putMemberInfo(handle: string, data: any): Promise { +export async function putMemberInfo(handle: string, data: any): Promise { return putAsync(profile(handle || ''), data) } diff --git a/src/apps/onboarding/src/styles/global/_flex.scss b/src/apps/onboarding/src/styles/global/_flex.scss index b4d3ec1a5..f3e92b0ff 100644 --- a/src/apps/onboarding/src/styles/global/_flex.scss +++ b/src/apps/onboarding/src/styles/global/_flex.scss @@ -7,9 +7,15 @@ .gap-20 { gap: 20px; } +.gap-30 { + gap: 30px; +} .gap-50 { gap: 50px; } +.gap-100 { + gap: 100px; +} .d-flex { display: flex; diff --git a/src/apps/onboarding/src/utils/validation.ts b/src/apps/onboarding/src/utils/validation.ts new file mode 100644 index 000000000..aa01f7cd3 --- /dev/null +++ b/src/apps/onboarding/src/utils/validation.ts @@ -0,0 +1,11 @@ +export const validatePhonenumber: (phone: string) => boolean = (phone: string) => { + if (!phone) { + return true + } + if (/[- +()0-9]+/.test(phone)) { + return true + } + + return false; + +} \ No newline at end of file From eb75c699deb7a13717825c9f5522c811e93d43e9 Mon Sep 17 00:00:00 2001 From: dat Date: Wed, 28 Jun 2023 10:02:32 +0700 Subject: [PATCH 12/25] Onboarding: page 5,6/7 --- .../src/components/FieldAvatar/index.tsx | 83 +++++++++++++++++-- .../components/FieldAvatar/styles.module.scss | 19 +++++ .../components/InputTextAutoSave/index.tsx | 1 - .../InputTextareaAutoSave/index.tsx | 1 - src/apps/onboarding/src/models/MemberInfo.ts | 8 +- .../onboarding/src/models/SelectOption.ts | 5 ++ src/apps/onboarding/src/onboarding.routes.tsx | 10 ++- .../src/pages/account-details/index.tsx | 61 +++++++++----- .../src/pages/personalization/index.tsx | 68 ++++++++++----- .../onboarding/src/redux/actions/member.ts | 35 ++++---- src/apps/onboarding/src/services/members.ts | 1 + src/apps/onboarding/src/utils/validation.ts | 6 +- 12 files changed, 217 insertions(+), 81 deletions(-) create mode 100644 src/apps/onboarding/src/models/SelectOption.ts diff --git a/src/apps/onboarding/src/components/FieldAvatar/index.tsx b/src/apps/onboarding/src/components/FieldAvatar/index.tsx index d65383d12..7a9119d48 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/index.tsx +++ b/src/apps/onboarding/src/components/FieldAvatar/index.tsx @@ -7,28 +7,89 @@ * * A Form Field Is a wrapper for input to add the label to it */ -import React, { FC } from 'react' +import React, { Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useRef, useState } from 'react' import classNames from 'classnames' +import _ from 'lodash' -import styles from './styles.module.scss' import { Button, IconOutline } from '~/libs/ui' +import { updateMemberPhotoAsync } from '~/libs/core' + +import styles from './styles.module.scss' +import MemberInfo from '../../models/MemberInfo' interface FieldAvatarProps { className?: string + memberInfo?: MemberInfo, } const FieldAvatar: FC = ({ className, - ...props + memberInfo, }: FieldAvatarProps) => { + const fileElRef: MutableRefObject = useRef() + const [imgUrl, setImgUrl] = useState('') + useEffect(() => { + if (memberInfo && !imgUrl) { + setImgUrl(memberInfo.photoURL) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [memberInfo]) + + const [file, setFile]: [File | undefined, Dispatch>] + = useState(undefined) + const [isSaving, setIsSaving]: [boolean, Dispatch>] + = useState(false) + + function handleFilePickChange(event: React.ChangeEvent): void { + const pickedFile: File | undefined = event.target.files?.[0] + + if (pickedFile && pickedFile?.size < 2000000) { // max 2mb limit + setFile(pickedFile) + } else { + setFile(undefined) + } + } + + async function handleModifyPhotoSave(): Promise { + const formData: FormData = new FormData() + + if (file && memberInfo) { + formData.append('photo', file) + + setIsSaving(true) + try { + await updateMemberPhotoAsync(memberInfo.handle, formData) + } catch (error) { + } + + setIsSaving(false) + } + } + + useEffect(() => { + if (file) { + setImgUrl(URL.createObjectURL(file)) + handleModifyPhotoSave() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [file]) + return (

A picture can speek a thousand words

-
-
- +
+
+ {imgUrl ? (avatar) : null}
Requirements: @@ -38,15 +99,23 @@ const FieldAvatar: FC = ({
+ -
) } diff --git a/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss b/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss index 89eae3a2a..20f0c4a3d 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss +++ b/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss @@ -7,4 +7,23 @@ list-style: unset; padding-left: 30px; } +} + +.blockImg { + width: 200px; + height: 200px; + display: flex; + background-color: #eee; + border: 1px dashed gray; + + &.haveImg { + border: none; + background-color: transparent; + } + + img { + width: 100%; + height: 100%; + object-fit: contain; + } } \ No newline at end of file diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx index f704a7169..89c348eb1 100644 --- a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx @@ -11,7 +11,6 @@ import React, { FC, useEffect, useState } from 'react' import { InputText, InputValue } from '~/libs/ui' import { InputTextTypes } from '~/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText' - export interface InputTextProps { readonly checked?: boolean readonly className?: string diff --git a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx index a60147448..425abd003 100644 --- a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx @@ -10,7 +10,6 @@ import React, { FC, useEffect, useState } from 'react' import { InputTextarea } from '~/libs/ui' - export interface InputTextareaProps { readonly dirty?: boolean readonly disabled?: boolean diff --git a/src/apps/onboarding/src/models/MemberInfo.ts b/src/apps/onboarding/src/models/MemberInfo.ts index aff63d0f2..ff524419b 100644 --- a/src/apps/onboarding/src/models/MemberInfo.ts +++ b/src/apps/onboarding/src/models/MemberInfo.ts @@ -1,7 +1,7 @@ -import { MemberMaxRating, MemberEmsiSkill } from "~/apps/talent-search/src/lib/models" -import { MemberStats } from "~/libs/core" -import MemberAddress from "./MemberAddress" - +/* eslint-disable ordered-imports/ordered-imports */ +import { MemberMaxRating, MemberEmsiSkill } from '~/apps/talent-search/src/lib/models' +import { MemberStats } from '~/libs/core' +import MemberAddress from './MemberAddress' export default interface MemberInfo { userId: number diff --git a/src/apps/onboarding/src/models/SelectOption.ts b/src/apps/onboarding/src/models/SelectOption.ts new file mode 100644 index 000000000..b1d94d69a --- /dev/null +++ b/src/apps/onboarding/src/models/SelectOption.ts @@ -0,0 +1,5 @@ +/* eslint-disable sort-keys */ +export default interface SelectOption { + label: string + key: string +} diff --git a/src/apps/onboarding/src/onboarding.routes.tsx b/src/apps/onboarding/src/onboarding.routes.tsx index 052b8ee3a..5559c8516 100644 --- a/src/apps/onboarding/src/onboarding.routes.tsx +++ b/src/apps/onboarding/src/onboarding.routes.tsx @@ -8,8 +8,14 @@ const PageStart: LazyLoadedComponent = lazyLoad(() => import('./pages/start/inde const PageSkills: LazyLoadedComponent = lazyLoad(() => import('./pages/skills/index'), 'PageSkills') const PageWorks: LazyLoadedComponent = lazyLoad(() => import('./pages/works/index'), 'PageWorks') const PageEducations: LazyLoadedComponent = lazyLoad(() => import('./pages/educations/index'), 'PageEducations') -const PagePersonalization: LazyLoadedComponent = lazyLoad(() => import('./pages/personalization/index'), 'PagePersonalization') -const PageAccountDetails: LazyLoadedComponent = lazyLoad(() => import('./pages/account-details/index'), 'PageAccountDetails') +const PagePersonalization: LazyLoadedComponent = lazyLoad( + () => import('./pages/personalization/index'), + 'PagePersonalization', +) +const PageAccountDetails: LazyLoadedComponent = lazyLoad( + () => import('./pages/account-details/index'), + 'PageAccountDetails', +) const toolTitle: string = ToolTitle.onboarding const onboardingRootRoute: string = '/onboarding' diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx index bf9048068..7c1d5d082 100644 --- a/src/apps/onboarding/src/pages/account-details/index.tsx +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -1,21 +1,26 @@ /* eslint-disable ordered-imports/ordered-imports */ /* eslint-disable react/jsx-no-bind */ /* eslint-disable unicorn/no-null */ +/* eslint-disable sort-keys */ import { useNavigate } from 'react-router-dom' import { connect } from 'react-redux' -import { FC, useEffect, useRef, useState } from 'react' +import { FC, MutableRefObject, useEffect, useRef, useState } from 'react' import _ from 'lodash' import classNames from 'classnames' import { Button, InputSelect, PageDivider } from '~/libs/ui' +import { getCountryLookup } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' import InputTextAutoSave from '../../components/InputTextAutoSave' import { validatePhonenumber } from '../../utils/validation' -import { getCountryLookup } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' -import { createMemberConnectInfos, updateMemberConnectInfos, updateMemberHomeAddresss } from '../../redux/actions/member' +import { + createMemberConnectInfos, + updateMemberConnectInfos, + updateMemberHomeAddresss, +} from '../../redux/actions/member' import MemberAddress, { emptyMemberAddress } from '../../models/MemberAddress' import ConnectInfo, { emptyConnectInfo } from '../../models/ConnectInfo' @@ -40,10 +45,10 @@ const PageAccountDetailsContent: FC<{ const [countryOptions, setCountryOptions] = useState<{ label: string value: string - }[]>([]); - const shouldSavingAddressData = useRef(false) - const shouldSavingConnectInfoData = useRef(false) - const shouldNavigateTo = useRef('') + }[]>([]) + const shouldSavingAddressData: MutableRefObject = useRef(false) + const shouldSavingConnectInfoData: MutableRefObject = useRef(false) + const shouldNavigateTo: MutableRefObject = useRef('') const validateField: any = () => { const errorTmp: any = {} @@ -59,6 +64,7 @@ const PageAccountDetailsContent: FC<{ if (!connectInfo || !validateField()) { return } + setLoadingConnectInfo(true) if (!props.reduxConnectInfo) { await props.createMemberConnectInfos([connectInfo]) @@ -75,6 +81,7 @@ const PageAccountDetailsContent: FC<{ shouldSavingConnectInfoData.current = true return } + saveConnectInfoData() .then(_.noop) } @@ -85,6 +92,7 @@ const PageAccountDetailsContent: FC<{ if (!memberAddress) { return } + setLoadingAddress(true) await props.updateMemberHomeAddresss([memberAddress]) @@ -97,6 +105,7 @@ const PageAccountDetailsContent: FC<{ shouldSavingAddressData.current = true return } + saveAddressData() .then(_.noop) } @@ -104,13 +113,14 @@ const PageAccountDetailsContent: FC<{ }, [memberAddress]) useEffect(() => { - const doSaveAddress = !loadingAddress && shouldSavingAddressData.current + const doSaveAddress: boolean = !loadingAddress && shouldSavingAddressData.current if (doSaveAddress) { shouldSavingAddressData.current = false saveAddressData() .then(_.noop) } - const doSaveConnectData = !loadingConnectInfo && shouldSavingConnectInfoData.current + + const doSaveConnectData: boolean = !loadingConnectInfo && shouldSavingConnectInfoData.current if (doSaveConnectData) { shouldSavingConnectInfoData.current = false saveConnectInfoData() @@ -131,19 +141,17 @@ const PageAccountDetailsContent: FC<{ // Get all countries useEffect(() => { getCountryLookup() - .then((results) => { + .then(results => { if (results) { - setCountryOptions(_.sortBy(results, 'country').map((country: any) => ({ - value: country.country, - label: country.country, - }))); + setCountryOptions(_.sortBy(results, 'country') + .map((country: any) => ({ + value: country.country, + label: country.country, + }))) } }) - .catch((e) => { - // eslint-disable-next-line no-console - console.log(e); - }); - }, []); + .catch(_.noop) + }, []) useEffect(() => { if (!memberAddress && props.reduxAddress) { @@ -159,14 +167,18 @@ const PageAccountDetailsContent: FC<{ /* eslint-disable react-hooks/exhaustive-deps */ }, [props.reduxConnectInfo]) - return (

Final account details

Account mailing address

- Your mailing address is required for account activation and verification to do work with Topcoder or Topcoder customers. This information will not be displayed on your profile nor to anyone visiting the Topcoder site. + + Your mailing address is required for account activation and verification to do work with Topcoder or + Topcoder customers. This information + will not + be displayed on your profile nor to anyone visiting the Topcoder site. +

Valid mobile phone number

- Your phone number may be necessary to validate your account and verify your identity. Topcoder will not share this information with any other parties or use this to contact you for any other reason. It will not display on your public profile nor to anyone visiting the Topcoder site. + + Your phone number may be necessary to validate your account and verify your identity. + Topcoder will not share this information with any other parties or use this to contact + you for any other reason. It will not display on your public profile nor to anyone + visiting the Topcoder site. + void createMemberPersonalizations: (infos: PersonalizationInfo[]) => void @@ -43,8 +46,9 @@ const PagePersonalizationContent: FC<{ const navigate: any = useNavigate() const [loading, setLoading] = useState(false) const [personalizationInfo, setPersonalizationInfo] = useState(null) - const shouldSavingData = useRef(false) - const shouldNavigateTo = useRef('') + const shouldSavingData: MutableRefObject = useRef(false) + const shouldNavigateTo: MutableRefObject = useRef('') + useEffect(() => { if (!personalizationInfo && props.reduxPersonalization) { setPersonalizationInfo(props.reduxPersonalization) @@ -56,6 +60,7 @@ const PagePersonalizationContent: FC<{ if (!personalizationInfo) { return } + setLoading(true) if (!props.reduxPersonalization) { await props.createMemberPersonalizations([personalizationInfo]) @@ -72,6 +77,7 @@ const PagePersonalizationContent: FC<{ shouldSavingData.current = true return } + saveData() .then(_.noop) } @@ -89,33 +95,46 @@ const PagePersonalizationContent: FC<{ /* eslint-disable react-hooks/exhaustive-deps */ }, [loading]) - return (

Show us your personality!

- When members personalize theirs profiles with a photo and description, - they are more likely to get notices by our customers for work and opportunities. + + When members personalize theirs profiles with a photo and description, + they are more likely to get notices by our customers for work and opportunities. +
- +
-

Would you like to add a "handle" to use in community communications?

- Some of our members prefer to be known within our community with a "handle" or display name that is not their official name. - for example: DannyCoder or ZiggyZ123. You will have an opportunity to set preference for how this is used in your profile. +

Would you like to add a "handle" to use in community communications?

+ + Some of our members prefer to be known within our community with a "handle" + or display name that is not their official name. + for example: DannyCoder or ZiggyZ123. You will have an opportunity to + set preference for how this is used in your profile. option.key !== personalizationInfo?.referAs ? option : ({ - ...option, - value: true, - }))} + options={referAsOptions.map(option => (( + option.key !== personalizationInfo?.referAs) ? option : ({ + ...option, + value: true, + })))} onChange={(newValues: any) => { - const matchValue = _.find(newValues, { value: true }) + const matchValue: SelectOption = _.find( + newValues, + { value: true }, + ) as SelectOption if (matchValue) { - const matchOption = _.find(referAsOptions, { label: matchValue.label }) + const matchOption: SelectOption = _.find( + referAsOptions, + { label: matchValue.label }, + ) as SelectOption if (matchOption) { setPersonalizationInfo({ ...(personalizationInfo || blankPersonalizationInfo), @@ -148,7 +167,10 @@ const PagePersonalizationContent: FC<{ />

Write a short biography

- Describe yourself in your own words. A short bio will give potential customers a sense of who you are. + + Describe yourself in your own words. A short bio will give + potential customers a sense of who you are. + { const { loadingMemberTraits, personalization, + memberInfo, }: any = state.member return { + memberInfo, loadingMemberTraits, reduxPersonalization: personalization, } diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index b1a313720..a729c3fb1 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -1,7 +1,10 @@ /* eslint-disable ordered-imports/ordered-imports */ /* eslint-disable react/jsx-no-bind */ /* eslint-disable sort-keys */ +/* eslint-disable unicorn/no-null */ import moment from 'moment' +import _ from 'lodash' + import { getAsync as getAsyncToken } from '~/libs/core/lib/auth/token-functions/token.functions' import { TokenModel } from '~/libs/core' import { @@ -18,7 +21,6 @@ import PersonalizationInfo from '../../models/PersonalizationInfo' import MemberAddress from '../../models/MemberAddress' import MemberInfo from '../../models/MemberInfo' import ConnectInfo from '../../models/ConnectInfo' -import _ from 'lodash' export const updateMemberInfo: any = (memberInfo: MemberInfo) => ({ type: ACTIONS.MEMBER.GET_MEMBER, @@ -55,7 +57,6 @@ export const updateLoadingMemberTraits: any = (loading: boolean) => ({ payload: loading, }) - export const updateLoadingMemberInfo: any = (loading: boolean) => ({ type: ACTIONS.MEMBER.SET_LOADING_MEMBER_INFO, payload: loading, @@ -75,7 +76,7 @@ export const fetchMemberInfo: any = () => async (dispatch: any) => { dispatch(updateMemberInfo(memberInfo)) if (memberInfo.addresses) { - const addresses = memberInfo.addresses.map(address => ({ + const addresses: MemberAddress[] = memberInfo.addresses.map(address => ({ ...address, streetAddr1: address.streetAddr1, streetAddr2: address.streetAddr2, @@ -83,7 +84,7 @@ export const fetchMemberInfo: any = () => async (dispatch: any) => { stateCode: address.stateCode, zip: address.zip, })) - const matchAddress = _.find(addresses, { type: 'HOME' }) + const matchAddress: MemberAddress = _.find(addresses, { type: 'HOME' }) as MemberAddress if (matchAddress) { dispatch(updateAddress(matchAddress)) } @@ -183,14 +184,12 @@ export const fetchMemberTraits: any = () => async (dispatch: any) => { ) const personalizationExpValue: any = personalizationExp?.traits?.data if (personalizationExpValue) { - const personalizations: PersonalizationInfo[] = personalizationExpValue.map((e: any) => { - return ({ - referAs: e.referAs, - profileSelfTitle: e.profileSelfTitle, - shortBio: e.shortBio, - ...e, - }) - }) + const personalizations: PersonalizationInfo[] = personalizationExpValue.map((e: any) => ({ + ...e, + referAs: e.referAs, + profileSelfTitle: e.profileSelfTitle, + shortBio: e.shortBio, + })) dispatch(updatePersonalization(personalizations[0])) } @@ -199,13 +198,11 @@ export const fetchMemberTraits: any = () => async (dispatch: any) => { ) const connectInfoExpValue: any = connectInfoExp?.traits?.data if (connectInfoExpValue) { - const connectInfos: ConnectInfo[] = connectInfoExpValue.map((e: any) => { - return ({ - ...e, - country: e.country, - phoneNumber: e.phoneNumber, - }) - }) + const connectInfos: ConnectInfo[] = connectInfoExpValue.map((e: any) => ({ + ...e, + country: e.country, + phoneNumber: e.phoneNumber, + })) dispatch(updateConnectInfo(connectInfos[0])) } } diff --git a/src/apps/onboarding/src/services/members.ts b/src/apps/onboarding/src/services/members.ts index 293a439ca..8c3bf26b1 100644 --- a/src/apps/onboarding/src/services/members.ts +++ b/src/apps/onboarding/src/services/members.ts @@ -1,3 +1,4 @@ +/* eslint-disable ordered-imports/ordered-imports */ import { getAsync, putAsync } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' import { profile } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config' import MemberInfo from '../models/MemberInfo' diff --git a/src/apps/onboarding/src/utils/validation.ts b/src/apps/onboarding/src/utils/validation.ts index aa01f7cd3..2edde9c8f 100644 --- a/src/apps/onboarding/src/utils/validation.ts +++ b/src/apps/onboarding/src/utils/validation.ts @@ -2,10 +2,10 @@ export const validatePhonenumber: (phone: string) => boolean = (phone: string) = if (!phone) { return true } + if (/[- +()0-9]+/.test(phone)) { return true } - return false; - -} \ No newline at end of file + return false +} From 46649bd81b38d4667edff3d989800d081b951673 Mon Sep 17 00:00:00 2001 From: dat Date: Sun, 2 Jul 2023 16:21:20 +0700 Subject: [PATCH 13/25] Minor onboarding issues --- .../src/components/FieldAvatar/index.tsx | 12 ++++++++---- src/apps/onboarding/src/config/index.ts | 2 ++ .../onboarding/src/pages/account-details/index.tsx | 2 +- src/apps/onboarding/src/pages/educations/index.tsx | 13 +++++++------ .../onboarding/src/pages/personalization/index.tsx | 13 ++++++++++--- src/apps/onboarding/src/pages/skills/index.tsx | 2 +- src/apps/onboarding/src/pages/works/index.tsx | 13 +++++++------ src/apps/onboarding/src/redux/actions/member.ts | 5 +++++ src/apps/onboarding/src/redux/reducers/member.ts | 14 ++++++++++++++ src/apps/onboarding/src/styles/global/_layout.scss | 6 +++++- 10 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/apps/onboarding/src/components/FieldAvatar/index.tsx b/src/apps/onboarding/src/components/FieldAvatar/index.tsx index 7a9119d48..ab04c651b 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/index.tsx +++ b/src/apps/onboarding/src/components/FieldAvatar/index.tsx @@ -20,11 +20,13 @@ import MemberInfo from '../../models/MemberInfo' interface FieldAvatarProps { className?: string memberInfo?: MemberInfo, + setMemberPhotoUrl: (photoUrl: string) => void } const FieldAvatar: FC = ({ className, memberInfo, + setMemberPhotoUrl, }: FieldAvatarProps) => { const fileElRef: MutableRefObject = useRef() const [imgUrl, setImgUrl] = useState('') @@ -50,7 +52,7 @@ const FieldAvatar: FC = ({ } } - async function handleModifyPhotoSave(): Promise { + async function handleModifyPhotoSave(newImgUrl: string): Promise { const formData: FormData = new FormData() if (file && memberInfo) { @@ -58,6 +60,7 @@ const FieldAvatar: FC = ({ setIsSaving(true) try { + setMemberPhotoUrl(newImgUrl) await updateMemberPhotoAsync(memberInfo.handle, formData) } catch (error) { } @@ -68,8 +71,9 @@ const FieldAvatar: FC = ({ useEffect(() => { if (file) { - setImgUrl(URL.createObjectURL(file)) - handleModifyPhotoSave() + const newImgUrl: string = URL.createObjectURL(file) + setImgUrl(newImgUrl) + handleModifyPhotoSave(newImgUrl) .then(_.noop) } /* eslint-disable react-hooks/exhaustive-deps */ @@ -79,7 +83,7 @@ const FieldAvatar: FC = ({
-

A picture can speek a thousand words

+

A picture can speak a thousand words

Your mailing address is required for account activation and verification to do work with Topcoder or Topcoder customers. This information - will not + will not be displayed on your profile nor to anyone visiting the Topcoder site. diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index e2d3fadd7..7feb275ce 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -64,6 +64,13 @@ export const PageEducationsContent: FC<{
+ {(!educations || educations.length === 0) ? ( + + Relevant education details will help make your + profile more valuable to potential employers, add it here! + + ) : null} +
{(educations || []).map(education => (
@@ -107,12 +114,6 @@ export const PageEducationsContent: FC<{ > + add education - {(!educations || educations.length === 0) ? ( - - Relevant education details will help make your - profile more valuable to potential employers, add it here! - - ) : null}
void createMemberPersonalizations: (infos: PersonalizationInfo[]) => void + setMemberPhotoUrl: (photoUrl: string) => void loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() @@ -102,12 +107,13 @@ const PagePersonalizationContent: FC<{
When members personalize theirs profiles with a photo and description, - they are more likely to get notices by our customers for work and opportunities. + they are more likely to get noticed by our customers for work and opportunities.
@@ -160,7 +166,7 @@ const PagePersonalizationContent: FC<{ profileSelfTitle: value || '', }) }} - placeholder='Company Name' + placeholder='Title' tabIndex={0} type='text' disabled={props.loadingMemberTraits} @@ -244,6 +250,7 @@ const mapStateToProps: any = (state: any) => { const mapDispatchToProps: any = { createMemberPersonalizations, updateMemberPersonalizations, + setMemberPhotoUrl, } export const PagePersonalization: any = connect(mapStateToProps, mapDispatchToProps)(PagePersonalizationContent) diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 106601ac3..6e0d4dd4f 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -87,7 +87,7 @@ const PageSkillsContent: FC<{ cacheOptions autoFocus defaultOptions - placeholder='Start typing to autocomplete available EMSI skills' + placeholder='Type to add a skill...' loadOptions={autoCompleteSkills} name='skills' className='basic-multi-select mt-30' diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index 9348be2e2..692cb4b49 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -65,6 +65,13 @@ export const PageWorksContent: FC<{
+ {(!works || works.length === 0) ? ( + + You will be able to add details for each of the work experiences + that you think will demonstrate your abilities. + + ) : null} +
{(works || []).map(work => (
@@ -116,12 +123,6 @@ export const PageWorksContent: FC<{ > + add work experience - {(!works || works.length === 0) ? ( - - You will be able to add details for each of the work experiences - that you think will demonstrate your abilities. - - ) : null}
diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index a729c3fb1..c7a731566 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -402,6 +402,11 @@ export const setMemberSkills: any = (skills: SkillInfo[]) => ({ payload: skills, }) +export const setMemberPhotoUrl: any = (photoUrl: string) => ({ + type: ACTIONS.MEMBER.UPDATE_MEMBER_PHOTO_URL, + payload: photoUrl, +}) + export const updateMemberSkills: any = (skills: SkillInfo[]) => async (dispatch: any) => { try { const tokenInfo: TokenModel = await getAsyncToken() diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index 081eed3bf..b710353e2 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -77,6 +77,20 @@ const memberReducer: any = (state = initialState, action: { type: any; payload: } } + case ACTIONS.MEMBER.UPDATE_MEMBER_PHOTO_URL: { + if (!state.memberInfo) { + return state + } + + return { + ...state, + memberInfo: { + ...state.memberInfo, + photoURL: action.payload, + }, + } + } + default: return state } diff --git a/src/apps/onboarding/src/styles/global/_layout.scss b/src/apps/onboarding/src/styles/global/_layout.scss index 03cc86de4..de1668e16 100644 --- a/src/apps/onboarding/src/styles/global/_layout.scss +++ b/src/apps/onboarding/src/styles/global/_layout.scss @@ -1,3 +1,7 @@ .mt-30 { margin-top: 30px; -} \ No newline at end of file +} + +.mb-30 { + margin-bottom: 30px; +} From 0d5a14f866080cb921987a2c7d5533a4f9357831 Mon Sep 17 00:00:00 2001 From: dat Date: Thu, 6 Jul 2023 00:43:55 +0700 Subject: [PATCH 14/25] Merge branch 'skill-selector-input' into onboarding # Conflicts: # yarn.lock --- .storybook/main.ts | 10 +- .vscode/components.code-snippets | 2 +- src/.eslintrc.js | 2 +- .../tabs/preferences/PreferencesTab.tsx | 2 +- .../src/components/skill-tag/index.tsx | 32 -- .../components/skill-tag/styles.module.scss | 23 -- src/apps/onboarding/src/config/index.ts | 2 - src/apps/onboarding/src/models/SkillInfo.ts | 7 - .../onboarding/src/pages/skills/index.tsx | 121 +----- .../onboarding/src/redux/actions/member.ts | 23 -- .../onboarding/src/redux/reducers/member.ts | 13 - src/apps/onboarding/src/services/skills.ts | 9 - .../EditMemberPropertyBtn.module.scss | 8 + .../EditMemberPropertyBtn.tsx | 3 + src/apps/profiles/src/config/constants.ts | 2 + .../src/lib/assets/profile-header-bg.png | Bin 0 -> 326896 bytes .../about-me/AboutMe.module.scss | 5 +- .../CommunityAwards.module.scss | 8 +- .../community-awards/CommunityAwards.tsx | 2 +- .../EducationAndCertifications.module.scss | 17 + .../EducationAndCertifications.tsx | 100 +++++ .../EducationCard/EducationCard.module.scss | 12 + .../EducationCard/EducationCard.tsx | 42 ++ .../EducationCard/index.ts | 1 + .../ModifyEducationModal.module.scss | 91 +++++ .../ModifyEducationModal.tsx | 318 ++++++++++++++++ .../ModifyEducationModal/index.ts | 1 + .../education-and-certifications/index.ts | 1 + .../languages/MemberLanguages.tsx | 2 +- .../ModifyLanguagesModal.module.scss | 1 + .../ModifyLanguagesModal.tsx | 26 +- .../src/member-profile/links/MemberLinks.tsx | 19 +- .../page-layout/ProfilePageLayout.module.scss | 58 ++- .../page-layout/ProfilePageLayout.tsx | 75 ++-- .../OpenForGigsModifyModal.tsx | 6 +- .../profile-header/ProfileHeader.module.scss | 53 ++- .../profile-header/ProfileHeader.tsx | 24 +- .../skills/MemberSkillsInfo.module.scss | 31 +- .../skills/MemberSkillsInfo.tsx | 76 +++- .../ModifySkillsModal.module.scss | 12 + .../ModifySkillsModal/ModifySkillsModal.tsx | 46 +++ .../skills/ModifySkillsModal/index.ts | 1 + .../ChallengeWinsBanner.module.scss | 40 ++ .../ChallengeWinsBanner.tsx | 26 ++ .../ChallengeWinsBanner/index.ts | 1 + .../MemberTCAchievements.module.scss | 10 +- .../tc-achievements/MemberTCAchievements.tsx | 35 +- .../TCOWinsBanner/TCOWinsBanner.module.scss | 39 ++ .../TCOWinsBanner/TCOWinsBanner.tsx | 25 ++ .../TCOWinsBanner/assets/tco-logo-banner.png | Bin 0 -> 12335 bytes .../tc-achievements/TCOWinsBanner/index.ts | 1 + .../tca-info/MemberTCAInfo.module.scss | 12 +- .../ModifyWorkExpirenceModal.module.scss | 91 +++++ .../ModifyWorkExpirenceModal.tsx | 360 ++++++++++++++++++ .../ModifyWorkExpirenceModal/index.ts | 1 + .../work-expirence/WorkExpirence.module.scss | 17 + .../work-expirence/WorkExpirence.tsx | 94 +++++ .../WorkExpirenceCard.module.scss | 12 + .../WorkExpirenceCard/WorkExpirenceCard.tsx | 48 +++ .../work-expirence/WorkExpirenceCard/index.ts | 1 + .../member-profile/work-expirence/index.ts | 1 + .../ProfilesLandingPage.tsx | 24 ++ .../src/profiles-landing-page/index.ts | 1 + src/apps/profiles/src/profiles.routes.tsx | 7 + .../talent-search/src/lib/models/Member.ts | 1 + .../src/lib/models/MemberEmsiSkill.ts | 2 +- .../src/lib/services/MatcherService.ts | 2 +- .../SkillSearchResults.tsx | 2 +- .../renderers/MemberHandleRenderer.tsx | 7 +- .../renderers/SkillTag.tsx | 4 +- .../profile/data-providers/useMemberSkills.ts | 1 + .../profile-functions/rating.functions.ts | 6 + .../core/lib/profile/user-profile.model.ts | 3 + src/libs/core/lib/profile/user-skill.model.ts | 17 + .../InputSkillSelector.tsx | 37 ++ .../components/input-skill-selector/index.ts | 1 + .../emsi-skills/emsi-skills.service.ts | 8 + .../shared/lib/services/emsi-skills/index.ts | 2 + .../lib/services/emsi-skills/skill.model.ts | 4 + .../form/form-groups/form-input/index.ts | 2 + .../InputDatePicker.module.scss | 73 ++++ .../input-date-picker/InputDatePicker.tsx | 114 ++++++ .../form-input/input-date-picker/index.ts | 1 + .../InputMultiselect.module.scss | 146 +++++++ .../InputMultiselect.stories.tsx | 45 +++ .../input-multiselect/InputMultiselect.tsx | 82 ++++ .../form-input/input-multiselect/index.ts | 2 + 87 files changed, 2321 insertions(+), 374 deletions(-) delete mode 100644 src/apps/onboarding/src/components/skill-tag/index.tsx delete mode 100644 src/apps/onboarding/src/components/skill-tag/styles.module.scss delete mode 100644 src/apps/onboarding/src/models/SkillInfo.ts delete mode 100644 src/apps/onboarding/src/services/skills.ts create mode 100644 src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.module.scss create mode 100644 src/apps/profiles/src/lib/assets/profile-header-bg.png create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/EducationAndCertifications.module.scss create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/EducationAndCertifications.tsx create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/EducationCard/EducationCard.module.scss create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/EducationCard/EducationCard.tsx create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/EducationCard/index.ts create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/ModifyEducationModal/ModifyEducationModal.module.scss create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/ModifyEducationModal/ModifyEducationModal.tsx create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/ModifyEducationModal/index.ts create mode 100644 src/apps/profiles/src/member-profile/education-and-certifications/index.ts create mode 100644 src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.module.scss create mode 100644 src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.tsx create mode 100644 src/apps/profiles/src/member-profile/skills/ModifySkillsModal/index.ts create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.module.scss create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.tsx create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/index.ts create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.module.scss create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.tsx create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/assets/tco-logo-banner.png create mode 100644 src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/index.ts create mode 100644 src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.module.scss create mode 100644 src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx create mode 100644 src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/index.ts create mode 100644 src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.module.scss create mode 100644 src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx create mode 100644 src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.module.scss create mode 100644 src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.tsx create mode 100644 src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/index.ts create mode 100644 src/apps/profiles/src/member-profile/work-expirence/index.ts create mode 100644 src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx create mode 100644 src/apps/profiles/src/profiles-landing-page/index.ts create mode 100644 src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx create mode 100644 src/libs/shared/lib/components/input-skill-selector/index.ts create mode 100644 src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts create mode 100644 src/libs/shared/lib/services/emsi-skills/index.ts create mode 100644 src/libs/shared/lib/services/emsi-skills/skill.model.ts create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.module.scss create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/index.ts create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.stories.tsx create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx create mode 100644 src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 26a4cb686..e5f14586f 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -32,7 +32,15 @@ const config: StorybookConfig = { }; } - return config; + return { + ...config, + plugins: config.plugins?.filter(plugin => { + if (plugin.constructor.name === 'ESLintWebpackPlugin') { + return false + } + return true + }), + }; } }; export default config; diff --git a/.vscode/components.code-snippets b/.vscode/components.code-snippets index da4851e5f..85c9b54fa 100644 --- a/.vscode/components.code-snippets +++ b/.vscode/components.code-snippets @@ -26,7 +26,7 @@ "interface ${1:ComponentName}Props {", "}", "", - "const ${1:ComponentName}: FC<${1:ComponentName}Props> = (props: ${1:ComponentName}Props) => {", + "const ${1:ComponentName}: FC<${1:ComponentName}Props> = props => {", "", " return (", "
", diff --git a/src/.eslintrc.js b/src/.eslintrc.js index c2a131980..3a4ea5127 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -71,7 +71,7 @@ module.exports = { parameter: true, memberVariableDeclaration: true, callSignature: true, - variableDeclaration: true, + variableDeclaration: false, arrayDestructuring: false, objectDestructuring: true, }, diff --git a/src/apps/accounts/src/settings/tabs/preferences/PreferencesTab.tsx b/src/apps/accounts/src/settings/tabs/preferences/PreferencesTab.tsx index a4e85542e..6b654ea36 100644 --- a/src/apps/accounts/src/settings/tabs/preferences/PreferencesTab.tsx +++ b/src/apps/accounts/src/settings/tabs/preferences/PreferencesTab.tsx @@ -23,7 +23,7 @@ const PreferencesTab: FC = (props: PreferencesTabProps) => const mailChimpFormAction: string = emailPreferences?.status === 'subscribed' ? unsubscribeLink : subscribeLink function handleGoToForumPreferences(): void { - window.open(`https://vanilla.${EnvironmentConfig.TC_DOMAIN}/profile/preferences`, '_blank') + window.open(`https://${EnvironmentConfig.ENV === 'prod' ? 'discussions' : 'vanilla'}.${EnvironmentConfig.TC_DOMAIN}/profile/preferences`, '_blank') } function handleSubscribtionStatusChange(): void { diff --git a/src/apps/onboarding/src/components/skill-tag/index.tsx b/src/apps/onboarding/src/components/skill-tag/index.tsx deleted file mode 100644 index 4b24d1934..000000000 --- a/src/apps/onboarding/src/components/skill-tag/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -import React, { FC } from 'react' -import classNames from 'classnames' - -import { Skill } from '~/apps/talent-search/src/lib/models' - -import XIcon from '../../assets/images/x-icon.svg' - -import styles from './styles.module.scss' - -interface SkillTagProps { - skill: Skill - onDelete?: () => void - disabled?: boolean -} - -const SkillTag: FC = (props: SkillTagProps) => ( -
- {props.skill.name} - -
-) - -export default SkillTag diff --git a/src/apps/onboarding/src/components/skill-tag/styles.module.scss b/src/apps/onboarding/src/components/skill-tag/styles.module.scss deleted file mode 100644 index 7f91068f5..000000000 --- a/src/apps/onboarding/src/components/skill-tag/styles.module.scss +++ /dev/null @@ -1,23 +0,0 @@ -.container { - background-color: gray; - color: white; - border-radius: 3px; - padding: 0 10px; - font-size: 14px; - gap: 5px; -} - -.btnDelete { - display: flex; - align-items: center; - justify-content: center; - background-color: white; - border-radius: 100%; - flex-shrink: 0; - width: 15px; - height: 15px; - - &:disabled { - opacity: 0.2; - } -} \ No newline at end of file diff --git a/src/apps/onboarding/src/config/index.ts b/src/apps/onboarding/src/config/index.ts index 7726b869d..1a5dd87f2 100644 --- a/src/apps/onboarding/src/config/index.ts +++ b/src/apps/onboarding/src/config/index.ts @@ -2,7 +2,6 @@ export const ACTIONS: { MEMBER: { GET_MEMBER: string; - UPDATE_MEMBER_SKILLS: string; UPDATE_MEMBER_PHOTO_URL: string; SET_WORKS: string; SET_EDUCATIONS: string; @@ -15,7 +14,6 @@ export const ACTIONS: { } = { MEMBER: { GET_MEMBER: 'GET_MEMBER', - UPDATE_MEMBER_SKILLS: 'UPDATE_MEMBER_SKILLS', UPDATE_MEMBER_PHOTO_URL: 'UPDATE_MEMBER_PHOTO_URL', SET_WORKS: 'SET_WORKS', SET_EDUCATIONS: 'SET_EDUCATIONS', diff --git a/src/apps/onboarding/src/models/SkillInfo.ts b/src/apps/onboarding/src/models/SkillInfo.ts deleted file mode 100644 index cbfb4a167..000000000 --- a/src/apps/onboarding/src/models/SkillInfo.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface SkillInfo { - category: string - emsiId: string - name: string - skillSources?: string[] - subCategory: string -} diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 6e0d4dd4f..f876579fd 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -2,50 +2,18 @@ /* eslint-disable react/jsx-no-bind */ /* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' -import { connect } from 'react-redux' -import { FC, useEffect, useState } from 'react' -import _ from 'lodash' -import AsyncSelect from 'react-select/async' +import { FC } from 'react' import classNames from 'classnames' import { Button, PageDivider } from '~/libs/ui' +import { InputSkillSelector } from '~/libs/shared/lib/components/input-skill-selector' import { ProgressBar } from '../../components/progress-bar' -import { autoCompleteSkills } from '../../services/skills' -import { updateMemberSkills } from '../../redux/actions/member' -import SkillInfo from '../../models/SkillInfo' -import SkillTag from '../../components/skill-tag' import styles from './styles.module.scss' -import MemberInfo from '../../models/MemberInfo' -const PageSkillsContent: FC<{ - memberInfo?: MemberInfo, - updateMemberSkills: (skills: SkillInfo[]) => void -}> = props => { +export const PageSkills: FC<{}> = () => { const navigate: any = useNavigate() - const [skillsFilter, setSkillsFilter] = useState | null>(null) - const [loading, setLoading] = useState(false) - useEffect(() => { - if (!skillsFilter && props.memberInfo) { - setSkillsFilter(props.memberInfo?.emsiSkills || []) - } - /* eslint-disable react-hooks/exhaustive-deps */ - }, [props.memberInfo]) - - useEffect(() => { - const saveData: any = async () => { - setLoading(true) - await props.updateMemberSkills([...(skillsFilter || [])]) - setLoading(false) - } - - if (!!skillsFilter && !_.isEqual(props.memberInfo?.emsiSkills, skillsFilter)) { - saveData() - .then(_.noop) - } - /* eslint-disable react-hooks/exhaustive-deps */ - }, [skillsFilter]) return (
@@ -58,69 +26,8 @@ const PageSkillsContent: FC<{ Add industry standard skills to your profile to let employers search and find you for opportunities that fit your capabilities. -
- {(skillsFilter && skillsFilter.length > 0) ? ( -
- {(skillsFilter || []).map(skillItem => ( - { - setSkillsFilter( - _.filter(skillsFilter, skill => skill.name !== skillItem.name), - ) - }} - /> - ))} -
- ) : null} - skill.name} - getOptionValue={(skill: SkillInfo) => skill.emsiId} - onChange={(options: readonly SkillInfo[]) => { - if (options.length > 0) { - const newSkillFilter: SkillInfo[] = _.uniqBy( - [...(skillsFilter || []), ...options], - 'name', - ) - _.forEach(options, option => { - const matchSkill: SkillInfo | undefined = _.find( - newSkillFilter, - { name: option.name }, - ) - if (matchSkill && !matchSkill?.skillSources) { - matchSkill.skillSources = ['SelfPicked'] - } else if ( - matchSkill - && matchSkill.skillSources - && matchSkill.skillSources.indexOf('SelfPicked') < 0 - ) { - matchSkill.skillSources.push('SelfPicked') - } - }) - setSkillsFilter(newSkillFilter) - } - }} - value={[]} - isDisabled={!props.memberInfo || loading} - /> +
+
@@ -136,7 +43,6 @@ const PageSkillsContent: FC<{ size='lg' primary iconToLeft - disabled={loading} onClick={() => navigate('../start')} > back @@ -145,7 +51,6 @@ const PageSkillsContent: FC<{ size='lg' primary iconToLeft - disabled={loading} onClick={() => navigate('../works')} > next @@ -155,20 +60,4 @@ const PageSkillsContent: FC<{ ) } -const mapStateToProps: any = (state: any) => { - const { - memberInfo, - }: any = state.member - - return { - memberInfo, - } -} - -const mapDispatchToProps: any = { - updateMemberSkills, -} - -export const PageSkills: any = connect(mapStateToProps, mapDispatchToProps)(PageSkillsContent) - export default PageSkills diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index c7a731566..b6ac89988 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -13,7 +13,6 @@ import { } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' import { ACTIONS } from '../../config' -import SkillInfo from '../../models/SkillInfo' import { getMemberInfo, getMemberTraits, putMemberInfo } from '../../services/members' import WorkInfo from '../../models/WorkInfo' import EducationInfo from '../../models/EducationInfo' @@ -397,33 +396,11 @@ export const createMemberConnectInfos: any = (connectInfos: ConnectInfo[]) => as } } -export const setMemberSkills: any = (skills: SkillInfo[]) => ({ - type: ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS, - payload: skills, -}) - export const setMemberPhotoUrl: any = (photoUrl: string) => ({ type: ACTIONS.MEMBER.UPDATE_MEMBER_PHOTO_URL, payload: photoUrl, }) -export const updateMemberSkills: any = (skills: SkillInfo[]) => async (dispatch: any) => { - try { - const tokenInfo: TokenModel = await getAsyncToken() - await putMemberInfo(tokenInfo.handle || '', { - emsiSkills: skills.map(skill => ({ - skillSources: skill.skillSources, - subCategory: skill.subCategory, - emsiId: skill.emsiId, - name: skill.name, - category: skill.category, - })), - }) - dispatch(setMemberSkills(skills)) - } catch (error) { - } -} - export const updateMemberHomeAddresss: any = (addresses: MemberAddress[]) => async (dispatch: any) => { try { const tokenInfo: TokenModel = await getAsyncToken() diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index b710353e2..2cb28dc3f 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -63,19 +63,6 @@ const memberReducer: any = (state = initialState, action: { type: any; payload: ...state, educations: action.payload, } - case ACTIONS.MEMBER.UPDATE_MEMBER_SKILLS: { - if (!state.memberInfo) { - return state - } - - return { - ...state, - memberInfo: { - ...state.memberInfo, - emsiSkills: action.payload, - }, - } - } case ACTIONS.MEMBER.UPDATE_MEMBER_PHOTO_URL: { if (!state.memberInfo) { diff --git a/src/apps/onboarding/src/services/skills.ts b/src/apps/onboarding/src/services/skills.ts deleted file mode 100644 index bd92089c9..000000000 --- a/src/apps/onboarding/src/services/skills.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -import { EnvironmentConfig } from '~/config' -import { xhrGetAsync } from '~/libs/core' -import SkillInfo from '../models/SkillInfo' - -export async function autoCompleteSkills(search: string): Promise> { - return xhrGetAsync(`${EnvironmentConfig.API.V5}/emsi-skills/skills/auto-complete?term=${search}`) -} diff --git a/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.module.scss b/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.module.scss new file mode 100644 index 000000000..d03cd08ea --- /dev/null +++ b/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.module.scss @@ -0,0 +1,8 @@ +@import "@libs/ui/styles/includes"; + +.editMemberPropertyBtn { + svg { + width: 16px; + height: 16px; + } +} diff --git a/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.tsx b/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.tsx index 29cb39c35..944f39bb1 100644 --- a/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.tsx +++ b/src/apps/profiles/src/components/EditMemberPropertyBtn/EditMemberPropertyBtn.tsx @@ -2,6 +2,8 @@ import { FC } from 'react' import { Button, IconOutline } from '~/libs/ui' +import styles from './EditMemberPropertyBtn.module.scss' + interface EditMemberPropertyBtnProps { onClick: () => void } @@ -10,6 +12,7 @@ const EditMemberPropertyBtn: FC = (props: EditMember
+ )} + > +
+
+ { + memberEducation?.map((education: UserTrait, indx: number) => ( +
+ +
+
+
+ )) + } +
+ +

+ Enter information about your schooling and degrees. +

+ +
+ + +
+ + +
+ +
+ {editedItemIndex === undefined ? : undefined} +
+ +
+ + ) +} + +export default ModifyEducationModal diff --git a/src/apps/profiles/src/member-profile/education-and-certifications/ModifyEducationModal/index.ts b/src/apps/profiles/src/member-profile/education-and-certifications/ModifyEducationModal/index.ts new file mode 100644 index 000000000..0fcda38b8 --- /dev/null +++ b/src/apps/profiles/src/member-profile/education-and-certifications/ModifyEducationModal/index.ts @@ -0,0 +1 @@ +export { default as ModifyEducationModal } from './ModifyEducationModal' diff --git a/src/apps/profiles/src/member-profile/education-and-certifications/index.ts b/src/apps/profiles/src/member-profile/education-and-certifications/index.ts new file mode 100644 index 000000000..618ab2ac2 --- /dev/null +++ b/src/apps/profiles/src/member-profile/education-and-certifications/index.ts @@ -0,0 +1 @@ +export { default as EducationAndCertifications } from './EducationAndCertifications' diff --git a/src/apps/profiles/src/member-profile/languages/MemberLanguages.tsx b/src/apps/profiles/src/member-profile/languages/MemberLanguages.tsx index 518d74c41..0204e6820 100644 --- a/src/apps/profiles/src/member-profile/languages/MemberLanguages.tsx +++ b/src/apps/profiles/src/member-profile/languages/MemberLanguages.tsx @@ -59,7 +59,7 @@ const MemberLanguages: FC = (props: MemberLanguagesProps) return (
-

My Langagues:

+

My Languages:

{ canEdit && ( = (props: ModifyLanguagesModalProps) => { const formElRef: MutableRefObject = useRef() @@ -58,14 +70,12 @@ const ModifyLanguagesModal: FC = (props: ModifyLangua 'label', ), []) - const spokenLevel: any = useMemo(() => sortBy( - dropDowns.spokenLevel.map(lang => ({ label: lang.label, value: lang.label })), - 'label', + const spokenLevel: any = useMemo(() => dropDowns.spokenLevel.map( + lang => ({ label: lang.label, value: lang.label }), ), []) - const writtenLevel: any = useMemo(() => sortBy( - dropDowns.writtenLevel.map(lang => ({ label: lang.label, value: lang.label })), - 'label', + const writtenLevel: any = useMemo(() => dropDowns.writtenLevel.map( + lang => ({ label: lang.label, value: lang.label }), ), []) const [currentMemberLanguages, setCurrentMemberLanguages]: [ @@ -89,7 +99,7 @@ const ModifyLanguagesModal: FC = (props: ModifyLangua function handleLanguagesSave(): void { setIsSaving(true) - updateMemberTraitsAsync(props.profile.handle, [{ + methodsMap[!!props.memberLanguages ? 'update' : 'create'](props.profile.handle, [{ categoryName: UserTraitCategoryNames.languages, traitId: UserTraitIds.languages, traits: { diff --git a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx index bdf3b14da..c4788affa 100644 --- a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx +++ b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx @@ -3,7 +3,14 @@ import { KeyedMutator } from 'swr' import { useSearchParams } from 'react-router-dom' import { useMemberTraits, UserProfile, UserTrait, UserTraitIds, UserTraits } from '~/libs/core' -import { IconOutline } from '~/libs/ui' +import { + IconOutline, + SocialIconFacebook, + SocialIconInstagram, + SocialIconLinkedIn, + SocialIconTwitter, + SocialIconYoutube, +} from '~/libs/ui' import { EditMemberPropertyBtn } from '../../components' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' @@ -19,7 +26,15 @@ interface MemberLinksProps { export function renderLinkIcon(linkName: string): JSX.Element { switch (linkName) { case 'Facebook': - return + return + case 'Twitter': + return + case 'LinkedIn': + return + case 'Instagram': + return + case 'YouTube': + return default: return } diff --git a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss index 4e51da63e..74053ae51 100644 --- a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss +++ b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss @@ -3,13 +3,37 @@ .container { display: flex; flex-direction: column; + background-color: $black-5; - .contentLayoutOuter { - margin-top: $sp-12; + .profileHeaderWrap { + background: url('../../lib/assets/profile-header-bg.png') no-repeat right center / auto, linear-gradient(#0d83c5, #0e89d5); + + .profileHeaderContent { + padding: 0; + max-height: 200px; + overflow: visible; + + @include ltelg { + max-height: 100%; + } + } + + .profileHeaderContentOuter { + margin-top: 75px; + } + + .profileHeaderBottom { + height: 90px; + background-color: $tc-white + } + } + + .profileOuter { + margin-top: 130px; .profileInfoWrap { display: grid; - grid-template-columns: 1fr 360px; + grid-template-columns: 300px 1fr; grid-gap: $sp-15; margin-bottom: $sp-13; @@ -32,8 +56,12 @@ .profileInfoRight { display: flex; flex-direction: column; - margin-top: $sp-15; - + margin-top: -215px; + + @include ltelg { + margin-top: 0; + } + .shortBio { margin-bottom: $sp-8; @@ -49,5 +77,23 @@ } } } + + .expirenceWrap { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + + @include ltelg { + grid-template-columns: 1fr; + gap: 0; + } + } } -} \ No newline at end of file +} + +.sectionWrap { + background-color: $tc-white; + padding: $sp-8 $sp-8 0; + margin-bottom: $sp-8; + border-radius: 16px; +} diff --git a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx index ac8946565..291b72735 100644 --- a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx +++ b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx @@ -5,16 +5,16 @@ import { ContentLayout, PageTitle } from '~/libs/ui' import { useCheckIsMobile } from '~/libs/shared' // import { MemberTCActivityInfo } from '../tc-activity' -import { MemberTracksInfo } from '../tracks' import { MemberSkillsInfo } from '../skills' import { CommunityAwards } from '../community-awards' -import { MemberTCAInfo } from '../tca-info' import { ProfileHeader } from '../profile-header' import { MemberLocalInfo } from '../local-info' import { MemberLanguages } from '../languages' import { AboutMe } from '../about-me' import { MemberLinks } from '../links' import { MemberTCAchievements } from '../tc-achievements' +import { WorkExpirence } from '../work-expirence' +import { EducationAndCertifications } from '../education-and-certifications' import styles from './ProfilePageLayout.module.scss' @@ -30,21 +30,29 @@ const ProfilePageLayout: FC = (props: ProfilePageLayoutP return (
+ {`${props.profile.handle} | Community Profile | Topcoder`} + +
+ + + +
+
+ - {`${props.profile.handle} | Community Profile | Topcoder`} -
- - { - isMobile && ( + !isMobile && ( = (props: ProfilePageLayoutP ) } -
- -
- - - - + - + - +
{ - !isMobile && ( + isMobile && ( = (props: ProfilePageLayoutP ) } - +
+
+ +
+
- +
+ - + +
+ +
+
+
+ +
+
+
+ +
+
diff --git a/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx b/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx index 2692d1b9d..38f4543a6 100644 --- a/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx @@ -19,9 +19,6 @@ const OpenForGigsModifyModal: FC = (props: OpenForG const [isSaving, setIsSaving]: [boolean, Dispatch>] = useState(false) - const [isFormChanged, setIsFormChanged]: [boolean, Dispatch>] - = useState(false) - const [openForWork, setOpenForWork]: [boolean, Dispatch>] = useState(props.openForWork) @@ -60,7 +57,6 @@ const OpenForGigsModifyModal: FC = (props: OpenForG } function handleOpenForWorkToggle(): void { - setIsFormChanged(true) setOpenForWork(!openForWork) } @@ -80,7 +76,7 @@ const OpenForGigsModifyModal: FC = (props: OpenForG label='Save' onClick={handleOpenForWorkSave} primary - disabled={isSaving || !isFormChanged} + disabled={isSaving} />
)} diff --git a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.module.scss b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.module.scss index eb1dc2486..1081e19ba 100644 --- a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.module.scss +++ b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.module.scss @@ -3,18 +3,31 @@ .container { display: flex; flex-wrap: wrap; - align-items: center; - margin-bottom: $sp-8; + align-items: flex-start; + color: $tc-white; .photoWrap { position: relative; margin-right: $sp-13; + .verifiedBadge { + position: absolute; + right: 0; + top: 0; + color: $turq-120; + + svg { + width: 70px; + height: 70px; + } + } + .profilePhoto { - width: 200px; - height: 200px; - border: 3px solid #fff; + width: 300px; + height: 300px; + border: 12px solid $tc-white; border-radius: 50%; + background-color: $tc-white; } button { @@ -29,22 +42,42 @@ display: flex; flex-direction: column; flex: 1; - - .verified { - color: $tc-black; - } + margin-top: $sp-8; .nameWrap { display: flex; align-items: center; margin-bottom: $sp-2; + + p { + font-size: 40px; + font-weight: $font-weight-bold; + font-family: $font-barlow; + } + } + + .memberSince { + margin-top: $sp-2; + font-size: 20px; + + span { + font-weight: $font-weight-bold; + } } } .profileActions { - margin-bottom: $sp-13; display: flex; align-items: center; + background-color: $tc-white; + padding: $sp-4; + border-radius: 200px; + color: $black-100; + margin-top: $sp-8; + + > span { + margin-right: $sp-2; + } >button { margin-left: $sp-2; diff --git a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx index 6664f456f..e91188fb7 100644 --- a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom' import moment from 'moment' import { getVerificationStatusAsync, UserProfile } from '~/libs/core' -import { Button, VerifiedMemberBadge } from '~/libs/ui' +import { Button, IconSolid } from '~/libs/ui' import { EditMemberPropertyBtn } from '../../components' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' @@ -97,6 +97,13 @@ const ProfileHeader: FC = (props: ProfileHeaderProps) => {
Topcoder - Member Profile Avatar + { + isMemberVerified ? ( +
+ +
+ ) : undefined + } { canEdit && ( = (props: ProfileHeaderProps) => {
-

+

{props.profile.firstName} {' '} {props.profile.lastName} @@ -123,7 +130,7 @@ const ProfileHeader: FC = (props: ProfileHeaderProps) => {

- {props.profile.handle} + {props.profile.handle} {' '} | {' '} @@ -132,15 +139,14 @@ const ProfileHeader: FC = (props: ProfileHeaderProps) => { {moment(props.profile.createdAt) .format('MMM YYYY')}

- - { - isMemberVerified ? ( - - ) : undefined - }
+ + {props.profile.firstName} + {' '} + is + { !canEdit && ( diff --git a/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.module.scss b/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.module.scss index f6b10cd97..e7f54797c 100644 --- a/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.module.scss +++ b/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.module.scss @@ -9,6 +9,12 @@ padding-bottom: $sp-6; } + .headerWrap { + display: flex; + align-items: center; + margin-bottom: $sp-4; + } + .titleWrap { display: flex; justify-content: space-between; @@ -24,20 +30,24 @@ margin-top: $sp-2; .skillItem { - background-color: $teal-140; - font-weight: $font-weight-medium; + border: 1px solid $black-20; font-size: 14px; line-height: 16px; - letter-spacing: .5px; - color: $tc-white; white-space: nowrap; - padding: $sp-1 $sp-2; - border-radius: 4px; + padding: $sp-2 $sp-3; + border-radius: 24px; display: flex; align-items: center; - svg { - margin-right: $sp-1; + &.verifiedSkillItem { + border: 2px solid $turq-120; + + svg { + margin-left: $sp-1; + width: 16px; + height: 16px; + color: $turq-120; + } } } } @@ -45,5 +55,10 @@ .legendWrap { display: flex; margin-top: $sp-2; + color: $link-blue-dark; + + &:hover { + text-decoration: underline; + } } } \ No newline at end of file diff --git a/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx b/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx index 99dfa98ab..8d14da6ad 100644 --- a/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx +++ b/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx @@ -1,41 +1,85 @@ -import { FC } from 'react' +import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react' +import classNames from 'classnames' -import { useMemberSkills, UserProfile, UserSkill } from '~/libs/core' -import { TCVerifiedSkillIcon, TCVerifiedSkillWhiteIcon } from '~/libs/ui' +import { isVerifiedSkill, UserEMSISkill, UserProfile } from '~/libs/core' +import { IconOutline } from '~/libs/ui' -import { TC_VERIFIED_SKILL_LABEL } from '../../config' +import { EditMemberPropertyBtn } from '../../components' +import { ModifySkillsModal } from './ModifySkillsModal' import styles from './MemberSkillsInfo.module.scss' interface MemberSkillsInfoProps { - profile: UserProfile | undefined + profile: UserProfile + authProfile: UserProfile | undefined } const MemberSkillsInfo: FC = (props: MemberSkillsInfoProps) => { + const canEdit: boolean = props.authProfile?.handle === props.profile.handle - const memberSkills: UserSkill[] | undefined = useMemberSkills(props.profile?.handle) + const memberEMSISkills: UserEMSISkill[] = useMemo( + () => (props.profile.emsiSkills || []) + .sort((a, b) => (isVerifiedSkill(a.skillSources) ? -1 : (isVerifiedSkill(b.skillSources) ? 1 : 0))), + [props.profile.emsiSkills], + ) - return memberSkills ? ( + const [isEditMode, setIsEditMode]: [boolean, Dispatch>] + = useState(false) + + function handleEditSkillsClick(): void { + setIsEditMode(true) + } + + function handleModyfSkillsModalClose(): void { + setIsEditMode(false) + } + + return memberEMSISkills ? (
-

My Skills

-
- - {' = '} - {TC_VERIFIED_SKILL_LABEL} +
+

Skills

+ { + canEdit && ( + + ) + }
+ + How skills work? +
{ - memberSkills.map((memberSkill: UserSkill) => ( -
- {memberSkill.sources?.includes('CHALLENGE') && } - {memberSkill.tagName} + memberEMSISkills.map((memberEMSISkill: UserEMSISkill) => ( +
+ {memberEMSISkill.name} + {isVerifiedSkill(memberEMSISkill.skillSources) && }
)) }
+ + { + isEditMode && ( + + ) + }
) : <> } diff --git a/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.module.scss b/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.module.scss new file mode 100644 index 000000000..aa0ef5871 --- /dev/null +++ b/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.module.scss @@ -0,0 +1,12 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; +} + +.modalButtons { + display: flex; + justify-content: space-between; + width: 100%; +} diff --git a/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.tsx b/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.tsx new file mode 100644 index 000000000..efe6e45bf --- /dev/null +++ b/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/ModifySkillsModal.tsx @@ -0,0 +1,46 @@ +import { Dispatch, FC, SetStateAction, useState } from 'react' + +// import { UserProfile } from '~/libs/core' +import { BaseModal, Button } from '~/libs/ui' + +import styles from './ModifySkillsModal.module.scss' + +interface ModifySkillsModalProps { + // profile: UserProfile + onClose: () => void +} + +const ModifySkillsModal: FC = (props: ModifySkillsModalProps) => { + const [isSaving, setIsSaving]: [boolean, Dispatch>] + = useState(false) + + function handleModifySkillsSave(): void { + setIsSaving(true) + } + + return ( + +
+ )} + /> + ) +} + +export default ModifySkillsModal diff --git a/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/index.ts b/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/index.ts new file mode 100644 index 000000000..444848cda --- /dev/null +++ b/src/apps/profiles/src/member-profile/skills/ModifySkillsModal/index.ts @@ -0,0 +1 @@ +export { default as ModifySkillsModal } from './ModifySkillsModal' diff --git a/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.module.scss b/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.module.scss new file mode 100644 index 000000000..f2f4b666b --- /dev/null +++ b/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.module.scss @@ -0,0 +1,40 @@ +@import "@libs/ui/styles/includes"; + + +.containerWrap { + background-color: $black-5; + border-radius: 15px; + padding: $sp-6; + flex: 1; + + > p { + font-size: 20px; + } + + .container { + display: flex; + flex-direction: column; + align-items: center; + border-radius: 15px; + margin-bottom: $sp-8; + background-image: linear-gradient(90deg, #7B21A7, #1974AD); + color: $tc-white; + padding: $sp-8; + + .wins { + font-size: 44px; + font-weight: 500; + line-height: 34px; + margin-top: $sp-8; + } + + .champText { + font-family: $font-barlow-condensed; + font-size: 54px; + font-weight: 500; + text-transform: uppercase; + margin-bottom: $sp-8; + line-height: 54px; + } + } +} \ No newline at end of file diff --git a/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.tsx b/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.tsx new file mode 100644 index 000000000..c73299211 --- /dev/null +++ b/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/ChallengeWinsBanner.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react' + +import styles from './ChallengeWinsBanner.module.scss' + +interface ChallengeWinsBannerProps { + challengeWins: number +} + +const ChallengeWinsBanner: FC = (props: ChallengeWinsBannerProps) => ( +
+
+

Topcoder Challenge Winner

+

+ {props.challengeWins} + {' '} +

+

WINS

+
+

+ Topcoder challenges are open competitions where community + members participate in small units of work to deliver projects. +

+
+) + +export default ChallengeWinsBanner diff --git a/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/index.ts b/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/index.ts new file mode 100644 index 000000000..fcb50d371 --- /dev/null +++ b/src/apps/profiles/src/member-profile/tc-achievements/ChallengeWinsBanner/index.ts @@ -0,0 +1 @@ +export { default as ChallengeWinsBanner } from './ChallengeWinsBanner' diff --git a/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.module.scss b/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.module.scss index 2dfe3d414..3d699b814 100644 --- a/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.module.scss +++ b/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.module.scss @@ -7,16 +7,10 @@ .achievementsWrap { display: flex; - align-items: center; margin: $sp-4 0 $sp-2; - .achievement { - padding: $sp-8 $sp-4; - text-align: center; - border: 1px solid $black-10; - border-radius: 8px; - display: flex; - flex-direction: column; + > div:not(:last-child) { + margin-right: $sp-8; } } diff --git a/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.tsx b/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.tsx index 4bea8d6d8..c80220036 100644 --- a/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.tsx +++ b/src/apps/profiles/src/member-profile/tc-achievements/MemberTCAchievements.tsx @@ -1,7 +1,6 @@ import { FC, useMemo } from 'react' import { - ratingToCSScolor, useMemberBadges, useMemberStats, UserBadge, @@ -10,6 +9,8 @@ import { UserStats, } from '~/libs/core' +import { TCOWinsBanner } from './TCOWinsBanner' +import { ChallengeWinsBanner } from './ChallengeWinsBanner' import styles from './MemberTCAchievements.module.scss' interface MemberTCAchievementsProps { @@ -33,25 +34,23 @@ const MemberTCAchievements: FC = (props: MemberTCAchi // console.log('handleOpenTCAchievements') // } - return ( + return memberStats?.wins || tcoWins ? (
-

Achievements with Topcoder

+

Achievements @ Topcoder

-
-

{tcoWins}

-

TCO Wins

-
-
-

{memberStats?.wins || 0}

-

Challenge Wins

-
-
- - {props.profile.maxRating?.rating || 0} - -

RATING

-
+ { + tcoWins > 0 && ( + + ) + } + { + !!memberStats?.wins && memberStats.wins > 0 && ( + + ) + }
{ @@ -63,7 +62,7 @@ const MemberTCAchievements: FC = (props: MemberTCAchi ) }
- ) + ) : <> } export default MemberTCAchievements diff --git a/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.module.scss b/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.module.scss new file mode 100644 index 000000000..24e31d8d7 --- /dev/null +++ b/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.module.scss @@ -0,0 +1,39 @@ +@import "@libs/ui/styles/includes"; + +.container { + display: flex; + flex-direction: column; + align-items: center; + border-radius: 15px; + background: url('./assets/tco-logo-banner.png') no-repeat left top / auto, linear-gradient(90deg, #05456D, #0A7AC0); + color: $tc-white; + padding: $sp-8; + flex: 1; + + .wins { + font-size: 64px; + font-weight: 700; + line-height: 44px; + margin-top: $sp-8; + + span { + font-family: $font-barlow-condensed; + font-size: 35px; + font-weight: 500; + text-transform: uppercase; + } + } + + .champText { + font-family: $font-barlow-condensed; + font-size: 54px; + font-weight: 500; + text-transform: uppercase; + margin-bottom: $sp-8; + line-height: 54px; + } + + .text { + font-size: 20px; + } +} \ No newline at end of file diff --git a/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.tsx b/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.tsx new file mode 100644 index 000000000..4799cbcad --- /dev/null +++ b/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/TCOWinsBanner.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react' + +import styles from './TCOWinsBanner.module.scss' + +interface TCOWinsBannerProps { + tcoWins: number +} + +const TCOWinsBanner: FC = (props: TCOWinsBannerProps) => ( +
+

Topcoder Open (TCO)

+

+ {props.tcoWins} + {' '} + {props.tcoWins === 1 ? 'time' : 'times'} +

+

Champion

+

+ Topcoder Open (TCO) is the ultimate programming tournament, + that earns our winners major prestige in the programming community. +

+
+) + +export default TCOWinsBanner diff --git a/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/assets/tco-logo-banner.png b/src/apps/profiles/src/member-profile/tc-achievements/TCOWinsBanner/assets/tco-logo-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..a041acd3b3c27a1270cfe6e8df8a5dda36ab5544 GIT binary patch literal 12335 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 void + onSave: () => void + profile: UserProfile + workExpirence: UserTrait[] | undefined +} + +const methodsMap: { [key: string]: any } = { + create: createMemberTraitsAsync, + update: updateMemberTraitsAsync, +} + +const ModifyWorkExpirenceModal: FC = (props: ModifyWorkExpirenceModalProps) => { + const [isSaving, setIsSaving]: [boolean, Dispatch>] + = useState(false) + + const [isFormChanged, setIsFormChanged]: [boolean, Dispatch>] + = useState(false) + + const [formValues, setFormValues]: [ + { [key: string]: string | boolean | Date | undefined }, + Dispatch> + ] + = useState<{ [key: string]: string | boolean | Date | undefined }>({}) + + const [formErrors, setFormErrors]: [ + { [key: string]: string }, + Dispatch> + ] + = useState<{ [key: string]: string }>({}) + + const formElRef: MutableRefObject = useRef() + + const [editedItemIndex, setEditedItemIndex]: [ + number | undefined, + Dispatch> + ] = useState() + + const [workExpirence, setWorkExpirence]: [ + UserTrait[] | undefined, + Dispatch> + ] + = useState(props.workExpirence) + + function handleModifyWorkExpirenceSave(): void { + setIsSaving(true) + + methodsMap[!!props.workExpirence ? 'update' : 'create'](props.profile.handle, [{ + categoryName: UserTraitCategoryNames.work, + traitId: UserTraitIds.work, + traits: { + data: workExpirence || [], + }, + }]) + .then(() => { + toast.success('Work Experience updated successfully.') + props.onSave() + }) + .catch(() => { + toast.error('Failed to update user\'s Work Experience.') + setIsSaving(false) + }) + } + + function handleFormValueChange(key: string, event: React.ChangeEvent): void { + let value: string | boolean | Date | undefined + + switch (key) { + case 'currentlyWorking': + value = event.target.checked + break + case 'startDate': + case 'endDate': + value = event as unknown as Date + break + default: + value = event.target.value + break + } + + setFormValues({ + ...formValues, + [key]: value, + }) + } + + function handleCancelEditMode(): void { + setEditedItemIndex(undefined) + resetForm() + } + + function resetForm(): void { + setFormValues({}) + formElRef.current.reset() + setEditedItemIndex(undefined) + } + + function handleFormAction(): void { + setFormErrors({}) + + if (!trim(formValues.company as string)) { + setFormErrors({ + company: 'Company is required', + }) + return + } + + if (!trim(formValues.position as string)) { + setFormErrors({ + position: 'Position is required', + }) + return + } + + if (!trim(formValues.industry as string)) { + setFormErrors({ + industry: 'Industry is required', + }) + return + } + + if (!trim(formValues.city as string)) { + setFormErrors({ + city: 'City is required', + }) + return + } + + if (!formValues.startDate) { + setFormErrors({ + startDate: 'Start date is required', + }) + return + } + + const updatedWorkExpirence: UserTrait = { + cityTown: formValues.city, + company: formValues.company, + industry: formValues.industry, + position: formValues.position, + timePeriodFrom: formValues.startDate ? (formValues.startDate as Date).toISOString() : undefined, + timePeriodTo: formValues.endDate ? (formValues.endDate as Date).toISOString() : undefined, + working: formValues.currentlyWorking, + } + + if (editedItemIndex !== undefined && workExpirence) { + workExpirence[editedItemIndex] = updatedWorkExpirence + + setWorkExpirence([ + ...workExpirence, + ]) + } else { + setWorkExpirence( + [...workExpirence || [], updatedWorkExpirence], + ) + } + + setIsFormChanged(true) + resetForm() + } + + function handleWorkExpirenceEdit(indx: number): void { + const work: UserTrait = workExpirence ? workExpirence[indx] : {} + + setEditedItemIndex(indx) + + setFormValues({ + city: work.cityTown, + company: work.company, + currentlyWorking: work.working, + endDate: work.timePeriodTo ? new Date(work.timePeriodTo) : undefined, + industry: work.industry, + position: work.position, + startDate: work.timePeriodFrom ? new Date(work.timePeriodFrom) : undefined, + }) + } + + function handleWorkExpirenceDelete(indx: number): void { + const updatedWorkExpirence: UserTrait[] = [...workExpirence || []] + + updatedWorkExpirence.splice(indx, 1) + setWorkExpirence(updatedWorkExpirence) + setIsFormChanged(true) + } + + return ( + +
+ )} + > +
+
+ { + workExpirence?.map((work: UserTrait, indx: number) => ( +
+ +
+
+
+ )) + } +
+ +

+ Enter your work experience to show customers + the roles and responsibilites you have held in the past. +

+ +
+ + +
+ + +
+
+ + +
+ +
+ {editedItemIndex === undefined ? : undefined} +
+ +
+ + ) +} + +export default ModifyWorkExpirenceModal diff --git a/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/index.ts b/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/index.ts new file mode 100644 index 000000000..b624b2ebc --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/index.ts @@ -0,0 +1 @@ +export { default as ModifyWorkExpirenceModal } from './ModifyWorkExpirenceModal' diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.module.scss b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.module.scss new file mode 100644 index 000000000..7fd60b9f0 --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.module.scss @@ -0,0 +1,17 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; + + .headerWrap { + display: flex; + align-items: center; + margin-bottom: $sp-4; + } + + .contentWrap { + display: flex; + flex-direction: column; + } +} diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx new file mode 100644 index 000000000..f964b7b97 --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx @@ -0,0 +1,94 @@ +import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { KeyedMutator } from 'swr' + +import { useMemberTraits, UserProfile, UserTrait, UserTraitIds, UserTraits } from '~/libs/core' + +import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' +import { EditMemberPropertyBtn } from '../../components' + +import { ModifyWorkExpirenceModal } from './ModifyWorkExpirenceModal' +import { WorkExpirenceCard } from './WorkExpirenceCard' +import styles from './WorkExpirence.module.scss' + +interface WorkExpirenceProps { + profile: UserProfile + authProfile: UserProfile | undefined +} + +const WorkExpirence: FC = (props: WorkExpirenceProps) => { + const [queryParams]: [URLSearchParams, any] = useSearchParams() + const editMode: string | null = queryParams.get(EDIT_MODE_QUERY_PARAM) + + const canEdit: boolean = props.authProfile?.handle === props.profile.handle + + const [isEditMode, setIsEditMode]: [boolean, Dispatch>] + = useState(false) + + const { data: memberWorkExpirenceTraits, mutate: mutateTraits }: { + data: UserTraits[] | undefined, + mutate: KeyedMutator, + } + = useMemberTraits(props.profile.handle, { traitIds: UserTraitIds.work }) + + const workExpirence: UserTrait[] | undefined + = useMemo(() => memberWorkExpirenceTraits?.[0]?.traits?.data, [memberWorkExpirenceTraits]) + + useEffect(() => { + if (props.authProfile && editMode === profileEditModes.workExperience) { + setIsEditMode(true) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.authProfile]) + + function handleEditWorkExpirenceClick(): void { + setIsEditMode(true) + } + + function handleModyfyWorkExpirenceModalClose(): void { + setIsEditMode(false) + } + + function handleModyfyWorkExpirenceSave(): void { + setTimeout(() => { + setIsEditMode(false) + mutateTraits() + }, 1000) + } + + return ( +
+
+

Work Experience

+ { + canEdit && ( + + ) + } +
+ +
+ { + workExpirence?.map((work: UserTrait) => ( + + )) + } +
+ + { + isEditMode && ( + + ) + } +
+ ) +} + +export default WorkExpirence diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.module.scss b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.module.scss new file mode 100644 index 000000000..c94f99e44 --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.module.scss @@ -0,0 +1,12 @@ +@import '@libs/ui/styles/includes'; + +.workExpirenceCard { + margin-bottom: $sp-4; + flex: 1; + + .workExpirenceCardHeader { + display: flex; + justify-content: space-between; + align-items: flex-end; + } +} diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.tsx b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.tsx new file mode 100644 index 000000000..b4d63f763 --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/WorkExpirenceCard.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react' +import moment from 'moment' + +import { UserTrait } from '~/libs/core' + +import styles from './WorkExpirenceCard.module.scss' + +interface WorkExpirenceCardProps { + work: UserTrait +} + +const WorkExpirenceCard: FC = (props: WorkExpirenceCardProps) => ( +
+
+
+

+ {props.work.position} + , + {' '} + {props.work.industry} +

+

+ {props.work.company} + , + {' '} + {props.work.cityTown} +

+
+ { + props.work.timePeriodFrom || props.work.timePeriodTo ? ( +
+

+ {props.work.timePeriodFrom ? moment(props.work.timePeriodFrom) + .format('MM/YYYY') : ''} + {' '} + - + {' '} + {props.work.timePeriodTo ? moment(props.work.timePeriodTo) + .format('MM/YYYY') : (props.work.currentlyWorking ? 'Present' : '')} +

+
+ ) : undefined + } +
+
+) + +export default WorkExpirenceCard diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/index.ts b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/index.ts new file mode 100644 index 000000000..6a5524e25 --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirenceCard/index.ts @@ -0,0 +1 @@ +export { default as WorkExpirenceCard } from './WorkExpirenceCard' diff --git a/src/apps/profiles/src/member-profile/work-expirence/index.ts b/src/apps/profiles/src/member-profile/work-expirence/index.ts new file mode 100644 index 000000000..5438fcfac --- /dev/null +++ b/src/apps/profiles/src/member-profile/work-expirence/index.ts @@ -0,0 +1 @@ +export { default as WorkExpirence } from './WorkExpirence' diff --git a/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx b/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx new file mode 100644 index 000000000..7d520eaec --- /dev/null +++ b/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx @@ -0,0 +1,24 @@ +import { FC, useContext, useEffect } from 'react' +import { NavigateFunction, useNavigate } from 'react-router-dom' + +import { profileContext, ProfileContextData } from '~/libs/core' + +const ProfilesLandingPage: FC = () => { + const navigate: NavigateFunction = useNavigate() + + const { profile: authProfile }: ProfileContextData = useContext(profileContext) + + // redirect to profile page if logged in + useEffect(() => { + if (authProfile) { + navigate(`/${authProfile.handle}`) + } + }, [authProfile, navigate]) + + return ( + // TODO: no profile specified - redirect to talent search or dedicated page + <> + ) +} + +export default ProfilesLandingPage diff --git a/src/apps/profiles/src/profiles-landing-page/index.ts b/src/apps/profiles/src/profiles-landing-page/index.ts new file mode 100644 index 000000000..6169cd143 --- /dev/null +++ b/src/apps/profiles/src/profiles-landing-page/index.ts @@ -0,0 +1 @@ +export { default as ProfilesLandingPage } from './ProfilesLandingPage' diff --git a/src/apps/profiles/src/profiles.routes.tsx b/src/apps/profiles/src/profiles.routes.tsx index bdbcf4e88..eee6288b0 100644 --- a/src/apps/profiles/src/profiles.routes.tsx +++ b/src/apps/profiles/src/profiles.routes.tsx @@ -4,6 +4,8 @@ import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config' const ProfilesApp: LazyLoadedComponent = lazyLoad(() => import('./ProfilesApp')) const MemberProfilePage: LazyLoadedComponent = lazyLoad(() => import('./member-profile'), 'MemberProfilePage') const MemberBadgesPage: LazyLoadedComponent = lazyLoad(() => import('./member-badges'), 'MemberBadgesPage') +const ProfilesLandingPage: LazyLoadedComponent + = lazyLoad(() => import('./profiles-landing-page'), 'ProfilesLandingPage') export const rootRoute: string = ( EnvironmentConfig.SUBDOMAIN === AppSubdomain.profiles ? '' : `/${AppSubdomain.profiles}` @@ -15,6 +17,11 @@ export const absoluteRootRoute: string = `${window.location.origin}${rootRoute}` export const profilesRoutes: ReadonlyArray = [ { children: [ + { + element: , + id: 'ProfilesLandingPage', + route: '', + }, { element: , id: 'MemberProfilePage', diff --git a/src/apps/talent-search/src/lib/models/Member.ts b/src/apps/talent-search/src/lib/models/Member.ts index fdd5df422..87a8174b6 100644 --- a/src/apps/talent-search/src/lib/models/Member.ts +++ b/src/apps/talent-search/src/lib/models/Member.ts @@ -16,5 +16,6 @@ export default interface Member { stats:Array; country:string; photoURL:string; + skillScore:number; createdAt:number; } diff --git a/src/apps/talent-search/src/lib/models/MemberEmsiSkill.ts b/src/apps/talent-search/src/lib/models/MemberEmsiSkill.ts index 7e8733daf..7a234e163 100644 --- a/src/apps/talent-search/src/lib/models/MemberEmsiSkill.ts +++ b/src/apps/talent-search/src/lib/models/MemberEmsiSkill.ts @@ -1,7 +1,7 @@ export default interface MemberEmsiSkill { skillSources: Array; subCategory: string; - emsiId:string + skillId:string name:string; category:string; isSearched: boolean; diff --git a/src/apps/talent-search/src/lib/services/MatcherService.ts b/src/apps/talent-search/src/lib/services/MatcherService.ts index caba85a8c..5d619a868 100644 --- a/src/apps/talent-search/src/lib/services/MatcherService.ts +++ b/src/apps/talent-search/src/lib/services/MatcherService.ts @@ -18,7 +18,7 @@ export async function retrieveMatchesForSkills( ): Promise>{ const params = new URLSearchParams() skills.forEach(value => params.append('skillId', value.emsiId)) - params.append('sortBy', 'numberOfChallengesWon') + params.append('sortBy', 'skillScore') params.append('sortOrder', 'desc') params.append('page', `${page}`) params.append('perPage', `${pageSize}`) diff --git a/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/SkillSearchResults.tsx b/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/SkillSearchResults.tsx index 17a55bcba..edf484432 100644 --- a/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/SkillSearchResults.tsx +++ b/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/SkillSearchResults.tsx @@ -88,7 +88,7 @@ export default class SkillSearchResult extends Component { emsiSkill.isSearched = false for (let i:number = 0; i < filter.length; i++) { - if (emsiSkill.emsiId === filter[i].emsiId) { + if (emsiSkill.skillId === filter[i].emsiId) { emsiSkill.isSearched = true } } diff --git a/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/MemberHandleRenderer.tsx b/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/MemberHandleRenderer.tsx index 94b4afcdc..250be8512 100644 --- a/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/MemberHandleRenderer.tsx +++ b/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/MemberHandleRenderer.tsx @@ -12,10 +12,7 @@ const MemberHandleRenderer: (member:Member) => JSX.Element window.open(`${EnvironmentConfig.URLS.USER_PROFILE}/${member.handle}`, '_blank') } - let winsText: string = '0 wins' - if (member.stats && member.stats.length > 0) { - winsText = `${member.stats[0].wins} wins` - } + const scoreText: string = `Skill score: ${member.skillScore}` return (
@@ -28,7 +25,7 @@ const MemberHandleRenderer: (member:Member) => JSX.Element
{member.country} | - {winsText} + {scoreText}
Member since  diff --git a/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/SkillTag.tsx b/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/SkillTag.tsx index 5eb53ef86..715c5f2a5 100644 --- a/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/SkillTag.tsx +++ b/src/apps/talent-search/src/routes/talent-search/components/skill-search-results/renderers/SkillTag.tsx @@ -24,8 +24,8 @@ export default class SkillTag extends Component { return (
{this.props.skill.skillSources.includes('ChallengeWin') diff --git a/src/libs/core/lib/profile/data-providers/useMemberSkills.ts b/src/libs/core/lib/profile/data-providers/useMemberSkills.ts index bfab9f715..717978ac6 100644 --- a/src/libs/core/lib/profile/data-providers/useMemberSkills.ts +++ b/src/libs/core/lib/profile/data-providers/useMemberSkills.ts @@ -4,6 +4,7 @@ import useSWR, { SWRResponse } from 'swr' import { UserSkill } from '../user-skill.model' import { getProfileUrl } from '../profile-functions' +// This is currently providing legacy TC skills, not EMSI! export function useMemberSkills(handle?: string): UserSkill[] | undefined { const { data }: SWRResponse = useSWR(handle ? `${getProfileUrl(handle)}/skills` : undefined) diff --git a/src/libs/core/lib/profile/profile-functions/rating.functions.ts b/src/libs/core/lib/profile/profile-functions/rating.functions.ts index 05d1c6620..4ee00920f 100644 --- a/src/libs/core/lib/profile/profile-functions/rating.functions.ts +++ b/src/libs/core/lib/profile/profile-functions/rating.functions.ts @@ -1,5 +1,7 @@ import { CSSProperties } from 'react' +import { EMSISkillSources } from '../user-skill.model' + export const TC_RATING_COLORS: Array<{ color: string, limit: number }> = [{ color: '#555555' /* Grey */, limit: 900, @@ -30,3 +32,7 @@ export function ratingToCSScolor(rating: number): CSSProperties { color, } } + +export function isVerifiedSkill(skillOriginSources: EMSISkillSources[]): boolean { + return skillOriginSources.includes('TCACertified') || skillOriginSources.includes('ChallengeWin') +} diff --git a/src/libs/core/lib/profile/user-profile.model.ts b/src/libs/core/lib/profile/user-profile.model.ts index f32b71f28..749f031e3 100644 --- a/src/libs/core/lib/profile/user-profile.model.ts +++ b/src/libs/core/lib/profile/user-profile.model.ts @@ -1,3 +1,5 @@ +import { UserEMSISkill } from './user-skill.model' + export type TC_TRACKS = 'DEVELOP' | 'DESIGN' | 'DATA_SCIENCE' export interface UserProfile { @@ -6,6 +8,7 @@ export interface UserProfile { description: string diceEnabled: boolean email: string + emsiSkills: Array firstName: string handle: string handleLower: string diff --git a/src/libs/core/lib/profile/user-skill.model.ts b/src/libs/core/lib/profile/user-skill.model.ts index e6fb703aa..2ad685542 100644 --- a/src/libs/core/lib/profile/user-skill.model.ts +++ b/src/libs/core/lib/profile/user-skill.model.ts @@ -7,3 +7,20 @@ export type UserSkill = { sources: skillSources[] tagName: string } + +export type EMSISkillSources = 'TCACertified' | 'SelfPicked' | 'ChallengeWin' + +export type UserEMSISkill = { + id: string + name: string + skillCategory: { + name: string + id: number + } + skillId: string + skillSources: Array + skillSubcategory: { + name: string + id: number + } +} diff --git a/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx new file mode 100644 index 000000000..78f0800ed --- /dev/null +++ b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx @@ -0,0 +1,37 @@ +import { ChangeEvent, FC } from 'react' +import { noop } from 'lodash' + +import { InputMultiselect } from '~/libs/ui' + +import { autoCompleteSkills } from '../../services/emsi-skills' + +interface Option { + label: string + value: string +} + +const fetchSkills = (queryTerm: string): Promise => ( + autoCompleteSkills(queryTerm) + .then(skills => ( + skills.map(skill => ({ + label: skill.name, + value: skill.emsiId, + })) + )) +) + +interface InputSkillSelectorProps { + readonly onChange?: (event: ChangeEvent) => void +} + +const InputSkillSelector: FC = props => ( + +) + +export default InputSkillSelector diff --git a/src/libs/shared/lib/components/input-skill-selector/index.ts b/src/libs/shared/lib/components/input-skill-selector/index.ts new file mode 100644 index 000000000..40ffd6dbd --- /dev/null +++ b/src/libs/shared/lib/components/input-skill-selector/index.ts @@ -0,0 +1 @@ +export { default as InputSkillSelector } from './InputSkillSelector' diff --git a/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts b/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts new file mode 100644 index 000000000..0adc93c5d --- /dev/null +++ b/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts @@ -0,0 +1,8 @@ +import { EnvironmentConfig } from '~/config' +import { xhrGetAsync } from '~/libs/core' + +import Skill from './skill.model' + +export async function autoCompleteSkills(queryTerm: string): Promise { + return xhrGetAsync(`${EnvironmentConfig.API.V5}/emsi-skills/skills/auto-complete?term=${queryTerm}`) +} diff --git a/src/libs/shared/lib/services/emsi-skills/index.ts b/src/libs/shared/lib/services/emsi-skills/index.ts new file mode 100644 index 000000000..214766f94 --- /dev/null +++ b/src/libs/shared/lib/services/emsi-skills/index.ts @@ -0,0 +1,2 @@ +export * from './skill.model' +export * from './emsi-skills.service' diff --git a/src/libs/shared/lib/services/emsi-skills/skill.model.ts b/src/libs/shared/lib/services/emsi-skills/skill.model.ts new file mode 100644 index 000000000..e9c21d4fc --- /dev/null +++ b/src/libs/shared/lib/services/emsi-skills/skill.model.ts @@ -0,0 +1,4 @@ +export default interface Skill { + name: string; + emsiId: string; +} diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/index.ts b/src/libs/ui/lib/components/form/form-groups/form-input/index.ts index f425ff835..bb617f0c3 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/index.ts +++ b/src/libs/ui/lib/components/form/form-groups/form-input/index.ts @@ -3,6 +3,8 @@ export * from './input-image-picker' export * from './form-input-autcomplete-option.enum' export * from './input-rating' export * from './input-select' +export * from './input-multiselect' export * from './input-text' export * from './input-textarea' export { inputOptional, InputWrapper } from './input-wrapper' +export * from './input-date-picker' diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.module.scss new file mode 100644 index 000000000..177f87ba9 --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.module.scss @@ -0,0 +1,73 @@ +@import '@libs/ui/styles/includes'; + +.container { + :global(.react-datepicker__header) { + background-color: $tc-white; + border-bottom: none; + margin: 0 $sp-3; + } + + :global(.react-datepicker__day-name) { + color: $black-100; + font-size: 14px; + font-weight: $font-weight-medium; + font-family: $font-roboto; + } + + :global(.react-datepicker__day) { + color: $black-100; + font-size: 16px; + font-weight: $font-weight-normal; + font-family: $font-roboto; + } + :global(.react-datepicker__day--disabled) { + color: $black-60; + } + + :global(.react-datepicker__day--selected), + :global(.react-datepicker__day--keyboard-selected) { + background-color: $turq-160; + color: $tc-white; + border-radius: 50%; + } + + .headerWrap { + display: flex; + justify-content: space-between; + padding: $sp-2 0; + border-bottom: 2px solid $black-5; + + button { + svg { + width: 27px; + height: 27px; + } + } + + select { + border: none; + background-color: transparent; + + option:hover { + background-color: $turq-160; + color: $tc-white; + } + } + } + + input { + border: none; + pointer-events: none; + height: 22px; + + &:focus { + outline: none; + } + + &::placeholder { + color: $black-60; + opacity: 1; + text-transform: none; + } + } +} \ No newline at end of file diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx new file mode 100644 index 000000000..83ec9adda --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx @@ -0,0 +1,114 @@ +/* eslint-disable react/jsx-no-bind */ +import { FC } from 'react' +import { getMonth, getYear } from 'date-fns' +import { range } from 'lodash' +import DatePicker from 'react-datepicker' +import classNames from 'classnames' +import 'react-datepicker/dist/react-datepicker.css' + +import { InputWrapper } from '../input-wrapper' +import { IconOutline } from '../../../../svgs' + +import styles from './InputDatePicker.module.scss' + +interface InputDatePickerProps { + date: Date | undefined + onChange: (date: Date | null) => void + readonly className?: string + readonly dirty?: boolean + readonly disabled: boolean + readonly error?: string + readonly hideInlineErrors?: boolean + readonly hint?: string + readonly label: string | JSX.Element + readonly maxDate?: Date | null | undefined; + readonly maxTime?: Date | undefined; + readonly minDate?: Date | null | undefined; + readonly minTime?: Date | undefined; + readonly placeholder?: string + readonly tabIndex?: number +} + +const years: number[] = range(1979, getYear(new Date()) + 1, 1) +const months: string[] = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +] + +const InputDatePicker: FC = (props: InputDatePickerProps) => { + function renderCustomHeader({ + date, + changeYear, + changeMonth, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }: any): JSX.Element { + return ( +
+ + + + + + + +
+ ) + } + + return ( + + + + ) +} + +export default InputDatePicker diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/index.ts b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/index.ts new file mode 100644 index 000000000..604cdb2b5 --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/index.ts @@ -0,0 +1 @@ +export { default as InputDatePicker } from './InputDatePicker' diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss new file mode 100644 index 000000000..d1b667f2c --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss @@ -0,0 +1,146 @@ +@import '../../../../../styles/includes'; + +.multiselect .ms { + display: block; + + &:global(__value-container) { + display: flex; + align-items: center; + flex: 1; + flex-wrap: wrap; + position: relative; + overflow: hidden; + margin: 0 10px; + padding: 0; + gap: 8px; + } + + &:global(__indicators) { + display: none; + } + + &:global(__placeholder) { + position: absolute; + font-size: 14px; + line-height: 16px; + color: $black-60; + } + + &:global(__control) { + border: 0 none; + box-shadow: none; + + align-items: center; + cursor: default; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + min-height: 0; + outline: 0!important; + position: relative; + transition: all 100ms; + background: none; + border-radius: 4px; + } + + &:global(__input-container) { + font-size: 14px; + line-height: 16px; + color: $black-60; + display: inline-grid; + flex: 1 1 auto; + margin: 0; + grid-template-columns: 0 min-content; + padding: 0; + visibility: visible; + } + + &:global(__multi-value) { + margin: 0; + background: $teal-140; + color: $tc-white; + border-radius: 4px; + + &:global(__remove) { + cursor: pointer; + border: 0 none; + background: none; + outline: none; + appearance: none; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + padding: 2px; + margin: 2px 6px 2px 0; + + svg { + display: block; + width: 16px; + height: 16px; + } + } + &:global(__label) { + color: $tc-white; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: 4px; + padding: 4px; + padding-left: 8px; + padding-right: 2px; + font-size: 14px; + line-height: 16px; + letter-spacing: 0.5px; + font-family: $font-roboto; + font-weight: $font-weight-medium; + } + } + + &:global(__menu) { + top: 100%; + position: absolute; + width: 100%; + z-index: 1; + background-color: $tc-white; + border-radius: 4px; + box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.25); + margin-bottom: 5px; + margin-top: 5px; + border: 1px solid $black-40; + &:global(-list) { + max-height: 300px; + overflow-y: auto; + position: relative; + -webkit-overflow-scrolling: touch; + padding: 8px 0; + } + &:global(-notice) { + text-align: center; + color: #999; + padding: 8px 12px; + } + } + &:global(__option) { + cursor: default; + display: block; + width: 100%; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + background-color: transparent; + color: $black-100; + padding: 8px 16px; + + font-size: 16px; + line-height: 24px; + + &:global(--is-focused) { + background-color: $turq-160; + color: $tc-white; + } + } +} + +.multiselect { + margin: 8px -10px 0; +} diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.stories.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.stories.tsx new file mode 100644 index 000000000..5bfa617c9 --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.stories.tsx @@ -0,0 +1,45 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable camelcase */ +import { Meta, StoryObj } from '@storybook/react' + +import { InputMultiselect } from '.' + +const meta: Meta = { + argTypes: { + }, + component: InputMultiselect, + excludeStories: /.*Decorator$/, + // tags: ['autodocs'], + title: 'Forms/InputMultiselect', +} + +export default meta + +type Story = StoryObj; + +export const Basic: Story = { + args: { + onChange: d => console.log(d), + onFetchOptions: d => Promise.resolve(d ? [ + { label: 'Option 1', value: '1' }, + { label: 'Option 2', value: '2' }, + { label: 'Option 3', value: '3' }, + { label: 'Option 4', value: '4' }, + { label: 'Option 5', value: '5' }, + { label: 'Option 6', value: '6' }, + { label: 'Option 7', value: '7' }, + { label: 'Option 8', value: '8' }, + { label: 'Option 9', value: '9' }, + { label: 'Option 10', value: '10' }, + { label: 'Option 11', value: '11' }, + { label: 'Option 12', value: '12' }, + { label: 'Option 13', value: '13' }, + { label: 'Option 14', value: '14' }, + { label: 'Option 15', value: '15' }, + { label: 'Option 16', value: '16' }, + { label: 'Option 17', value: '17' }, + { label: 'Option 18', value: '18' }, + { label: 'Option 19', value: '19' }, + ] : []), + }, +} diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx new file mode 100644 index 000000000..33bd0405b --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx @@ -0,0 +1,82 @@ +import { + ChangeEvent, + FC, + ReactNode, +} from 'react' +import { noop } from 'lodash' +import { components } from 'react-select' +import AsyncSelect from 'react-select/async' + +import { InputWrapper } from '../input-wrapper' +import { IconSolid } from '../../../../svgs' + +import styles from './InputMultiselect.module.scss' + +export interface InputMultiselectOption { + label?: ReactNode + value: string +} + +interface InputMultiselectProps { + readonly dirty?: boolean + readonly disabled?: boolean + readonly error?: string + readonly hideInlineErrors?: boolean + readonly hint?: string + readonly label?: string + readonly name: string + readonly onChange: (event: ChangeEvent) => void + readonly options?: ReadonlyArray + readonly placeholder?: string + readonly tabIndex?: number + readonly value?: string + readonly onFetchOptions?: (query: string) => Promise +} + +const MultiValueRemove: FC = (props: any) => ( + + + +) + +const InputMultiselect: FC = (props: InputMultiselectProps) => { + + function handleOnChange(options: readonly InputMultiselectOption[]): void { + props.onChange({ + target: { value: options }, + } as unknown as ChangeEvent) + } + + return ( + + + MultiValueRemove, + }} + /> + + ) +} + +export default InputMultiselect diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts new file mode 100644 index 000000000..38d4054f9 --- /dev/null +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts @@ -0,0 +1,2 @@ +export { default as InputMultiselect } from './InputMultiselect' +export { type InputMultiselectOption } from './InputMultiselect' From 1d8069a133c252e8ca66141d15b3256202a58d84 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 5 Jul 2023 13:18:46 -0500 Subject: [PATCH 15/25] Remove the "welcome to Topcoder" app name https://topcoder.atlassian.net/browse/MP-171 --- src/config/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index 98cb64460..c32a2699a 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -17,7 +17,7 @@ export enum ToolTitle { profiles = 'Profiles', tcAcademy = 'Topcoder Academy', selfService = 'Self Service Challenges', - onboarding = 'Welcome to Topcoder', + onboarding = '', talentSearch = 'Talent Search' } From 445f4f1a473afda921e6abf10e75ad27c1f66e30 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 5 Jul 2023 13:32:42 -0500 Subject: [PATCH 16/25] =?UTF-8?q?Let=E2=80=99s=20try=20this=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index c32a2699a..0d6d7858f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -17,7 +17,7 @@ export enum ToolTitle { profiles = 'Profiles', tcAcademy = 'Topcoder Academy', selfService = 'Self Service Challenges', - onboarding = '', + onboarding = ' ', talentSearch = 'Talent Search' } From 573bc1b9148b2dd68b7d0fa01dc917882b9025e2 Mon Sep 17 00:00:00 2001 From: dat Date: Sat, 8 Jul 2023 20:23:31 +0700 Subject: [PATCH 17/25] - Change number of onboarding steps from 7 to 6 - Exiting out of onboarding flow takes the user to their profile --- .../src/pages/account-details/index.tsx | 26 ++++++++++++++++--- .../onboarding/src/pages/educations/index.tsx | 4 +-- .../src/pages/personalization/index.tsx | 4 +-- .../onboarding/src/pages/skills/index.tsx | 4 +-- src/apps/onboarding/src/pages/start/index.tsx | 4 +-- src/apps/onboarding/src/pages/works/index.tsx | 4 +-- src/config/environments/default.env.ts | 1 + .../environments/global-config.model.ts | 1 + 8 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx index 1912eafdb..bfdb25573 100644 --- a/src/apps/onboarding/src/pages/account-details/index.tsx +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -10,6 +10,8 @@ import classNames from 'classnames' import { Button, InputSelect, PageDivider } from '~/libs/ui' import { getCountryLookup } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' +import { EnvironmentConfig } from '~/config' +import { Member } from '~/apps/talent-search/src/lib/models' import { ProgressBar } from '../../components/progress-bar' @@ -30,6 +32,7 @@ const blankConnectInfo: ConnectInfo = emptyConnectInfo() const PageAccountDetailsContent: FC<{ reduxAddress: MemberAddress | null reduxConnectInfo: ConnectInfo | null + reduxMemberInfo: Member | null updateMemberConnectInfos: (infos: ConnectInfo[]) => void createMemberConnectInfos: (infos: ConnectInfo[]) => void updateMemberHomeAddresss: (infos: MemberAddress[]) => void @@ -133,7 +136,11 @@ const PageAccountDetailsContent: FC<{ && !loadingAddress && !loadingConnectInfo && shouldNavigateTo.current) { - navigate(shouldNavigateTo.current) + if (shouldNavigateTo.current.startsWith('../')) { + navigate(shouldNavigateTo.current) + } else { + window.location.href = shouldNavigateTo.current + } } /* eslint-disable react-hooks/exhaustive-deps */ }, [loadingAddress, loadingConnectInfo]) @@ -313,8 +320,8 @@ const PageAccountDetailsContent: FC<{
@@ -337,7 +344,16 @@ const PageAccountDetailsContent: FC<{ size='lg' primary iconToLeft - disabled={!_.isEmpty(formErrors)} + disabled={!_.isEmpty(formErrors) || !props.reduxMemberInfo} + onClick={() => { + if (loadingAddress || loadingConnectInfo) { + shouldNavigateTo.current + = `${EnvironmentConfig.USER_PROFILE_URL}/${props.reduxMemberInfo?.handle}` + } else { + window.location.href + = `${EnvironmentConfig.USER_PROFILE_URL}/${props.reduxMemberInfo?.handle}` + } + }} > next @@ -352,6 +368,7 @@ const mapStateToProps: any = (state: any) => { loadingMemberInfo, address, connectInfo, + memberInfo, }: any = state.member return { @@ -359,6 +376,7 @@ const mapStateToProps: any = (state: any) => { loadingMemberTraits, reduxAddress: address, reduxConnectInfo: connectInfo, + reduxMemberInfo: memberInfo, } } diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 7feb275ce..695d55bdc 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -118,8 +118,8 @@ export const PageEducationsContent: FC<{
diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index fdc8e51ad..80724e43c 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -195,8 +195,8 @@ const PagePersonalizationContent: FC<{
diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index f876579fd..5240ec1ba 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -34,8 +34,8 @@ export const PageSkills: FC<{}> = () => {
diff --git a/src/apps/onboarding/src/pages/start/index.tsx b/src/apps/onboarding/src/pages/start/index.tsx index 6c76ae4ae..bb177135a 100644 --- a/src/apps/onboarding/src/pages/start/index.tsx +++ b/src/apps/onboarding/src/pages/start/index.tsx @@ -55,8 +55,8 @@ export const PageStart: FC<{}> = () => {
diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index 692cb4b49..e42f9aa74 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -128,8 +128,8 @@ export const PageWorksContent: FC<{
diff --git a/src/config/environments/default.env.ts b/src/config/environments/default.env.ts index 9e728c9c4..ed9683888 100644 --- a/src/config/environments/default.env.ts +++ b/src/config/environments/default.env.ts @@ -13,6 +13,7 @@ export const TC_DOMAIN: string = get({ export const TOPCODER_URL: string = `https://www.${TC_DOMAIN}` export const PLATFORMUI_URL: string = `https://platform-ui.${TC_DOMAIN}` +export const USER_PROFILE_URL: string = `https://profiles.${TC_DOMAIN}` export const API = { V1: `https://api.${TC_DOMAIN}/v1`, diff --git a/src/config/environments/global-config.model.ts b/src/config/environments/global-config.model.ts index db79f75bb..adcc3d426 100644 --- a/src/config/environments/global-config.model.ts +++ b/src/config/environments/global-config.model.ts @@ -2,6 +2,7 @@ export interface GlobalConfig { TC_DOMAIN: string TOPCODER_URL: string PLATFORMUI_URL: string + USER_PROFILE_URL: string API: { V1: string V3: string From fa70d2fd942c4e76f01cc2a3cbab94f0c382d3c9 Mon Sep 17 00:00:00 2001 From: dat Date: Mon, 10 Jul 2023 01:46:16 +0700 Subject: [PATCH 18/25] New onboarding look and feel --- .../src/assets/images/avatar-placeholder.png | Bin 0 -> 8726 bytes .../src/assets/images/back-green.svg | 3 + .../onboarding/src/assets/images/calendar.svg | 3 + .../src/assets/images/cheveron-down.svg | 3 + .../onboarding/src/assets/images/edit.svg | 6 +- .../onboarding/src/assets/images/trash.svg | 6 +- .../src/components/DateInput/index.tsx | 30 +--- .../components/DateInput/styles.module.scss | 4 +- .../src/components/FieldAvatar/index.tsx | 144 +++++++++------- .../components/FieldAvatar/styles.module.scss | 27 +-- .../src/components/card-item/index.tsx | 51 ++++++ .../components/card-item/styles.module.scss | 41 +++++ .../components/modal-add-education/index.tsx | 92 +++++----- .../modal-add-education/styles.module.scss | 23 +++ .../src/components/modal-add-work/index.tsx | 154 ++++++++--------- .../modal-add-work/styles.module.scss | 23 +++ .../components/modal-upload-photo/index.tsx | 140 +++++++++++++++ .../modal-upload-photo/styles.module.scss | 99 +++++++++++ .../src/components/progress-bar/index.tsx | 25 ++- .../progress-bar/styles.module.scss | 30 +--- .../src/hooks/useAutoSavePersonalization.ts | 81 +++++++++ .../onboarding/src/models/EducationInfo.ts | 2 - .../src/models/PersonalizationInfo.ts | 8 +- src/apps/onboarding/src/models/WorkInfo.ts | 2 - src/apps/onboarding/src/onboarding.routes.tsx | 7 +- .../src/pages/account-details/index.tsx | 12 +- .../pages/account-details/styles.module.scss | 4 +- .../onboarding/src/pages/educations/index.tsx | 83 ++++----- .../src/pages/educations/styles.module.scss | 3 +- .../onboarding/src/pages/onboarding/index.tsx | 49 ++++-- .../src/pages/onboarding/styles.module.scss | 53 +++++- .../src/pages/open-to-work/index.tsx | 143 ++++++++++++++++ .../src/pages/open-to-work/styles.module.scss | 17 ++ .../src/pages/personalization/index.tsx | 162 ++++-------------- .../pages/personalization/styles.module.scss | 3 +- .../onboarding/src/pages/skills/index.tsx | 55 +++--- .../src/pages/skills/styles.module.scss | 6 +- src/apps/onboarding/src/pages/start/index.tsx | 4 +- .../src/pages/start/styles.module.scss | 2 +- src/apps/onboarding/src/pages/works/index.tsx | 97 ++++------- .../src/pages/works/styles.module.scss | 15 +- .../onboarding/src/redux/actions/member.ts | 27 +-- .../onboarding/src/styles/global/_color.scss | 7 + .../onboarding/src/styles/global/_flex.scss | 6 + .../onboarding/src/styles/global/_grid.scss | 15 ++ .../onboarding/src/styles/global/_index.scss | 4 + .../onboarding/src/styles/global/_layout.scss | 24 +++ .../onboarding/src/styles/global/_text.scss | 3 + 48 files changed, 1201 insertions(+), 597 deletions(-) create mode 100644 src/apps/onboarding/src/assets/images/avatar-placeholder.png create mode 100644 src/apps/onboarding/src/assets/images/back-green.svg create mode 100644 src/apps/onboarding/src/assets/images/calendar.svg create mode 100644 src/apps/onboarding/src/assets/images/cheveron-down.svg create mode 100644 src/apps/onboarding/src/components/card-item/index.tsx create mode 100644 src/apps/onboarding/src/components/card-item/styles.module.scss create mode 100644 src/apps/onboarding/src/components/modal-upload-photo/index.tsx create mode 100644 src/apps/onboarding/src/components/modal-upload-photo/styles.module.scss create mode 100644 src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts create mode 100644 src/apps/onboarding/src/pages/open-to-work/index.tsx create mode 100644 src/apps/onboarding/src/pages/open-to-work/styles.module.scss create mode 100644 src/apps/onboarding/src/styles/global/_color.scss create mode 100644 src/apps/onboarding/src/styles/global/_grid.scss create mode 100644 src/apps/onboarding/src/styles/global/_text.scss diff --git a/src/apps/onboarding/src/assets/images/avatar-placeholder.png b/src/apps/onboarding/src/assets/images/avatar-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..78a9e9f514a4088fe3682c82b38245816837226a GIT binary patch literal 8726 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 + + \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/calendar.svg b/src/apps/onboarding/src/assets/images/calendar.svg new file mode 100644 index 000000000..f33a6a342 --- /dev/null +++ b/src/apps/onboarding/src/assets/images/calendar.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/cheveron-down.svg b/src/apps/onboarding/src/assets/images/cheveron-down.svg new file mode 100644 index 000000000..f6af4858a --- /dev/null +++ b/src/apps/onboarding/src/assets/images/cheveron-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/edit.svg b/src/apps/onboarding/src/assets/images/edit.svg index 87e715e15..e8aa0411d 100644 --- a/src/apps/onboarding/src/assets/images/edit.svg +++ b/src/apps/onboarding/src/assets/images/edit.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/trash.svg b/src/apps/onboarding/src/assets/images/trash.svg index 4253c2a03..9687957df 100644 --- a/src/apps/onboarding/src/assets/images/trash.svg +++ b/src/apps/onboarding/src/assets/images/trash.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/src/apps/onboarding/src/components/DateInput/index.tsx b/src/apps/onboarding/src/components/DateInput/index.tsx index f4ff0e3c4..9257029a1 100644 --- a/src/apps/onboarding/src/components/DateInput/index.tsx +++ b/src/apps/onboarding/src/components/DateInput/index.tsx @@ -16,36 +16,10 @@ import moment from 'moment' import 'react-datepicker/dist/react-datepicker.css' +import { ReactComponent as CalendarIcon } from '../../assets/images/calendar.svg' +import { ReactComponent as ArrowIcon } from '../../assets/images/cheveron-down.svg' import styles from './styles.module.scss' -const CalendarIcon: any = () => ( - - - -) -const ArrowIcon: any = () => ( - -) - interface CalendarContainerProps { children?: any } diff --git a/src/apps/onboarding/src/components/DateInput/styles.module.scss b/src/apps/onboarding/src/components/DateInput/styles.module.scss index 7e82dc04f..b898c441f 100644 --- a/src/apps/onboarding/src/components/DateInput/styles.module.scss +++ b/src/apps/onboarding/src/components/DateInput/styles.module.scss @@ -1,3 +1,5 @@ +@import '@libs/ui/styles/includes'; + .datepicker-wrapper { position: relative; padding: 0 10px; @@ -61,7 +63,7 @@ font-weight: 400; &::placeholder { - color: #7F7F7F; + color: $black-60; font-size: 14px; font-weight: 400; text-transform: none !important; diff --git a/src/apps/onboarding/src/components/FieldAvatar/index.tsx b/src/apps/onboarding/src/components/FieldAvatar/index.tsx index ab04c651b..194645f44 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/index.tsx +++ b/src/apps/onboarding/src/components/FieldAvatar/index.tsx @@ -7,83 +7,62 @@ * * A Form Field Is a wrapper for input to add the label to it */ -import React, { Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useRef, useState } from 'react' +import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' import classNames from 'classnames' import _ from 'lodash' import { Button, IconOutline } from '~/libs/ui' -import { updateMemberPhotoAsync } from '~/libs/core' import styles from './styles.module.scss' import MemberInfo from '../../models/MemberInfo' +import AvatarPlaceholder from '../../assets/images/avatar-placeholder.png' +import ModalUploadPhoto from '../modal-upload-photo' interface FieldAvatarProps { className?: string memberInfo?: MemberInfo, setMemberPhotoUrl: (photoUrl: string) => void + updateMemberPhotoUrl: (photoUrl: string) => void } const FieldAvatar: FC = ({ className, memberInfo, setMemberPhotoUrl, + updateMemberPhotoUrl, }: FieldAvatarProps) => { - const fileElRef: MutableRefObject = useRef() const [imgUrl, setImgUrl] = useState('') useEffect(() => { - if (memberInfo && !imgUrl) { + if (memberInfo) { setImgUrl(memberInfo.photoURL) } - /* eslint-disable react-hooks/exhaustive-deps */ + /* eslint-disable react-hooks/exhaustive-deps */ }, [memberInfo]) - const [file, setFile]: [File | undefined, Dispatch>] - = useState(undefined) + const [isPhotoEditMode, setIsPhotoEditMode]: [boolean, Dispatch>] + = useState(false) const [isSaving, setIsSaving]: [boolean, Dispatch>] = useState(false) - function handleFilePickChange(event: React.ChangeEvent): void { - const pickedFile: File | undefined = event.target.files?.[0] - - if (pickedFile && pickedFile?.size < 2000000) { // max 2mb limit - setFile(pickedFile) - } else { - setFile(undefined) - } + function handleModifyPhotoModalClose(): void { + setIsPhotoEditMode(false) } - async function handleModifyPhotoSave(newImgUrl: string): Promise { - const formData: FormData = new FormData() - - if (file && memberInfo) { - formData.append('photo', file) - - setIsSaving(true) - try { - setMemberPhotoUrl(newImgUrl) - await updateMemberPhotoAsync(memberInfo.handle, formData) - } catch (error) { - } - - setIsSaving(false) + async function handleRemovePhoto(): Promise { + setIsSaving(true) + try { + await updateMemberPhotoUrl('') + } catch (error) { } - } - useEffect(() => { - if (file) { - const newImgUrl: string = URL.createObjectURL(file) - setImgUrl(newImgUrl) - handleModifyPhotoSave(newImgUrl) - .then(_.noop) - } - /* eslint-disable react-hooks/exhaustive-deps */ - }, [file]) + setIsSaving(false) + } return (
-

A picture can speak a thousand words

+

Photo

= ({ }, )} > - {imgUrl ? (avatar) : null} + {imgUrl ? ( + avatar + ) : ( + avatar + )}
-
- Requirements: -
    -
  • PNG or JPG format.
  • -
  • Maximum size: 2MB.
  • -
+
+ + Make a great first impression to potential customers with a + professional photo that represents your style. + + {imgUrl ? ( +
+ + +
+ ) : ( + + )}
- - + + { + isPhotoEditMode && memberInfo && ( + + ) + }
) } diff --git a/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss b/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss index 20f0c4a3d..f8d472596 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss +++ b/src/apps/onboarding/src/components/FieldAvatar/styles.module.scss @@ -10,20 +10,27 @@ } .blockImg { - width: 200px; - height: 200px; + width: 120px; + height: 120px; display: flex; - background-color: #eee; - border: 1px dashed gray; + border-radius: 100%; + overflow: hidden; + border: 3px dashed white; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.09); + flex-shrink: 0; - &.haveImg { - border: none; - background-color: transparent; - } - - img { + .img { width: 100%; height: 100%; object-fit: contain; } + + .imgPlaceholder { + width: 144px; + height: 144px; + object-fit: contain; + max-width: none; + margin-left: -15px; + margin-top: -11px; + } } \ No newline at end of file diff --git a/src/apps/onboarding/src/components/card-item/index.tsx b/src/apps/onboarding/src/components/card-item/index.tsx new file mode 100644 index 000000000..993d85b4c --- /dev/null +++ b/src/apps/onboarding/src/components/card-item/index.tsx @@ -0,0 +1,51 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +import React, { FC } from 'react' +import classNames from 'classnames' + +import styles from './styles.module.scss' +import IconEdit from '../../assets/images/edit.svg' +import IconTrash from '../../assets/images/trash.svg' + +interface CardItemProps { + title: string + subTitle: string + description: string + className?: string + disabled?: boolean + onEdit?: () => void + onDelete?: () => void +} + +export const CardItem: FC = (props: CardItemProps) => ( +
+
+ {props.title} + +
+ + +
+
+ {props.subTitle} + {props.description} +
+) + +export default CardItem diff --git a/src/apps/onboarding/src/components/card-item/styles.module.scss b/src/apps/onboarding/src/components/card-item/styles.module.scss new file mode 100644 index 000000000..77acdf163 --- /dev/null +++ b/src/apps/onboarding/src/components/card-item/styles.module.scss @@ -0,0 +1,41 @@ +@import '../../../../../libs/ui/lib/styles/includes'; + +.container { + border-radius: 8px; + border: 1px solid $black-20; + padding: 24px; +} + +.textTitle { + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 24px; +} + +.textSubTitle { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; +} + +.textDescription { + color: $black-60; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; +} + +.blockBtns { + flex-shrink: 0; +} + +.btn { + flex-shrink: 0; + + &:disabled { + opacity: 0.2; + } +} diff --git a/src/apps/onboarding/src/components/modal-add-education/index.tsx b/src/apps/onboarding/src/components/modal-add-education/index.tsx index 4afbd8a01..2465ba4b9 100644 --- a/src/apps/onboarding/src/components/modal-add-education/index.tsx +++ b/src/apps/onboarding/src/components/modal-add-education/index.tsx @@ -7,15 +7,12 @@ import moment from 'moment' import _ from 'lodash' import { BaseModal, Button, InputText } from '~/libs/ui' -import { FormInputCheckbox } from '~/apps/self-service/src/components/form-elements' import styles from './styles.module.scss' import EducationInfo, { emptyEducationInfo } from '../../models/EducationInfo' import FormField from '../FormField' import DateInput from '../DateInput' -const FormInputCheckboxMiddleware: any = FormInputCheckbox as any - interface ModalAddEducationProps { onClose?: () => void editingEducation?: EducationInfo | null @@ -66,47 +63,52 @@ const ModalAddEducation: FC = (props: ModalAddEducationP return ( { - if (validateField()) { - const endDate: Date | undefined = educationInfo.endDate - let endDateString: string = endDate ? moment(endDate) - .format('YYYY') : '' - if (!educationInfo.graduated) { - endDateString = 'current' - } +
+
)} onClose={props.onClose || _.noop} open size='body' - title={props.editingEducation ? 'Edit education:' : 'Add education:'} + title={props.editingEducation ? 'Edit Education' : 'Add Education'} classNames={{ modal: styles.infoModal }} >
{ setEducationInfo({ @@ -114,7 +116,7 @@ const ModalAddEducation: FC = (props: ModalAddEducationP collegeName: event.target.value, }) }} - placeholder='Name of College or University' + placeholder='Enter school' tabIndex={0} type='text' dirty @@ -124,20 +126,20 @@ const ModalAddEducation: FC = (props: ModalAddEducationP
setEducationInfo({ ...educationInfo, major: event.target.value, })} - placeholder='Major' + placeholder='Enter degree' tabIndex={0} type='text' dirty error={formErrors.major} />
-
+
@@ -156,7 +158,7 @@ const ModalAddEducation: FC = (props: ModalAddEducationP }) }} style2 - placeholder='Start date' + placeholder='Select start date' />
@@ -164,10 +166,9 @@ const ModalAddEducation: FC = (props: ModalAddEducationP className='flex-1' > { setEducationInfo({ @@ -176,22 +177,11 @@ const ModalAddEducation: FC = (props: ModalAddEducationP }) }} style2 - placeholder='End date' + placeholder='Select end date' />
- { - setEducationInfo({ - ...educationInfo, - graduated: e.target.checked, - }) - }} - />
) diff --git a/src/apps/onboarding/src/components/modal-add-education/styles.module.scss b/src/apps/onboarding/src/components/modal-add-education/styles.module.scss index a8d052c57..bc191d879 100644 --- a/src/apps/onboarding/src/components/modal-add-education/styles.module.scss +++ b/src/apps/onboarding/src/components/modal-add-education/styles.module.scss @@ -1,8 +1,31 @@ @import '@libs/ui/styles/includes'; .infoModal { + padding: 35px 32px 32px 32px !important; + max-width: 600px !important; + :global(.react-responsive-modal-closeButton) { display: flex; + right: 32px; + top: 46px !important; + + path { + color: $turq-160; + } + } + + hr { + margin-top: 30px; + } + + h3 { + font-family: $font-barlow; + color: $black-100; + font-size: 34px; + font-style: normal; + font-weight: 600; + line-height: 40px; + text-transform: none; } .modalContent { diff --git a/src/apps/onboarding/src/components/modal-add-work/index.tsx b/src/apps/onboarding/src/components/modal-add-work/index.tsx index 6941e4451..fc99606b7 100644 --- a/src/apps/onboarding/src/components/modal-add-work/index.tsx +++ b/src/apps/onboarding/src/components/modal-add-work/index.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames' import _ from 'lodash' import moment from 'moment' -import { BaseModal, Button, InputSelect, InputText, InputTextarea } from '~/libs/ui' +import { BaseModal, Button, InputSelect, InputText } from '~/libs/ui' import { FormInputCheckbox } from '~/apps/self-service/src/components/form-elements' import styles from './styles.module.scss' @@ -73,47 +73,55 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { return ( { - if (validateField()) { - const endDate: Date | undefined = workInfo.endDate - let endDateString: string = endDate ? moment(endDate) - .format('YYYY') : '' - if (workInfo.currentlyWorking) { - endDateString = 'current' - } - - let startDateString: string = workInfo.startDate ? moment(workInfo.startDate) - .format('YYYY') : '' - if (startDateString) { - startDateString += '-' +
+
)} onClose={props.onClose || _.noop} open size='body' - title={props.editingWork ? 'Edit work experience:' : 'Add work experience:'} + title={props.editingWork ? 'Edit Experience' : 'Add Experience'} classNames={{ modal: styles.infoModal }} >
{ setWorkInfo({ @@ -121,59 +129,57 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { company: event.target.value, }) }} - placeholder='Company Name' + placeholder='Enter company' tabIndex={0} type='text' dirty error={formErrors.company} />
-
-
- setWorkInfo({ - ...workInfo, - industry: event.target.value, - })} - name='industry' - label='Industry' - placeholder='Industry' - /> -
-
- setWorkInfo({ - ...workInfo, - city: event.target.value, - })} - placeholder='Location' - tabIndex={0} - type='text' - /> -
-
setWorkInfo({ ...workInfo, position: event.target.value, })} - placeholder='Position / Job Title' + placeholder='Enter position' tabIndex={0} type='text' dirty error={formErrors.position} />
-
+
+ setWorkInfo({ + ...workInfo, + industry: event.target.value, + })} + name='industry' + label='Industry' + placeholder='Select industry' + /> +
+
+ setWorkInfo({ + ...workInfo, + city: event.target.value, + })} + placeholder='Enter city, country' + tabIndex={0} + type='text' + /> +
+
@@ -192,7 +198,7 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { }) }} style2 - placeholder='Start date' + placeholder='Select start date' />
@@ -212,13 +218,13 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { }) }} style2 - placeholder='End date' + placeholder='Select end date' />
{ @@ -228,20 +234,6 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { }) }} /> -
- setWorkInfo({ - ...workInfo, - description: event.target.value, - })} - onBlur={_.noop} - placeholder='Description' - tabIndex={0} - /> -
) diff --git a/src/apps/onboarding/src/components/modal-add-work/styles.module.scss b/src/apps/onboarding/src/components/modal-add-work/styles.module.scss index a8d052c57..bc191d879 100644 --- a/src/apps/onboarding/src/components/modal-add-work/styles.module.scss +++ b/src/apps/onboarding/src/components/modal-add-work/styles.module.scss @@ -1,8 +1,31 @@ @import '@libs/ui/styles/includes'; .infoModal { + padding: 35px 32px 32px 32px !important; + max-width: 600px !important; + :global(.react-responsive-modal-closeButton) { display: flex; + right: 32px; + top: 46px !important; + + path { + color: $turq-160; + } + } + + hr { + margin-top: 30px; + } + + h3 { + font-family: $font-barlow; + color: $black-100; + font-size: 34px; + font-style: normal; + font-weight: 600; + line-height: 40px; + text-transform: none; } .modalContent { diff --git a/src/apps/onboarding/src/components/modal-upload-photo/index.tsx b/src/apps/onboarding/src/components/modal-upload-photo/index.tsx new file mode 100644 index 000000000..07c20ad2c --- /dev/null +++ b/src/apps/onboarding/src/components/modal-upload-photo/index.tsx @@ -0,0 +1,140 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable sort-keys */ +/* eslint-disable unicorn/no-null */ +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react' +import classNames from 'classnames' +import _ from 'lodash' +import { DropzoneState, useDropzone } from 'react-dropzone' + +import { BaseModal, Button } from '~/libs/ui' +import { updateMemberPhotoAsync } from '~/libs/core' + +import styles from './styles.module.scss' +import MemberInfo from '../../models/MemberInfo' + +interface ModalUploadPhotoProps { + onClose?: () => void + memberInfo?: MemberInfo + setMemberPhotoUrl: (photoUrl: string) => void +} + +const ModalUploadPhoto: FC = (props: ModalUploadPhotoProps) => { + const [myFiles, setMyFiles] = useState([]) + const [imgUrl, setImgUrl] = useState('') + const [isSaving, setIsSaving]: [boolean, Dispatch>] + = useState(false) + + const onDrop = useCallback((acceptedFiles: File[]) => { + setMyFiles([...myFiles, ...acceptedFiles]) + }, [myFiles]) + + const { + getRootProps, + getInputProps, + }: DropzoneState = useDropzone({ + multiple: false, + accept: 'image/jpg, image/jpeg, image/png', + minSize: 1, + maxSize: 2097152, + disabled: isSaving, + onDrop, + }) + + useEffect(() => { + if (myFiles && myFiles.length) { + setImgUrl(URL.createObjectURL(myFiles[0])) + } + }, [myFiles]) + + async function handleModifyPhotoSave(): Promise { + const formData: FormData = new FormData() + + if (myFiles && myFiles.length > 0 && props.memberInfo) { + formData.append('photo', myFiles[0]) + + setIsSaving(true) + try { + await updateMemberPhotoAsync(props.memberInfo.handle, formData) + props.setMemberPhotoUrl(URL.createObjectURL(myFiles[0])) + setMyFiles([]) + } catch (error) { + } + + setIsSaving(false) + } + } + + return ( + +
+ )} + onClose={props.onClose || _.noop} + open + size='body' + title='Profile Photo' + classNames={{ modal: styles.infoModal }} + > +
+ {(!isSaving && !imgUrl) ? ( +
+ + + Drag and drop your file here +
+ or +
+ + BROWSE + +
+ ) : null} + {(!isSaving && imgUrl) ? ( +
+ + +
+ ) : null} + + {isSaving ? ( +
+ Uploading... +
+
+
+
+ ) : null} + +
+ Add a photo that you would like to share to the customers and community members. +
+ Requirements: +
    +
  • PNG or JPG format.
  • +
  • Maximum size: 2MB.
  • +
+
+
+ + ) +} + +export default ModalUploadPhoto diff --git a/src/apps/onboarding/src/components/modal-upload-photo/styles.module.scss b/src/apps/onboarding/src/components/modal-upload-photo/styles.module.scss new file mode 100644 index 000000000..70068533a --- /dev/null +++ b/src/apps/onboarding/src/components/modal-upload-photo/styles.module.scss @@ -0,0 +1,99 @@ +@import '@libs/ui/styles/includes'; + +.infoModal { + padding: 35px 32px 32px 32px !important; + max-width: 600px !important; + + :global(.react-responsive-modal-closeButton) { + display: flex; + right: 32px; + top: 46px !important; + + path { + color: $turq-160; + } + } + + hr { + margin-top: 30px; + } + + h3 { + font-family: $font-barlow; + color: $black-100; + font-size: 34px; + font-style: normal; + font-weight: 600; + line-height: 40px; + text-transform: none; + } + + .modalContent { + width: 100%; + } +} + +.blockDropZone { + width: 256px; + height: 256px; + margin-right: 24px; + border: 1px dashed $turq-160; + border-radius: 4px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: rgba(224, 250, 243, 0.30); + + .textDragAndDrop { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + text-align: center; + } + + .textBrowse { + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 14px; + color: $turq-160; + margin-top: 8px; + } + + .blockProgressContainer { + background-color: white; + width: 100px; + height: 4px; + display: flex; + margin-top: 8px; + + .blockProgress { + width: 50%; + height: 100%; + background-color: $turq-100; + } + } +} + +.blockPhoto { + width: 256px; + height: 256px; + margin-right: 24px; + flex-shrink: 0; + border: 1px dashed $turq-160; + background-color: rgba(224, 250, 243, 0.30); + + img { + width: 100%; + height: 100%; + object-fit: contain; + } +} + +.listRequirements { + list-style: disc; + padding-left: 24px; +} diff --git a/src/apps/onboarding/src/components/progress-bar/index.tsx b/src/apps/onboarding/src/components/progress-bar/index.tsx index ed32ae048..ed6c5e90f 100644 --- a/src/apps/onboarding/src/components/progress-bar/index.tsx +++ b/src/apps/onboarding/src/components/progress-bar/index.tsx @@ -1,30 +1,29 @@ /* eslint-disable ordered-imports/ordered-imports */ /* eslint-disable react/jsx-no-bind */ -import React, { FC } from 'react' +import React, { FC, useMemo } from 'react' +import classNames from 'classnames' import styles from './styles.module.scss' interface ProgressBarProps { progress: number - label?: string + maxStep: number className?: string } export const ProgressBar: FC = (props: ProgressBarProps) => { - - const progressProps: React.CSSProperties & { '--progress': number } = { - '--progress': props.progress, - } - + const progresses = useMemo(() => Array.from(Array(props.maxStep), (_, index) => index), [props.maxStep]) return ( -
- {props.label} -
+
+ {progresses.map(item => (
-
+ ))} +
) } diff --git a/src/apps/onboarding/src/components/progress-bar/styles.module.scss b/src/apps/onboarding/src/components/progress-bar/styles.module.scss index 47a45a27a..b9b656614 100644 --- a/src/apps/onboarding/src/components/progress-bar/styles.module.scss +++ b/src/apps/onboarding/src/components/progress-bar/styles.module.scss @@ -1,27 +1,15 @@ @import '../../../../../libs/ui/lib/styles/includes'; -.wrap { - background: $black-10; - border-radius: $sp-1; - height: 4px; - width: 100%; - display: flex; +.container { + gap: 8px; +} - :global(.progress) { - background: gray; - border-radius: inherit; - width: calc(var(--progress, 0) * 100%); - position: relative; +.blockProgress { + height: 4px; + background-color: $black-10; + flex: 1; - .percentage { - position: absolute; - top: 1px; - font-family: $font-barlow; - font-style: normal; - font-weight: $font-weight-bold; - font-size: 11px; - line-height: 14px; - color: $black-100; - } + &.active { + background-color: $turq-100; } } \ No newline at end of file diff --git a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts new file mode 100644 index 000000000..77fbe0320 --- /dev/null +++ b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts @@ -0,0 +1,81 @@ +/* eslint-disable ordered-imports/ordered-imports */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable unicorn/no-null */ +/* eslint-disable sort-keys */ +import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react' +import _ from 'lodash' + +import PersonalizationInfo from '../models/PersonalizationInfo' + +export interface useAutoSavePersonalizationType { + personalizationInfo: PersonalizationInfo | null + loading: boolean + setPersonalizationInfo: Dispatch> +} + +type useAutoSavePersonalizationFunctionType = ( + reduxPersonalization: PersonalizationInfo | null, + updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, + createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, + shouldSavingData: MutableRefObject, +) => useAutoSavePersonalizationType + +export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType = ( + reduxPersonalization: PersonalizationInfo | null, + updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, + createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, + shouldSavingData: MutableRefObject, +) => { + const [loading, setLoading] = useState(false) + const [personalizationInfo, setPersonalizationInfo] = useState(null) + + const saveData: any = async () => { + if (!personalizationInfo) { + return + } + + setLoading(true) + if (!reduxPersonalization) { + await createMemberPersonalizations([personalizationInfo]) + } else { + await updateMemberPersonalizations([personalizationInfo]) + } + + setLoading(false) + } + + useEffect(() => { + if (!!personalizationInfo && !_.isEqual(reduxPersonalization, personalizationInfo)) { + if (loading) { + shouldSavingData.current = true + return + } + + saveData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [personalizationInfo]) + + useEffect(() => { + if (!personalizationInfo && reduxPersonalization) { + setPersonalizationInfo(reduxPersonalization) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [reduxPersonalization]) + + useEffect(() => { + if (!loading && shouldSavingData.current) { + shouldSavingData.current = false + saveData() + .then(_.noop) + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [loading]) + + return { + personalizationInfo, + setPersonalizationInfo, + loading, + } +} diff --git a/src/apps/onboarding/src/models/EducationInfo.ts b/src/apps/onboarding/src/models/EducationInfo.ts index c43ccecf0..5f4b74b6a 100644 --- a/src/apps/onboarding/src/models/EducationInfo.ts +++ b/src/apps/onboarding/src/models/EducationInfo.ts @@ -5,7 +5,6 @@ export default interface EducationInfo { dateDescription: string startDate?: Date endDate?: Date - graduated?: boolean id: number } @@ -15,6 +14,5 @@ export const emptyEducationInfo: () => EducationInfo = () => ({ dateDescription: '', startDate: undefined, endDate: undefined, - graduated: false, id: 0, }) diff --git a/src/apps/onboarding/src/models/PersonalizationInfo.ts b/src/apps/onboarding/src/models/PersonalizationInfo.ts index 6f68bbfba..c6eeff553 100644 --- a/src/apps/onboarding/src/models/PersonalizationInfo.ts +++ b/src/apps/onboarding/src/models/PersonalizationInfo.ts @@ -1,12 +1,14 @@ /* eslint-disable sort-keys */ export default interface PersonalizationInfo { - referAs: string - profileSelfTitle: string - shortBio: string + referAs?: string + profileSelfTitle?: string + shortBio?: string + availableForGigs?: boolean } export const emptyPersonalizationInfo: () => PersonalizationInfo = () => ({ referAs: '', profileSelfTitle: '', shortBio: '', + availableForGigs: true, }) diff --git a/src/apps/onboarding/src/models/WorkInfo.ts b/src/apps/onboarding/src/models/WorkInfo.ts index 6342a9681..709c34cfe 100644 --- a/src/apps/onboarding/src/models/WorkInfo.ts +++ b/src/apps/onboarding/src/models/WorkInfo.ts @@ -6,7 +6,6 @@ export default interface WorkInfo { city?: string startDate?: Date dateDescription?: string - description?: string endDate?: Date currentlyWorking?: boolean id: number @@ -20,7 +19,6 @@ export const emptyWorkInfo: () => WorkInfo = () => ({ startDate: undefined, dateDescription: '', endDate: undefined, - description: '', currentlyWorking: false, id: 0, }) diff --git a/src/apps/onboarding/src/onboarding.routes.tsx b/src/apps/onboarding/src/onboarding.routes.tsx index 5559c8516..742a4b2bf 100644 --- a/src/apps/onboarding/src/onboarding.routes.tsx +++ b/src/apps/onboarding/src/onboarding.routes.tsx @@ -6,6 +6,7 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/c const PageOnboarding: LazyLoadedComponent = lazyLoad(() => import('./pages/onboarding/index'), 'OnboardingWrapper') const PageStart: LazyLoadedComponent = lazyLoad(() => import('./pages/start/index'), 'PageStart') const PageSkills: LazyLoadedComponent = lazyLoad(() => import('./pages/skills/index'), 'PageSkills') +const PageOpenToWork: LazyLoadedComponent = lazyLoad(() => import('./pages/open-to-work/index'), 'PageOpenToWork') const PageWorks: LazyLoadedComponent = lazyLoad(() => import('./pages/works/index'), 'PageWorks') const PageEducations: LazyLoadedComponent = lazyLoad(() => import('./pages/educations/index'), 'PageEducations') const PagePersonalization: LazyLoadedComponent = lazyLoad( @@ -26,7 +27,7 @@ export const onboardingRoutes: ReadonlyArray = [ authRequired: true, children: [ { - element: , + element: , route: '/', }, { @@ -37,6 +38,10 @@ export const onboardingRoutes: ReadonlyArray = [ element: , route: '/skills', }, + { + element: , + route: '/open-to-work', + }, { element: , route: '/works', diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx index bfdb25573..f98e87d9d 100644 --- a/src/apps/onboarding/src/pages/account-details/index.tsx +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -25,6 +25,7 @@ import { } from '../../redux/actions/member' import MemberAddress, { emptyMemberAddress } from '../../models/MemberAddress' import ConnectInfo, { emptyConnectInfo } from '../../models/ConnectInfo' +import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' const blankMemberAddress: MemberAddress = emptyMemberAddress() const blankConnectInfo: ConnectInfo = emptyConnectInfo() @@ -320,15 +321,16 @@ const PageAccountDetailsContent: FC<{
+ /> - -
- {education.major} - {education.dateDescription ? ({education.dateDescription}) : null} - -
- ))} -
- @@ -118,19 +102,18 @@ export const PageEducationsContent: FC<{
+ /> +
+
+ ) +} + +const mapStateToProps: any = (state: any) => { + const { + loadingMemberTraits, + personalization, + }: any = state.member + + return { + loadingMemberTraits, + reduxPersonalization: personalization, + } +} + +const mapDispatchToProps: any = { + createMemberPersonalizations, + updateMemberPersonalizations, +} + +export const PageOpenToWork: any = connect(mapStateToProps, mapDispatchToProps)(PageOpenToWorkContent) + +export default PageOpenToWork diff --git a/src/apps/onboarding/src/pages/open-to-work/styles.module.scss b/src/apps/onboarding/src/pages/open-to-work/styles.module.scss new file mode 100644 index 000000000..c30de304f --- /dev/null +++ b/src/apps/onboarding/src/pages/open-to-work/styles.module.scss @@ -0,0 +1,17 @@ +.container { + min-height: 100%; +} + +.blockContent { + margin-bottom: 40px; +} + +.ProgressBar { + margin-top: auto; + margin-bottom: 32px; + width: 100%; +} + +.blockSkilTags { + gap: 10px; +} \ No newline at end of file diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index 80724e43c..4611178f0 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -4,12 +4,10 @@ /* eslint-disable sort-keys */ import { useNavigate } from 'react-router-dom' import { connect } from 'react-redux' -import { FC, MutableRefObject, useEffect, useRef, useState } from 'react' -import _ from 'lodash' +import { FC, MutableRefObject, useEffect, useRef } from 'react' import classNames from 'classnames' import { Button, PageDivider } from '~/libs/ui' -import { RadioButton } from '~/apps/self-service/src/components/radio-button' import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' @@ -21,22 +19,11 @@ import { createMemberPersonalizations, setMemberPhotoUrl, updateMemberPersonalizations, + updateMemberPhotoUrl, } from '../../redux/actions/member' import MemberInfo from '../../models/MemberInfo' -import SelectOption from '../../models/SelectOption' - -const RadioButtonTypescript: any = RadioButton - -const referAsOptions: SelectOption[] = [ - { - label: 'Only show handle instead of name', - key: 'handle', - }, - { - label: 'Show first name, last name and handle', - key: 'name', - }, -] +import { useAutoSavePersonalization, useAutoSavePersonalizationType } from '../../hooks/useAutoSavePersonalization' +import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' const blankPersonalizationInfo: PersonalizationInfo = emptyPersonalizationInfo() @@ -46,55 +33,27 @@ const PagePersonalizationContent: FC<{ updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void createMemberPersonalizations: (infos: PersonalizationInfo[]) => void setMemberPhotoUrl: (photoUrl: string) => void + updateMemberPhotoUrl: (photoUrl: string) => void loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() - const [loading, setLoading] = useState(false) - const [personalizationInfo, setPersonalizationInfo] = useState(null) + const shouldSavingData: MutableRefObject = useRef(false) const shouldNavigateTo: MutableRefObject = useRef('') - useEffect(() => { - if (!personalizationInfo && props.reduxPersonalization) { - setPersonalizationInfo(props.reduxPersonalization) - } - /* eslint-disable react-hooks/exhaustive-deps */ - }, [props.reduxPersonalization]) - - const saveData: any = async () => { - if (!personalizationInfo) { - return - } - - setLoading(true) - if (!props.reduxPersonalization) { - await props.createMemberPersonalizations([personalizationInfo]) - } else { - await props.updateMemberPersonalizations([personalizationInfo]) - } - - setLoading(false) - } - - useEffect(() => { - if (!!personalizationInfo && !_.isEqual(props.reduxPersonalization, personalizationInfo)) { - if (loading) { - shouldSavingData.current = true - return - } - - saveData() - .then(_.noop) - } - /* eslint-disable react-hooks/exhaustive-deps */ - }, [personalizationInfo]) + const { + loading, + personalizationInfo, + setPersonalizationInfo, + }: useAutoSavePersonalizationType = useAutoSavePersonalization( + props.reduxPersonalization, + props.updateMemberPersonalizations, + props.createMemberPersonalizations, + shouldSavingData, + ) useEffect(() => { - if (!loading && shouldSavingData.current) { - shouldSavingData.current = false - saveData() - .then(_.noop) - } else if (!loading && !!shouldNavigateTo.current) { + if (!loading && !shouldSavingData.current && !!shouldNavigateTo.current) { navigate(shouldNavigateTo.current) } /* eslint-disable react-hooks/exhaustive-deps */ @@ -102,63 +61,20 @@ const PagePersonalizationContent: FC<{ return (
-

Show us your personality!

+

Make it personal

-
- - When members personalize theirs profiles with a photo and description, - they are more likely to get noticed by our customers for work and opportunities. - - -
- - -
-

Would you like to add a "handle" to use in community communications?

- - Some of our members prefer to be known within our community with a "handle" - or display name that is not their official name. - for example: DannyCoder or ZiggyZ123. You will have an opportunity to - set preference for how this is used in your profile. - - - (( - option.key !== personalizationInfo?.referAs) ? option : ({ - ...option, - value: true, - })))} - onChange={(newValues: any) => { - const matchValue: SelectOption = _.find( - newValues, - { value: true }, - ) as SelectOption - if (matchValue) { - const matchOption: SelectOption = _.find( - referAsOptions, - { label: matchValue.label }, - ) as SelectOption - if (matchOption) { - setPersonalizationInfo({ - ...(personalizationInfo || blankPersonalizationInfo), - referAs: matchOption.key, - }) - } - } - }} - disabled={props.loadingMemberTraits} - /> -
-
+
+ -

Give yourself a professional title

- An industry standard title will help employers know your general area of expertise. +

Bio

+ This is where we can really get to know you. { setPersonalizationInfo({ @@ -166,20 +82,16 @@ const PagePersonalizationContent: FC<{ profileSelfTitle: value || '', }) }} - placeholder='Title' + placeholder='Ex: I’m a creative rockstar' tabIndex={0} type='text' disabled={props.loadingMemberTraits} + className='mt-16' /> -

Write a short biography

- - Describe yourself in your own words. A short bio will give - potential customers a sense of who you are. - { setPersonalizationInfo({ @@ -187,7 +99,7 @@ const PagePersonalizationContent: FC<{ shortBio: value || '', }) }} - placeholder='Describe yourself and your work' + placeholder='Share something that makes you, you.' tabIndex={0} disabled={props.loadingMemberTraits} /> @@ -195,15 +107,16 @@ const PagePersonalizationContent: FC<{
+ /> - @@ -60,4 +61,16 @@ export const PageSkills: FC<{}> = () => { ) } +const mapStateToProps: any = (state: any) => { + const { + memberInfo, + }: any = state.member + + return { + reduxMemberInfo: memberInfo, + } +} + +export const PageSkills: any = connect(mapStateToProps, undefined)(PageSkillsContent) + export default PageSkills diff --git a/src/apps/onboarding/src/pages/skills/styles.module.scss b/src/apps/onboarding/src/pages/skills/styles.module.scss index 8efee791a..c30de304f 100644 --- a/src/apps/onboarding/src/pages/skills/styles.module.scss +++ b/src/apps/onboarding/src/pages/skills/styles.module.scss @@ -3,15 +3,13 @@ } .blockContent { - margin-bottom: 20px; + margin-bottom: 40px; } .ProgressBar { margin-top: auto; margin-bottom: 32px; -} -.blockLeft { - max-width: 530px; + width: 100%; } .blockSkilTags { diff --git a/src/apps/onboarding/src/pages/start/index.tsx b/src/apps/onboarding/src/pages/start/index.tsx index bb177135a..1c6043e02 100644 --- a/src/apps/onboarding/src/pages/start/index.tsx +++ b/src/apps/onboarding/src/pages/start/index.tsx @@ -55,8 +55,8 @@ export const PageStart: FC<{}> = () => {
diff --git a/src/apps/onboarding/src/pages/start/styles.module.scss b/src/apps/onboarding/src/pages/start/styles.module.scss index 3e8c24712..d9353b612 100644 --- a/src/apps/onboarding/src/pages/start/styles.module.scss +++ b/src/apps/onboarding/src/pages/start/styles.module.scss @@ -3,7 +3,7 @@ } .blockContent { - margin-bottom: 20px; + margin-bottom: 40px; } .ProgressBar { diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index e42f9aa74..2cb5cb950 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -13,9 +13,9 @@ import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' import WorkInfo from '../../models/WorkInfo' import ModalAddWork from '../../components/modal-add-work' -import IconEdit from '../../assets/images/edit.svg' -import IconTrash from '../../assets/images/trash.svg' +import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' import { createMemberWorks, updateMemberWorks } from '../../redux/actions/member' +import CardItem from '../../components/card-item' export const PageWorksContent: FC<{ reduxWorks: WorkInfo[] | null @@ -55,93 +55,68 @@ export const PageWorksContent: FC<{ saveData() .then(_.noop) } - /* eslint-disable react-hooks/exhaustive-deps */ + /* eslint-disable react-hooks/exhaustive-deps */ }, [works]) return (
-

Add your work experience here

+

Show us what you have done!

-
-
- {(!works || works.length === 0) ? ( - - You will be able to add details for each of the work experiences - that you think will demonstrate your abilities. - +
+
+

Add your experience

+ + + Add details for career experiences that demonstrate your abilities. + + + {(works || []).length > 0 ? ( +
+ {(works || []).map(work => ( + { + setEditingWork(work) + setShowAddWorkModal(true) + }} + onDelete={() => setWorks(_.filter(works, w => w.id !== work.id))} + /> + ))} +
) : null} -
- {(works || []).map(work => ( -
-
- {work.company} - - -
- {work.city ? ({work.city}) : null} - {work.position} - {work.dateDescription ? ({work.dateDescription}) : null} - {work.description ? ( - - {work.description} - - ) : null} - -
- ))} -
-
+ onClick={() => navigate('../open-to-work')} + />
- { - setWorkInfo({ - ...workInfo, - currentlyWorking: e.target.checked, - }) - }} - /> +
+ { + setWorkInfo({ + ...workInfo, + currentlyWorking: e.target.checked, + }) + }} + /> +
) From e47ec608164689dec21ff14a019e32523f34c4cd Mon Sep 17 00:00:00 2001 From: dat Date: Mon, 10 Jul 2023 01:55:59 +0700 Subject: [PATCH 20/25] onboarding fix wrong text --- src/apps/onboarding/src/pages/educations/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 3124d0cf7..98eaf54e4 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -60,7 +60,7 @@ export const PageEducationsContent: FC<{ return (
-

Add your education information

+

Education

From 954d22e89f69e89482117d7d58125a3e1a0700e1 Mon Sep 17 00:00:00 2001 From: dat Date: Mon, 10 Jul 2023 02:00:07 +0700 Subject: [PATCH 21/25] onboarding: fix final step link --- .../onboarding/src/pages/personalization/index.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index 4611178f0..9663a016a 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -8,6 +8,7 @@ import { FC, MutableRefObject, useEffect, useRef } from 'react' import classNames from 'classnames' import { Button, PageDivider } from '~/libs/ui' +import { EnvironmentConfig } from '~/config' import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' @@ -54,7 +55,11 @@ const PagePersonalizationContent: FC<{ useEffect(() => { if (!loading && !shouldSavingData.current && !!shouldNavigateTo.current) { - navigate(shouldNavigateTo.current) + if (shouldNavigateTo.current.startsWith('../')) { + navigate(shouldNavigateTo.current) + } else { + window.location.href = shouldNavigateTo.current + } } /* eslint-disable react-hooks/exhaustive-deps */ }, [loading]) @@ -131,9 +136,11 @@ const PagePersonalizationContent: FC<{ iconToLeft onClick={() => { if (loading) { - shouldNavigateTo.current = '../account-details' + shouldNavigateTo.current + = `${EnvironmentConfig.USER_PROFILE_URL}/${props.memberInfo?.handle}` } else { - navigate('../account-details') + window.location.href + = `${EnvironmentConfig.USER_PROFILE_URL}/${props.memberInfo?.handle}` } }} > From 4fb167eb4abd019ea9570d95b64543848a789c25 Mon Sep 17 00:00:00 2001 From: dat Date: Mon, 10 Jul 2023 11:25:01 +0700 Subject: [PATCH 22/25] onboarding new design minor fix --- .../src/components/DateInput/index.tsx | 1 + .../src/pages/open-to-work/index.tsx | 42 ++++++++++++------- .../onboarding/src/redux/actions/member.ts | 24 +++++++++++ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/apps/onboarding/src/components/DateInput/index.tsx b/src/apps/onboarding/src/components/DateInput/index.tsx index 9257029a1..e92f484d1 100644 --- a/src/apps/onboarding/src/components/DateInput/index.tsx +++ b/src/apps/onboarding/src/components/DateInput/index.tsx @@ -70,6 +70,7 @@ const DateInput: FC = (props: DateInputProps) => { }} onFocus={props.onFocus} showYearDropdown + dropdownMode='select' onCalendarOpen={() => setOpen(true)} maxDate={ props.allowFutureDate ? null : moment() diff --git a/src/apps/onboarding/src/pages/open-to-work/index.tsx b/src/apps/onboarding/src/pages/open-to-work/index.tsx index b37d2325b..20f3cbff3 100644 --- a/src/apps/onboarding/src/pages/open-to-work/index.tsx +++ b/src/apps/onboarding/src/pages/open-to-work/index.tsx @@ -2,7 +2,7 @@ /* eslint-disable react/jsx-no-bind */ /* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' -import { FC, MutableRefObject, useEffect, useRef } from 'react' +import { FC, MutableRefObject, useEffect, useMemo, useRef } from 'react' import classNames from 'classnames' import { connect } from 'react-redux' @@ -43,6 +43,14 @@ export const PageOpenToWorkContent: FC<{ shouldSavingData, ) + const availableForGigsValue: boolean | undefined = useMemo(() => { + if (!personalizationInfo || personalizationInfo.availableForGigs === undefined) { + return blankPersonalizationInfo.availableForGigs + } + + return personalizationInfo.availableForGigs + }, [personalizationInfo]) + useEffect(() => { if (!loading && !shouldSavingData.current && !!shouldNavigateTo.current) { navigate(shouldNavigateTo.current) @@ -50,6 +58,18 @@ export const PageOpenToWorkContent: FC<{ /* eslint-disable react-hooks/exhaustive-deps */ }, [loading]) + function checkToNavigateNextPage(pageUrl: string): void { + if (!personalizationInfo || personalizationInfo.availableForGigs === undefined) { + shouldNavigateTo.current = pageUrl + setPersonalizationInfo({ + ...(personalizationInfo || blankPersonalizationInfo), + availableForGigs: blankPersonalizationInfo.availableForGigs, + }) + } else { + navigate(pageUrl) + } + } + return (

@@ -68,15 +88,15 @@ export const PageOpenToWorkContent: FC<{
{ setPersonalizationInfo({ - ...personalizationInfo, + ...(personalizationInfo || blankPersonalizationInfo), availableForGigs: e.target.checked, }) }} - disabled={props.loadingMemberTraits} + disabled={props.loadingMemberTraits || loading} />

@@ -93,25 +113,19 @@ export const PageOpenToWorkContent: FC<{ size='lg' secondary iconToLeft + disabled={loading} icon={IconBackGreen} onClick={() => { - if (loading) { - shouldNavigateTo.current = '../skills' - } else { - navigate('../skills') - } + checkToNavigateNextPage('../skills') }} /> + * + * ``` + * @returns + */ + +export const useMemberSkillEditor = (): MemberSkillEditor => { + const { profile }: ProfileContextData = useContext(profileContext) + const [isEmsiInitialized, setIsEmsiInitialized] = useState(false) + const [skills, setSkills] = useState([]) + const [loading, setLoading] = useState(true) + const [, setError] = useState() + + // Function that saves the updated emsi skills, will be called from outside + const saveSkills = useCallback(async () => { + if (!profile?.userId) { + return + } + + const emsiSkills = skills.map(s => ({ emsiId: s.skillId, name: s.name, sources: s.skillSources })) + if (!isEmsiInitialized) { + await createMemberEmsiSkills(profile.userId, emsiSkills) + setIsEmsiInitialized(true) + return + } + + updateMemberEmsiSkills(profile.userId, emsiSkills) + }, [isEmsiInitialized, profile?.userId, skills]) + + // Handle user changes + + const handleRemoveSkill = useCallback((skillId: string): void => { + const skill = skills.find(s => s.skillId === skillId) + if (!skill) { + return + } + + if (skill.skillSources.includes(EmsiSkillSources.challengeWin)) { + return + } + + setSkills(skills.filter(s => s.skillId !== skillId)) + }, [skills]) + + const handleAddSkill = useCallback((skillData: any): void => { + if (skills.find(s => s.skillId === skillData.value)) { + return + } + + setSkills([...skills, { + name: skillData.label, + skillId: skillData.value, + skillSources: [EmsiSkillSources.selfPicked], + }]) + }, [skills]) + + const handleOnChange = useCallback(({ target: { value } }: any): void => { + const removed = differenceWith(skills, value, (s, v: any) => s.skillId === v.value) + if (removed.length) { + removed.map(s => handleRemoveSkill(s.skillId)) + } + + const added = differenceWith(value, skills, (v: any, s: any) => v.value === s.skillId) + if (added.length) { + added.forEach(handleAddSkill) + } + }, [handleAddSkill, handleRemoveSkill, skills]) + + // Load member's emsi skills, set loading state & isEmsiInitialized + useEffect(() => { + if (!profile?.userId) { + return undefined + } + + let mounted = true + fetchMemberSkills(profile.userId) + .catch(e => { + setError(e?.message ?? e) + return [] + }) + .then(emsiSkills => { + if (!mounted) { + return + } + + setIsEmsiInitialized(emsiSkills?.length > 0) + setSkills(emsiSkills) + setLoading(false) + }) + + return () => { mounted = false } + }, [profile?.userId]) + + // build the form input + const formInput = useMemo(() => ( + + ), [skills, handleOnChange, loading]) + + return { + formInput, + saveSkills, + } +} diff --git a/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts b/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts index 0adc93c5d..835fcd9a3 100644 --- a/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts +++ b/src/libs/shared/lib/services/emsi-skills/emsi-skills.service.ts @@ -1,8 +1,25 @@ import { EnvironmentConfig } from '~/config' -import { xhrGetAsync } from '~/libs/core' +import { xhrGetAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' -import Skill from './skill.model' +import { EmsiSkill, Skill } from './skill.model' export async function autoCompleteSkills(queryTerm: string): Promise { return xhrGetAsync(`${EnvironmentConfig.API.V5}/emsi-skills/skills/auto-complete?term=${queryTerm}`) } + +export async function fetchMemberSkills(userId?: string | number): Promise { + return xhrGetAsync(`${EnvironmentConfig.API.V5}/emsi-skills/member-emsi-skills/${userId}`) +} + +export async function createMemberEmsiSkills(userId: number, skills: Skill[]): Promise { + return xhrPostAsync(`${EnvironmentConfig.API.V5}/emsi-skills/member-emsi-skills`, { + emsiSkills: skills, + userId, + }) +} + +export async function updateMemberEmsiSkills(userId: string | number, skills: Skill[]): Promise { + return xhrPutAsync(`${EnvironmentConfig.API.V5}/emsi-skills/member-emsi-skills/${userId}`, { + emsiSkills: skills, + }) +} diff --git a/src/libs/shared/lib/services/emsi-skills/skill.model.ts b/src/libs/shared/lib/services/emsi-skills/skill.model.ts index e9c21d4fc..abde46c47 100644 --- a/src/libs/shared/lib/services/emsi-skills/skill.model.ts +++ b/src/libs/shared/lib/services/emsi-skills/skill.model.ts @@ -1,4 +1,16 @@ -export default interface Skill { +export enum EmsiSkillSources { + selfPicked = 'SelfPicked', + challengeWin = 'ChallengeWin', +} + +export interface Skill { name: string; emsiId: string; + sources?: EmsiSkillSources[]; +} + +export interface EmsiSkill { + name: string; + skillId: string; + skillSources: EmsiSkillSources[] } diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx index 33bd0405b..c28e60dda 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx @@ -15,10 +15,12 @@ import styles from './InputMultiselect.module.scss' export interface InputMultiselectOption { label?: ReactNode value: string + verified?: boolean } interface InputMultiselectProps { readonly dirty?: boolean + readonly loading?: boolean readonly disabled?: boolean readonly error?: string readonly hideInlineErrors?: boolean @@ -29,13 +31,19 @@ interface InputMultiselectProps { readonly options?: ReadonlyArray readonly placeholder?: string readonly tabIndex?: number - readonly value?: string + readonly value?: InputMultiselectOption[] readonly onFetchOptions?: (query: string) => Promise } const MultiValueRemove: FC = (props: any) => ( - + {props.data.verified ? ( + + + + ) : ( + + )} ) @@ -70,10 +78,9 @@ const InputMultiselect: FC = (props: InputMultiselectProp onChange={handleOnChange} onBlur={noop} blurInputOnSelect={false} - components={{ - // MultiValueLabel: () => - MultiValueRemove, - }} + isLoading={props.loading} + components={{ MultiValueRemove }} + value={props.value} /> ) From 24238beeacfe262f6cd2d31a9d35cf72dd236098 Mon Sep 17 00:00:00 2001 From: dat Date: Tue, 11 Jul 2023 00:40:33 +0700 Subject: [PATCH 24/25] Test new skill selector integration in onboarding --- .../onboarding/src/pages/skills/index.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 428298105..cfda82bc2 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -2,13 +2,13 @@ /* eslint-disable react/jsx-no-bind */ /* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' -import { FC } from 'react' +import { FC, useState } from 'react' import classNames from 'classnames' import { connect } from 'react-redux' import { Button, PageDivider } from '~/libs/ui' -import { InputSkillSelector } from '~/libs/shared/lib/components/input-skill-selector' import { Member } from '~/apps/talent-search/src/lib/models' +import { MemberSkillEditor, useMemberSkillEditor } from '~/libs/shared' import { ProgressBar } from '../../components/progress-bar' @@ -18,6 +18,19 @@ export const PageSkillsContent: FC<{ reduxMemberInfo: Member | null }> = props => { const navigate: any = useNavigate() + const [loading, setLoading] = useState(false) + const { formInput: emsiFormInput, saveSkills: saveEmsiSkills }: MemberSkillEditor = useMemberSkillEditor() + + const saveSkills = async (): Promise => { + setLoading(true) + try { + await saveEmsiSkills() + } catch (error) { + } + + setLoading(false) + navigate('../open-to-work') + } return (
@@ -36,7 +49,7 @@ export const PageSkillsContent: FC<{ Understanding your skills will allow us to connect you to the right opportunities.
- + {emsiFormInput}
@@ -52,7 +65,8 @@ export const PageSkillsContent: FC<{ size='lg' primary iconToLeft - onClick={() => navigate('../open-to-work')} + onClick={saveSkills} + disabled={loading} > next From a48a8cd8b1dc34fb8596f58cac1d629a925e14e6 Mon Sep 17 00:00:00 2001 From: dat Date: Tue, 11 Jul 2023 02:34:54 +0700 Subject: [PATCH 25/25] Onboarding code cleanup --- .../src/assets/images/back-green.svg | 3 - .../src/assets/images/cheveron-down.svg | 3 - .../onboarding/src/assets/images/edit.svg | 3 - .../onboarding/src/assets/images/trash.svg | 3 - .../onboarding/src/assets/images/x-icon.svg | 1 - .../src/components/DateInput/index.tsx | 36 +++++----- .../src/components/FieldAvatar/index.tsx | 52 ++++++-------- .../src/components/FormField/index.tsx | 22 +++--- .../components/InputTextAutoSave/index.tsx | 13 ++-- .../InputTextareaAutoSave/index.tsx | 13 ++-- .../src/components/card-item/index.tsx | 10 ++- .../components/card-item/styles.module.scss | 1 + .../components/modal-add-education/index.tsx | 30 ++++---- .../src/components/modal-add-work/index.tsx | 54 +++++++------- .../components/modal-upload-photo/index.tsx | 29 ++++---- .../src/components/progress-bar/index.tsx | 4 +- src/apps/onboarding/src/config/index.ts | 11 ++- .../src/hooks/useAutoSavePersonalization.ts | 16 ++--- src/apps/onboarding/src/models/ConnectInfo.ts | 1 - .../onboarding/src/models/EducationInfo.ts | 5 +- .../onboarding/src/models/MemberAddress.ts | 5 +- src/apps/onboarding/src/models/MemberInfo.ts | 4 +- .../src/models/PersonalizationInfo.ts | 5 +- .../onboarding/src/models/SelectOption.ts | 1 - src/apps/onboarding/src/models/WorkInfo.ts | 11 ++- .../src/pages/account-details/index.tsx | 51 ++++++------- .../onboarding/src/pages/educations/index.tsx | 55 +++++++------- .../onboarding/src/pages/onboarding/index.tsx | 6 +- .../src/pages/open-to-work/index.tsx | 24 +++---- .../src/pages/personalization/index.tsx | 36 +++++----- .../onboarding/src/pages/skills/index.tsx | 9 +-- src/apps/onboarding/src/pages/start/index.tsx | 15 ++-- src/apps/onboarding/src/pages/works/index.tsx | 55 +++++++------- .../onboarding/src/redux/actions/member.ts | 72 +++++++++---------- .../onboarding/src/redux/reducers/member.ts | 18 +++-- src/apps/onboarding/src/redux/store.ts | 6 +- src/apps/onboarding/src/services/members.ts | 2 +- 37 files changed, 321 insertions(+), 364 deletions(-) delete mode 100644 src/apps/onboarding/src/assets/images/back-green.svg delete mode 100644 src/apps/onboarding/src/assets/images/cheveron-down.svg delete mode 100644 src/apps/onboarding/src/assets/images/edit.svg delete mode 100644 src/apps/onboarding/src/assets/images/trash.svg delete mode 100644 src/apps/onboarding/src/assets/images/x-icon.svg diff --git a/src/apps/onboarding/src/assets/images/back-green.svg b/src/apps/onboarding/src/assets/images/back-green.svg deleted file mode 100644 index c6d8e928e..000000000 --- a/src/apps/onboarding/src/assets/images/back-green.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/cheveron-down.svg b/src/apps/onboarding/src/assets/images/cheveron-down.svg deleted file mode 100644 index f6af4858a..000000000 --- a/src/apps/onboarding/src/assets/images/cheveron-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/edit.svg b/src/apps/onboarding/src/assets/images/edit.svg deleted file mode 100644 index e8aa0411d..000000000 --- a/src/apps/onboarding/src/assets/images/edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/trash.svg b/src/apps/onboarding/src/assets/images/trash.svg deleted file mode 100644 index 9687957df..000000000 --- a/src/apps/onboarding/src/assets/images/trash.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/apps/onboarding/src/assets/images/x-icon.svg b/src/apps/onboarding/src/assets/images/x-icon.svg deleted file mode 100644 index 6d7f9b501..000000000 --- a/src/apps/onboarding/src/assets/images/x-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/apps/onboarding/src/components/DateInput/index.tsx b/src/apps/onboarding/src/components/DateInput/index.tsx index e92f484d1..df6a40251 100644 --- a/src/apps/onboarding/src/components/DateInput/index.tsx +++ b/src/apps/onboarding/src/components/DateInput/index.tsx @@ -1,32 +1,28 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ -/* eslint-disable max-len */ -/* eslint-disable react/destructuring-assignment */ /** * DateInput * * Date Input control. */ -import React, { FC, createRef, useState } from 'react' -import DatePicker from 'react-datepicker' import { Portal } from 'react-overlays' +import { createRef, FC, useState } from 'react' +import DatePicker from 'react-datepicker' import cn from 'classnames' import moment from 'moment' - import 'react-datepicker/dist/react-datepicker.css' +import { IconOutline } from '~/libs/ui' + import { ReactComponent as CalendarIcon } from '../../assets/images/calendar.svg' -import { ReactComponent as ArrowIcon } from '../../assets/images/cheveron-down.svg' + import styles from './styles.module.scss' interface CalendarContainerProps { children?: any } -const CalendarContainer: FC = ({ children }: CalendarContainerProps) => { +const CalendarContainer: FC = (props: CalendarContainerProps) => { const el: any = document.getElementById('calendar-portal') - return {children} + return {props.children} } interface DateInputProps { @@ -53,7 +49,9 @@ const DateInput: FC = (props: DateInputProps) => { )} >
calendarRef.current.setOpen(true)} + onClick={function openCalendar() { + calendarRef.current.setOpen(true) + }} className={cn(styles.icon, styles['icon-calendar'])} > @@ -65,15 +63,17 @@ const DateInput: FC = (props: DateInputProps) => { selected={props.value} onChange={props.onChange as any} onBlur={props.onBlur} - onCalendarClose={() => { + onCalendarClose={function closeCalendar() { setOpen(false) }} onFocus={props.onFocus} showYearDropdown dropdownMode='select' - onCalendarOpen={() => setOpen(true)} + onCalendarOpen={function openCalendar() { + setOpen(true) + }} maxDate={ - props.allowFutureDate ? null : moment() + props.allowFutureDate ? undefined : moment() .subtract(1, 'days') .toDate() } @@ -86,9 +86,11 @@ const DateInput: FC = (props: DateInputProps) => { styles['icon-arrow'], open ? styles['icon-arrow-open'] : '', )} - onClick={() => calendarRef.current.setOpen(true)} + onClick={function openCalendar() { + calendarRef.current.setOpen(true) + }} > - +
) diff --git a/src/apps/onboarding/src/components/FieldAvatar/index.tsx b/src/apps/onboarding/src/components/FieldAvatar/index.tsx index 194645f44..8d9f86df7 100644 --- a/src/apps/onboarding/src/components/FieldAvatar/index.tsx +++ b/src/apps/onboarding/src/components/FieldAvatar/index.tsx @@ -1,7 +1,3 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable unicorn/no-null */ /** * FieldAvatar * @@ -9,15 +5,15 @@ */ import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' import classNames from 'classnames' -import _ from 'lodash' import { Button, IconOutline } from '~/libs/ui' -import styles from './styles.module.scss' -import MemberInfo from '../../models/MemberInfo' import AvatarPlaceholder from '../../assets/images/avatar-placeholder.png' +import MemberInfo from '../../models/MemberInfo' import ModalUploadPhoto from '../modal-upload-photo' +import styles from './styles.module.scss' + interface FieldAvatarProps { className?: string memberInfo?: MemberInfo, @@ -25,19 +21,14 @@ interface FieldAvatarProps { updateMemberPhotoUrl: (photoUrl: string) => void } -const FieldAvatar: FC = ({ - className, - memberInfo, - setMemberPhotoUrl, - updateMemberPhotoUrl, -}: FieldAvatarProps) => { +const FieldAvatar: FC = (props: FieldAvatarProps) => { const [imgUrl, setImgUrl] = useState('') useEffect(() => { - if (memberInfo) { - setImgUrl(memberInfo.photoURL) + if (props.memberInfo) { + setImgUrl(props.memberInfo.photoURL) } /* eslint-disable react-hooks/exhaustive-deps */ - }, [memberInfo]) + }, [props.memberInfo]) const [isPhotoEditMode, setIsPhotoEditMode]: [boolean, Dispatch>] = useState(false) @@ -51,16 +42,20 @@ const FieldAvatar: FC = ({ async function handleRemovePhoto(): Promise { setIsSaving(true) try { - await updateMemberPhotoUrl('') + await props.updateMemberPhotoUrl('') } catch (error) { } setIsSaving(false) } + function showEditPhoto(): void { + setIsPhotoEditMode(true) + } + return (

Photo

@@ -90,8 +85,8 @@ const FieldAvatar: FC = ({ secondary iconToLeft icon={IconOutline.UploadIcon} - disabled={!memberInfo || isSaving} - onClick={() => setIsPhotoEditMode(true)} + disabled={!props.memberInfo || isSaving} + onClick={showEditPhoto} className='mt-16' > change @@ -101,11 +96,8 @@ const FieldAvatar: FC = ({ secondary iconToLeft icon={IconOutline.TrashIcon} - disabled={!memberInfo || isSaving} - onClick={() => { - handleRemovePhoto() - .then(_.noop) - }} + disabled={!props.memberInfo || isSaving} + onClick={handleRemovePhoto} className='mt-16' > delete @@ -117,8 +109,8 @@ const FieldAvatar: FC = ({ secondary iconToLeft icon={IconOutline.UploadIcon} - disabled={!memberInfo} - onClick={() => setIsPhotoEditMode(true)} + disabled={!props.memberInfo} + onClick={showEditPhoto} className='mt-16' > add image @@ -128,11 +120,11 @@ const FieldAvatar: FC = ({
{ - isPhotoEditMode && memberInfo && ( + isPhotoEditMode && props.memberInfo && ( ) } diff --git a/src/apps/onboarding/src/components/FormField/index.tsx b/src/apps/onboarding/src/components/FormField/index.tsx index 41057793f..64ead7b7f 100644 --- a/src/apps/onboarding/src/components/FormField/index.tsx +++ b/src/apps/onboarding/src/components/FormField/index.tsx @@ -1,7 +1,3 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable unicorn/no-null */ /** * FormField * @@ -21,13 +17,15 @@ interface FormFieldProps { error?: string } -const FormField: FC = ({ - children, - className, - label, - ...props -}: FormFieldProps) => { - const handleClick: any = (e: any) => { +const FormField: FC = (componentProps: FormFieldProps) => { + const { + children, + className, + label, + ...props + }: FormFieldProps = componentProps + + function handleClick(e: any): void { // focus on input label click const inputElement: any = e.target.closest('.form-field') .querySelector('input') @@ -45,7 +43,7 @@ const FormField: FC = ({ {children}
- {props.error ? () : null} + {props.error ? () : undefined} {props.error}
diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx index 89c348eb1..bb98db09b 100644 --- a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx @@ -1,13 +1,10 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable unicorn/no-null */ /** * InputTextAutoSave * * A Form Field Is a wrapper for input to add the label to it */ -import React, { FC, useEffect, useState } from 'react' +import { FC, FocusEvent, useEffect, useState } from 'react' + import { InputText, InputValue } from '~/libs/ui' import { InputTextTypes } from '~/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText' @@ -40,10 +37,12 @@ const InputTextAutoSave: FC = (props: InputTextProps) => { { + onChange={function onChange(event: FocusEvent) { setValue(event.target.value) }} - onBlur={() => props.onChange(`${value}`)} + onBlur={function onBlur() { + props.onChange(`${value}`) + }} /> ) } diff --git a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx index 425abd003..6e162560b 100644 --- a/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextareaAutoSave/index.tsx @@ -1,13 +1,10 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable unicorn/no-null */ /** * InputTextareaAutoSave * * A Form Field Is a wrapper for input to add the label to it */ -import React, { FC, useEffect, useState } from 'react' +import { FC, FocusEvent, useEffect, useState } from 'react' + import { InputTextarea } from '~/libs/ui' export interface InputTextareaProps { @@ -35,10 +32,12 @@ const InputTextareaAutoSave: FC = (props: InputTextareaProps { + onChange={function onChange(event: FocusEvent) { setValue(event.target.value) }} - onBlur={() => props.onChange(`${value}`)} + onBlur={function onBlur() { + props.onChange(`${value}`) + }} /> ) } diff --git a/src/apps/onboarding/src/components/card-item/index.tsx b/src/apps/onboarding/src/components/card-item/index.tsx index 993d85b4c..757f67f18 100644 --- a/src/apps/onboarding/src/components/card-item/index.tsx +++ b/src/apps/onboarding/src/components/card-item/index.tsx @@ -1,11 +1,9 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ import React, { FC } from 'react' import classNames from 'classnames' +import { IconOutline } from '~/libs/ui' + import styles from './styles.module.scss' -import IconEdit from '../../assets/images/edit.svg' -import IconTrash from '../../assets/images/trash.svg' interface CardItemProps { title: string @@ -30,7 +28,7 @@ export const CardItem: FC = (props: CardItemProps) => ( disabled={props.disabled} className={styles.btn} > - +
diff --git a/src/apps/onboarding/src/components/card-item/styles.module.scss b/src/apps/onboarding/src/components/card-item/styles.module.scss index 77acdf163..ebc49a55e 100644 --- a/src/apps/onboarding/src/components/card-item/styles.module.scss +++ b/src/apps/onboarding/src/components/card-item/styles.module.scss @@ -34,6 +34,7 @@ .btn { flex-shrink: 0; + color: $turq-160; &:disabled { opacity: 0.2; diff --git a/src/apps/onboarding/src/components/modal-add-education/index.tsx b/src/apps/onboarding/src/components/modal-add-education/index.tsx index 2465ba4b9..5b7cc5bb2 100644 --- a/src/apps/onboarding/src/components/modal-add-education/index.tsx +++ b/src/apps/onboarding/src/components/modal-add-education/index.tsx @@ -1,17 +1,15 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable sort-keys */ -import { FC, useEffect, useState } from 'react' +import { FC, FocusEvent, useEffect, useState } from 'react' +import _ from 'lodash' import classNames from 'classnames' import moment from 'moment' -import _ from 'lodash' import { BaseModal, Button, InputText } from '~/libs/ui' -import styles from './styles.module.scss' +import DateInput from '../DateInput' import EducationInfo, { emptyEducationInfo } from '../../models/EducationInfo' import FormField from '../FormField' -import DateInput from '../DateInput' + +import styles from './styles.module.scss' interface ModalAddEducationProps { onClose?: () => void @@ -74,7 +72,7 @@ const ModalAddEducation: FC = (props: ModalAddEducationP primary size='lg' label='save' - onClick={() => { + onClick={function onClick() { if (validateField()) { const endDate: Date | undefined = educationInfo.endDate const endDateString: string = endDate ? moment(endDate) @@ -110,7 +108,7 @@ const ModalAddEducation: FC = (props: ModalAddEducationP name='collegeName' label='College or University *' value={educationInfo.collegeName} - onChange={event => { + onChange={function onChange(event: FocusEvent) { setEducationInfo({ ...educationInfo, collegeName: event.target.value, @@ -128,10 +126,12 @@ const ModalAddEducation: FC = (props: ModalAddEducationP name='major' label='Degree *' value={educationInfo.major} - onChange={event => setEducationInfo({ - ...educationInfo, - major: event.target.value, - })} + onChange={function onChange(event: FocusEvent) { + setEducationInfo({ + ...educationInfo, + major: event.target.value, + }) + }} placeholder='Enter degree' tabIndex={0} type='text' @@ -151,7 +151,7 @@ const ModalAddEducation: FC = (props: ModalAddEducationP > { + onChange={function onChange(v: Date | null) { setEducationInfo({ ...educationInfo, startDate: v || undefined, @@ -170,7 +170,7 @@ const ModalAddEducation: FC = (props: ModalAddEducationP > { + onChange={function onChange(v: Date | null) { setEducationInfo({ ...educationInfo, endDate: v || undefined, diff --git a/src/apps/onboarding/src/components/modal-add-work/index.tsx b/src/apps/onboarding/src/components/modal-add-work/index.tsx index ce89d2822..ba722e322 100644 --- a/src/apps/onboarding/src/components/modal-add-work/index.tsx +++ b/src/apps/onboarding/src/components/modal-add-work/index.tsx @@ -1,19 +1,17 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable sort-keys */ import { FC, useEffect, useState } from 'react' -import classNames from 'classnames' import _ from 'lodash' +import classNames from 'classnames' import moment from 'moment' import { BaseModal, Button, InputSelect, InputText } from '~/libs/ui' import { FormInputCheckbox } from '~/apps/self-service/src/components/form-elements' -import styles from './styles.module.scss' -import WorkInfo, { emptyWorkInfo } from '../../models/WorkInfo' import { INDUSTRIES_OPTIONS } from '../../config' -import FormField from '../FormField' import DateInput from '../DateInput' +import FormField from '../FormField' +import WorkInfo, { emptyWorkInfo } from '../../models/WorkInfo' + +import styles from './styles.module.scss' const FormInputCheckboxMiddleware: any = FormInputCheckbox as any @@ -26,8 +24,8 @@ interface ModalAddWorkProps { const industryOptions: any = _.sortBy(INDUSTRIES_OPTIONS) .map(v => ({ - value: v, label: v, + value: v, })) const ModalAddWork: FC = (props: ModalAddWorkProps) => { @@ -84,7 +82,7 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { primary size='lg' label='save' - onClick={() => { + onClick={function onClick() { if (validateField()) { const endDate: Date | undefined = workInfo.endDate let endDateString: string = endDate ? moment(endDate) @@ -123,7 +121,7 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { name='company' label='Company *' value={workInfo.company} - onChange={event => { + onChange={function onChange(event: any) { setWorkInfo({ ...workInfo, company: event.target.value, @@ -141,10 +139,12 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { name='position' label='Position *' value={workInfo.position} - onChange={event => setWorkInfo({ - ...workInfo, - position: event.target.value, - })} + onChange={function onChange(event: any) { + setWorkInfo({ + ...workInfo, + position: event.target.value, + }) + }} placeholder='Enter position' tabIndex={0} type='text' @@ -156,10 +156,12 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { setWorkInfo({ - ...workInfo, - industry: event.target.value, - })} + onChange={function onChange(event: any) { + setWorkInfo({ + ...workInfo, + industry: event.target.value, + }) + }} name='industry' label='Industry' placeholder='Select industry' @@ -170,10 +172,12 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { name='location' label='Location' value={workInfo.city} - onChange={event => setWorkInfo({ - ...workInfo, - city: event.target.value, - })} + onChange={function onChange(event: any) { + setWorkInfo({ + ...workInfo, + city: event.target.value, + }) + }} placeholder='Enter city, country' tabIndex={0} type='text' @@ -191,7 +195,7 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { > { + onChange={function onChange(v: any) { setWorkInfo({ ...workInfo, startDate: v || undefined, @@ -211,7 +215,7 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { { + onChange={function onChange(v: any) { setWorkInfo({ ...workInfo, endDate: v || undefined, @@ -228,7 +232,7 @@ const ModalAddWork: FC = (props: ModalAddWorkProps) => { label='I am currently working in this role' checked={workInfo.currentlyWorking} inline - onChange={(e: any) => { + onChange={function onChange(e: any) { setWorkInfo({ ...workInfo, currentlyWorking: e.target.checked, diff --git a/src/apps/onboarding/src/components/modal-upload-photo/index.tsx b/src/apps/onboarding/src/components/modal-upload-photo/index.tsx index 07c20ad2c..27494dbce 100644 --- a/src/apps/onboarding/src/components/modal-upload-photo/index.tsx +++ b/src/apps/onboarding/src/components/modal-upload-photo/index.tsx @@ -1,18 +1,15 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable sort-keys */ -/* eslint-disable unicorn/no-null */ import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react' -import classNames from 'classnames' -import _ from 'lodash' import { DropzoneState, useDropzone } from 'react-dropzone' +import _ from 'lodash' +import classNames from 'classnames' import { BaseModal, Button } from '~/libs/ui' import { updateMemberPhotoAsync } from '~/libs/core' -import styles from './styles.module.scss' import MemberInfo from '../../models/MemberInfo' +import styles from './styles.module.scss' + interface ModalUploadPhotoProps { onClose?: () => void memberInfo?: MemberInfo @@ -33,11 +30,11 @@ const ModalUploadPhoto: FC = (props: ModalUploadPhotoProp getRootProps, getInputProps, }: DropzoneState = useDropzone({ - multiple: false, accept: 'image/jpg, image/jpeg, image/png', - minSize: 1, - maxSize: 2097152, disabled: isSaving, + maxSize: 2097152, + minSize: 1, + multiple: false, onDrop, }) @@ -80,10 +77,7 @@ const ModalUploadPhoto: FC = (props: ModalUploadPhotoProp size='lg' label='save profile picture' disabled={!props.memberInfo || isSaving || !myFiles.length} - onClick={() => { - handleModifyPhotoSave() - .then(_.noop) - }} + onClick={handleModifyPhotoSave} />
)} @@ -106,13 +100,14 @@ const ModalUploadPhoto: FC = (props: ModalUploadPhotoProp BROWSE
- ) : null} + ) : undefined} + {(!isSaving && imgUrl) ? (
- ) : null} + ) : undefined} {isSaving ? (
@@ -121,7 +116,7 @@ const ModalUploadPhoto: FC = (props: ModalUploadPhotoProp
- ) : null} + ) : undefined}
Add a photo that you would like to share to the customers and community members. diff --git a/src/apps/onboarding/src/components/progress-bar/index.tsx b/src/apps/onboarding/src/components/progress-bar/index.tsx index ed6c5e90f..71cb6c627 100644 --- a/src/apps/onboarding/src/components/progress-bar/index.tsx +++ b/src/apps/onboarding/src/components/progress-bar/index.tsx @@ -1,6 +1,4 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -import React, { FC, useMemo } from 'react' +import { FC, useMemo } from 'react' import classNames from 'classnames' import styles from './styles.module.scss' diff --git a/src/apps/onboarding/src/config/index.ts b/src/apps/onboarding/src/config/index.ts index 1a5dd87f2..7df974e16 100644 --- a/src/apps/onboarding/src/config/index.ts +++ b/src/apps/onboarding/src/config/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export const ACTIONS: { MEMBER: { GET_MEMBER: string; @@ -14,14 +13,14 @@ export const ACTIONS: { } = { MEMBER: { GET_MEMBER: 'GET_MEMBER', - UPDATE_MEMBER_PHOTO_URL: 'UPDATE_MEMBER_PHOTO_URL', - SET_WORKS: 'SET_WORKS', - SET_EDUCATIONS: 'SET_EDUCATIONS', - SET_PERSONALIZATION: 'SET_PERSONALIZATION', SET_ADDRESS: 'SET_ADDRESS', SET_CONNECT_INFO: 'SET_CONNECT_INFO', - SET_LOADING_MEMBER_TRAITS: 'SET_LOADING_MEMBER_TRAITS', + SET_EDUCATIONS: 'SET_EDUCATIONS', SET_LOADING_MEMBER_INFO: 'SET_LOADING_MEMBER_INFO', + SET_LOADING_MEMBER_TRAITS: 'SET_LOADING_MEMBER_TRAITS', + SET_PERSONALIZATION: 'SET_PERSONALIZATION', + SET_WORKS: 'SET_WORKS', + UPDATE_MEMBER_PHOTO_URL: 'UPDATE_MEMBER_PHOTO_URL', }, } diff --git a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts index 77fbe0320..5a807b350 100644 --- a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts +++ b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts @@ -1,33 +1,29 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ -/* eslint-disable sort-keys */ import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react' import _ from 'lodash' import PersonalizationInfo from '../models/PersonalizationInfo' export interface useAutoSavePersonalizationType { - personalizationInfo: PersonalizationInfo | null + personalizationInfo: PersonalizationInfo | undefined loading: boolean - setPersonalizationInfo: Dispatch> + setPersonalizationInfo: Dispatch> } type useAutoSavePersonalizationFunctionType = ( - reduxPersonalization: PersonalizationInfo | null, + reduxPersonalization: PersonalizationInfo | undefined, updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, shouldSavingData: MutableRefObject, ) => useAutoSavePersonalizationType export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType = ( - reduxPersonalization: PersonalizationInfo | null, + reduxPersonalization: PersonalizationInfo | undefined, updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, shouldSavingData: MutableRefObject, ) => { const [loading, setLoading] = useState(false) - const [personalizationInfo, setPersonalizationInfo] = useState(null) + const [personalizationInfo, setPersonalizationInfo] = useState(undefined) const saveData: any = async () => { if (!personalizationInfo) { @@ -74,8 +70,8 @@ export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType }, [loading]) return { + loading, personalizationInfo, setPersonalizationInfo, - loading, } } diff --git a/src/apps/onboarding/src/models/ConnectInfo.ts b/src/apps/onboarding/src/models/ConnectInfo.ts index e7f80beec..3fb5f0db4 100644 --- a/src/apps/onboarding/src/models/ConnectInfo.ts +++ b/src/apps/onboarding/src/models/ConnectInfo.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export default interface ConnectInfo { country: string phoneNumber: string diff --git a/src/apps/onboarding/src/models/EducationInfo.ts b/src/apps/onboarding/src/models/EducationInfo.ts index 5f4b74b6a..172d332b2 100644 --- a/src/apps/onboarding/src/models/EducationInfo.ts +++ b/src/apps/onboarding/src/models/EducationInfo.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export default interface EducationInfo { collegeName: string major: string @@ -10,9 +9,9 @@ export default interface EducationInfo { export const emptyEducationInfo: () => EducationInfo = () => ({ collegeName: '', - major: '', dateDescription: '', - startDate: undefined, endDate: undefined, id: 0, + major: '', + startDate: undefined, }) diff --git a/src/apps/onboarding/src/models/MemberAddress.ts b/src/apps/onboarding/src/models/MemberAddress.ts index 4c5eb8ba4..35d72c860 100644 --- a/src/apps/onboarding/src/models/MemberAddress.ts +++ b/src/apps/onboarding/src/models/MemberAddress.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export default interface MemberAddress { streetAddr1: string streetAddr2: string @@ -8,9 +7,9 @@ export default interface MemberAddress { } export const emptyMemberAddress: () => MemberAddress = () => ({ - streetAddr1: '', - streetAddr2: '', city: '', stateCode: '', + streetAddr1: '', + streetAddr2: '', zip: '', }) diff --git a/src/apps/onboarding/src/models/MemberInfo.ts b/src/apps/onboarding/src/models/MemberInfo.ts index ff524419b..5e623e4ce 100644 --- a/src/apps/onboarding/src/models/MemberInfo.ts +++ b/src/apps/onboarding/src/models/MemberInfo.ts @@ -1,6 +1,6 @@ -/* eslint-disable ordered-imports/ordered-imports */ -import { MemberMaxRating, MemberEmsiSkill } from '~/apps/talent-search/src/lib/models' +import { MemberEmsiSkill, MemberMaxRating } from '~/apps/talent-search/src/lib/models' import { MemberStats } from '~/libs/core' + import MemberAddress from './MemberAddress' export default interface MemberInfo { diff --git a/src/apps/onboarding/src/models/PersonalizationInfo.ts b/src/apps/onboarding/src/models/PersonalizationInfo.ts index c6eeff553..e23a7752f 100644 --- a/src/apps/onboarding/src/models/PersonalizationInfo.ts +++ b/src/apps/onboarding/src/models/PersonalizationInfo.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export default interface PersonalizationInfo { referAs?: string profileSelfTitle?: string @@ -7,8 +6,8 @@ export default interface PersonalizationInfo { } export const emptyPersonalizationInfo: () => PersonalizationInfo = () => ({ - referAs: '', + availableForGigs: true, profileSelfTitle: '', + referAs: '', shortBio: '', - availableForGigs: true, }) diff --git a/src/apps/onboarding/src/models/SelectOption.ts b/src/apps/onboarding/src/models/SelectOption.ts index b1d94d69a..473f70dfd 100644 --- a/src/apps/onboarding/src/models/SelectOption.ts +++ b/src/apps/onboarding/src/models/SelectOption.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export default interface SelectOption { label: string key: string diff --git a/src/apps/onboarding/src/models/WorkInfo.ts b/src/apps/onboarding/src/models/WorkInfo.ts index 709c34cfe..87669e989 100644 --- a/src/apps/onboarding/src/models/WorkInfo.ts +++ b/src/apps/onboarding/src/models/WorkInfo.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export default interface WorkInfo { company?: string position?: string @@ -12,13 +11,13 @@ export default interface WorkInfo { } export const emptyWorkInfo: () => WorkInfo = () => ({ - company: '', - position: '', - industry: '', city: '', - startDate: undefined, + company: '', + currentlyWorking: false, dateDescription: '', endDate: undefined, - currentlyWorking: false, id: 0, + industry: '', + position: '', + startDate: undefined, }) diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx index f98e87d9d..7b3fcb2f9 100644 --- a/src/apps/onboarding/src/pages/account-details/index.tsx +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -1,39 +1,34 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ -/* eslint-disable sort-keys */ import { useNavigate } from 'react-router-dom' import { connect } from 'react-redux' import { FC, MutableRefObject, useEffect, useRef, useState } from 'react' import _ from 'lodash' import classNames from 'classnames' -import { Button, InputSelect, PageDivider } from '~/libs/ui' +import { Button, IconOutline, InputSelect, PageDivider } from '~/libs/ui' import { getCountryLookup } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' import { EnvironmentConfig } from '~/config' import { Member } from '~/apps/talent-search/src/lib/models' import { ProgressBar } from '../../components/progress-bar' - -import styles from './styles.module.scss' -import InputTextAutoSave from '../../components/InputTextAutoSave' import { validatePhonenumber } from '../../utils/validation' import { createMemberConnectInfos, updateMemberConnectInfos, updateMemberHomeAddresss, } from '../../redux/actions/member' -import MemberAddress, { emptyMemberAddress } from '../../models/MemberAddress' import ConnectInfo, { emptyConnectInfo } from '../../models/ConnectInfo' -import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' +import InputTextAutoSave from '../../components/InputTextAutoSave' +import MemberAddress, { emptyMemberAddress } from '../../models/MemberAddress' + +import styles from './styles.module.scss' const blankMemberAddress: MemberAddress = emptyMemberAddress() const blankConnectInfo: ConnectInfo = emptyConnectInfo() const PageAccountDetailsContent: FC<{ - reduxAddress: MemberAddress | null - reduxConnectInfo: ConnectInfo | null - reduxMemberInfo: Member | null + reduxAddress: MemberAddress | undefined + reduxConnectInfo: ConnectInfo | undefined + reduxMemberInfo: Member | undefined updateMemberConnectInfos: (infos: ConnectInfo[]) => void createMemberConnectInfos: (infos: ConnectInfo[]) => void updateMemberHomeAddresss: (infos: MemberAddress[]) => void @@ -43,8 +38,8 @@ const PageAccountDetailsContent: FC<{ const navigate: any = useNavigate() const [loadingAddress, setLoadingAddress] = useState(false) const [loadingConnectInfo, setLoadingConnectInfo] = useState(false) - const [memberAddress, setMemberAddress] = useState(null) - const [connectInfo, setConnectInfo] = useState(null) + const [memberAddress, setMemberAddress] = useState(undefined) + const [connectInfo, setConnectInfo] = useState(undefined) const [formErrors, setFormErrors] = useState({}) const [countryOptions, setCountryOptions] = useState<{ label: string @@ -153,8 +148,8 @@ const PageAccountDetailsContent: FC<{ if (results) { setCountryOptions(_.sortBy(results, 'country') .map((country: any) => ({ - value: country.country, label: country.country, + value: country.country, }))) } }) @@ -192,7 +187,7 @@ const PageAccountDetailsContent: FC<{ name='streetAddr1' label='Address 1' value={memberAddress?.streetAddr1 || ''} - onChange={event => { + onChange={function (event?: string) { setMemberAddress({ ...(memberAddress || blankMemberAddress), streetAddr1: event || '', @@ -207,7 +202,7 @@ const PageAccountDetailsContent: FC<{ name='streetAddr2' label='Address 2' value={memberAddress?.streetAddr2 || ''} - onChange={event => { + onChange={function (event?: string) { setMemberAddress({ ...(memberAddress || blankMemberAddress), streetAddr2: event || '', @@ -226,7 +221,7 @@ const PageAccountDetailsContent: FC<{ name='city' label='City' value={memberAddress?.city || ''} - onChange={event => { + onChange={function (event?: string) { setMemberAddress({ ...(memberAddress || blankMemberAddress), city: event || '', @@ -245,7 +240,7 @@ const PageAccountDetailsContent: FC<{ name='stateCode' label='State / Province' value={memberAddress?.stateCode || ''} - onChange={event => { + onChange={function (event?: string) { setMemberAddress({ ...(memberAddress || blankMemberAddress), stateCode: event || '', @@ -265,7 +260,7 @@ const PageAccountDetailsContent: FC<{ name='zip' label='Zip / Postal Code' value={memberAddress?.zip || ''} - onChange={event => { + onChange={function (event?: string) { setMemberAddress({ ...(memberAddress || blankMemberAddress), zip: event || '', @@ -282,7 +277,7 @@ const PageAccountDetailsContent: FC<{ { + onChange={function (event: any) { setConnectInfo({ ...(connectInfo || blankConnectInfo), country: event.target.value, @@ -304,7 +299,7 @@ const PageAccountDetailsContent: FC<{ name='phoneNumber' label='Phone Number' value={connectInfo?.phoneNumber || ''} - onChange={event => { + onChange={function (event?: string) { setConnectInfo({ ...(connectInfo || blankConnectInfo), phoneNumber: event || '', @@ -330,9 +325,9 @@ const PageAccountDetailsContent: FC<{ size='lg' secondary iconToLeft - icon={IconBackGreen} + icon={IconOutline.ChevronLeftIcon} disabled={!_.isEmpty(formErrors)} - onClick={() => { + onClick={function onPrevious() { if (loadingAddress || loadingConnectInfo) { shouldNavigateTo.current = '../personalization' } else { @@ -345,7 +340,7 @@ const PageAccountDetailsContent: FC<{ primary iconToLeft disabled={!_.isEmpty(formErrors) || !props.reduxMemberInfo} - onClick={() => { + onClick={function onNext() { if (loadingAddress || loadingConnectInfo) { shouldNavigateTo.current = `${EnvironmentConfig.USER_PROFILE_URL}/${props.reduxMemberInfo?.handle}` @@ -381,9 +376,9 @@ const mapStateToProps: any = (state: any) => { } const mapDispatchToProps: any = { - updateMemberHomeAddresss, - updateMemberConnectInfos, createMemberConnectInfos, + updateMemberConnectInfos, + updateMemberHomeAddresss, } export const PageAccountDetails: any = connect(mapStateToProps, mapDispatchToProps)(PageAccountDetailsContent) diff --git a/src/apps/onboarding/src/pages/educations/index.tsx b/src/apps/onboarding/src/pages/educations/index.tsx index 98eaf54e4..c7a95e45c 100644 --- a/src/apps/onboarding/src/pages/educations/index.tsx +++ b/src/apps/onboarding/src/pages/educations/index.tsx @@ -1,31 +1,28 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ import { FC, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import classNames from 'classnames' -import _ from 'lodash' import { connect } from 'react-redux' +import _ from 'lodash' +import classNames from 'classnames' -import { Button, PageDivider } from '~/libs/ui' +import { Button, IconOutline, PageDivider } from '~/libs/ui' +import { createMemberEducations, updateMemberEducations } from '../../redux/actions/member' import { ProgressBar } from '../../components/progress-bar' -import styles from './styles.module.scss' +import CardItem from '../../components/card-item' import EducationInfo from '../../models/EducationInfo' import ModalAddEducation from '../../components/modal-add-education' -import { updateMemberEducations, createMemberEducations } from '../../redux/actions/member' -import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' -import CardItem from '../../components/card-item' + +import styles from './styles.module.scss' export const PageEducationsContent: FC<{ - reduxEducations: EducationInfo[] | null + reduxEducations: EducationInfo[] | undefined updateMemberEducations: (educations: EducationInfo[]) => void createMemberEducations: (educations: EducationInfo[]) => void loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() - const [editingEducation, setEditingEducation] = useState(null) - const [educations, setEducations] = useState(null) + const [editingEducation, setEditingEducation] = useState(undefined) + const [educations, setEducations] = useState(undefined) const [educationId, setEducationId] = useState(10) const [showAddEducationModal, setShowAddEducationModal] = useState(false) const [loading, setLoading] = useState(false) @@ -78,21 +75,25 @@ export const PageEducationsContent: FC<{ title={education.major || ''} subTitle={education.collegeName || ''} description={education.dateDescription || ''} - onEdit={() => { + onEdit={function onEdit() { setEditingEducation(education) setShowAddEducationModal(true) }} - onDelete={() => setEducations(_.filter(educations, w => w.id !== education.id))} + onDelete={function onDelete() { + setEducations(_.filter(educations, w => w.id !== education.id)) + }} /> ))}
- ) : null} + ) : undefined} @@ -127,22 +132,22 @@ export const PageEducationsContent: FC<{ {showAddEducationModal ? ( { + onClose={function onClose() { setShowAddEducationModal(false) - setEditingEducation(null) + setEditingEducation(undefined) }} - onAdd={newEducation => { + onAdd={function onAdd(newEducation: EducationInfo) { setEducations([...(educations || []), { ...newEducation, id: educationId + 1, }]) setEducationId(educationId + 1) }} - onEdit={editEducation => { + onEdit={function onEdit(editEducation: EducationInfo) { setEducations((educations || []).map(w => (w.id !== editEducation.id ? w : editEducation))) }} /> - ) : null} + ) : undefined}
) } diff --git a/src/apps/onboarding/src/pages/onboarding/index.tsx b/src/apps/onboarding/src/pages/onboarding/index.tsx index 7b6319c37..3b61e9d42 100644 --- a/src/apps/onboarding/src/pages/onboarding/index.tsx +++ b/src/apps/onboarding/src/pages/onboarding/index.tsx @@ -1,6 +1,3 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ import { FC, useContext, useEffect } from 'react' import { Outlet, Routes } from 'react-router-dom' import { connect, Provider } from 'react-redux' @@ -14,12 +11,13 @@ import { onboardRouteId } from '../../onboarding.routes' import { fetchMemberInfo, fetchMemberTraits } from '../../redux/actions/member' import store from '../../redux/store' import '../../styles/global/_index.scss' + import styles from './styles.module.scss' const OnboardingContent: FC<{ fetchMemberInfo: () => void fetchMemberTraits: () => void - reduxMemberInfo: Member | null + reduxMemberInfo: Member | undefined }> = props => { const { getChildRoutes }: RouterContextData = useContext(routerContext) useEffect(() => { diff --git a/src/apps/onboarding/src/pages/open-to-work/index.tsx b/src/apps/onboarding/src/pages/open-to-work/index.tsx index 20f3cbff3..899e235e3 100644 --- a/src/apps/onboarding/src/pages/open-to-work/index.tsx +++ b/src/apps/onboarding/src/pages/open-to-work/index.tsx @@ -1,28 +1,24 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' import { FC, MutableRefObject, useEffect, useMemo, useRef } from 'react' -import classNames from 'classnames' import { connect } from 'react-redux' +import classNames from 'classnames' -import { Button, PageDivider } from '~/libs/ui' +import { Button, IconOutline, PageDivider } from '~/libs/ui' import { FormInputCheckbox } from '~/apps/self-service/src/components/form-elements' +import { createMemberPersonalizations, updateMemberPersonalizations } from '../../redux/actions/member' import { ProgressBar } from '../../components/progress-bar' +import { useAutoSavePersonalization, useAutoSavePersonalizationType } from '../../hooks/useAutoSavePersonalization' +import PersonalizationInfo, { emptyPersonalizationInfo } from '../../models/PersonalizationInfo' import styles from './styles.module.scss' -import PersonalizationInfo, { emptyPersonalizationInfo } from '../../models/PersonalizationInfo' -import { createMemberPersonalizations, updateMemberPersonalizations } from '../../redux/actions/member' -import { useAutoSavePersonalization, useAutoSavePersonalizationType } from '../../hooks/useAutoSavePersonalization' -import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' const FormInputCheckboxMiddleware: any = FormInputCheckbox as any const blankPersonalizationInfo: PersonalizationInfo = emptyPersonalizationInfo() export const PageOpenToWorkContent: FC<{ - reduxPersonalization: PersonalizationInfo | null + reduxPersonalization: PersonalizationInfo | undefined updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void createMemberPersonalizations: (infos: PersonalizationInfo[]) => void loadingMemberTraits: boolean @@ -90,7 +86,7 @@ export const PageOpenToWorkContent: FC<{ label='Yes, I’m open to work' checked={availableForGigsValue} inline - onChange={(e: any) => { + onChange={function onChange(e: any) { setPersonalizationInfo({ ...(personalizationInfo || blankPersonalizationInfo), availableForGigs: e.target.checked, @@ -114,8 +110,8 @@ export const PageOpenToWorkContent: FC<{ secondary iconToLeft disabled={loading} - icon={IconBackGreen} - onClick={() => { + icon={IconOutline.ChevronLeftIcon} + onClick={function previousPage() { checkToNavigateNextPage('../skills') }} /> @@ -124,7 +120,7 @@ export const PageOpenToWorkContent: FC<{ primary iconToLeft disabled={loading} - onClick={() => { + onClick={function nextPage() { checkToNavigateNextPage('../works') }} > diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index 9663a016a..b55de90e8 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -1,36 +1,32 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ -/* eslint-disable sort-keys */ import { useNavigate } from 'react-router-dom' import { connect } from 'react-redux' import { FC, MutableRefObject, useEffect, useRef } from 'react' import classNames from 'classnames' -import { Button, PageDivider } from '~/libs/ui' +import { Button, IconOutline, PageDivider } from '~/libs/ui' import { EnvironmentConfig } from '~/config' +import { useAutoSavePersonalization, useAutoSavePersonalizationType } from '../../hooks/useAutoSavePersonalization' import { ProgressBar } from '../../components/progress-bar' -import styles from './styles.module.scss' -import FieldAvatar from '../../components/FieldAvatar' -import InputTextAutoSave from '../../components/InputTextAutoSave' -import PersonalizationInfo, { emptyPersonalizationInfo } from '../../models/PersonalizationInfo' -import InputTextareaAutoSave from '../../components/InputTextareaAutoSave' import { createMemberPersonalizations, setMemberPhotoUrl, updateMemberPersonalizations, updateMemberPhotoUrl, } from '../../redux/actions/member' +import FieldAvatar from '../../components/FieldAvatar' +import InputTextAutoSave from '../../components/InputTextAutoSave' +import InputTextareaAutoSave from '../../components/InputTextareaAutoSave' import MemberInfo from '../../models/MemberInfo' -import { useAutoSavePersonalization, useAutoSavePersonalizationType } from '../../hooks/useAutoSavePersonalization' -import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' +import PersonalizationInfo, { emptyPersonalizationInfo } from '../../models/PersonalizationInfo' + +import styles from './styles.module.scss' const blankPersonalizationInfo: PersonalizationInfo = emptyPersonalizationInfo() const PagePersonalizationContent: FC<{ memberInfo?: MemberInfo, - reduxPersonalization: PersonalizationInfo | null + reduxPersonalization: PersonalizationInfo | undefined updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void createMemberPersonalizations: (infos: PersonalizationInfo[]) => void setMemberPhotoUrl: (photoUrl: string) => void @@ -81,7 +77,7 @@ const PagePersonalizationContent: FC<{ name='title' label='Bio Title' value={personalizationInfo?.profileSelfTitle || ''} - onChange={value => { + onChange={function onChange(value: string | undefined) { setPersonalizationInfo({ ...(personalizationInfo || blankPersonalizationInfo), profileSelfTitle: value || '', @@ -98,7 +94,7 @@ const PagePersonalizationContent: FC<{ name='shortBio' label='Bio' value={personalizationInfo?.shortBio || ''} - onChange={value => { + onChange={function onChange(value: string | undefined) { setPersonalizationInfo({ ...(personalizationInfo || blankPersonalizationInfo), shortBio: value || '', @@ -121,8 +117,8 @@ const PagePersonalizationContent: FC<{ size='lg' secondary iconToLeft - icon={IconBackGreen} - onClick={() => { + icon={IconOutline.ChevronLeftIcon} + onClick={function previousPage() { if (loading) { shouldNavigateTo.current = '../educations' } else { @@ -134,7 +130,7 @@ const PagePersonalizationContent: FC<{ size='lg' primary iconToLeft - onClick={() => { + onClick={function nextPage() { if (loading) { shouldNavigateTo.current = `${EnvironmentConfig.USER_PROFILE_URL}/${props.memberInfo?.handle}` @@ -159,16 +155,16 @@ const mapStateToProps: any = (state: any) => { }: any = state.member return { - memberInfo, loadingMemberTraits, + memberInfo, reduxPersonalization: personalization, } } const mapDispatchToProps: any = { createMemberPersonalizations, - updateMemberPersonalizations, setMemberPhotoUrl, + updateMemberPersonalizations, updateMemberPhotoUrl, } diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index cfda82bc2..8d931d7b3 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -1,10 +1,7 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ import { useNavigate } from 'react-router-dom' import { FC, useState } from 'react' -import classNames from 'classnames' import { connect } from 'react-redux' +import classNames from 'classnames' import { Button, PageDivider } from '~/libs/ui' import { Member } from '~/apps/talent-search/src/lib/models' @@ -15,13 +12,13 @@ import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' export const PageSkillsContent: FC<{ - reduxMemberInfo: Member | null + reduxMemberInfo: Member | undefined }> = props => { const navigate: any = useNavigate() const [loading, setLoading] = useState(false) const { formInput: emsiFormInput, saveSkills: saveEmsiSkills }: MemberSkillEditor = useMemberSkillEditor() - const saveSkills = async (): Promise => { + async function saveSkills(): Promise { setLoading(true) try { await saveEmsiSkills() diff --git a/src/apps/onboarding/src/pages/start/index.tsx b/src/apps/onboarding/src/pages/start/index.tsx index 1c6043e02..6ab589cca 100644 --- a/src/apps/onboarding/src/pages/start/index.tsx +++ b/src/apps/onboarding/src/pages/start/index.tsx @@ -1,10 +1,11 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ import { FC } from 'react' -import { Button, PageDivider } from '~/libs/ui' import { useNavigate } from 'react-router-dom' import classNames from 'classnames' + +import { Button, PageDivider } from '~/libs/ui' + import { ProgressBar } from '../../components/progress-bar' + import styles from './styles.module.scss' export const PageStart: FC<{}> = () => { @@ -44,7 +45,9 @@ export const PageStart: FC<{}> = () => { size='lg' secondary iconToLeft - onClick={() => navigate('../skills')} + onClick={function nextPage() { + navigate('../skills') + }} > enter my info @@ -64,7 +67,9 @@ export const PageStart: FC<{}> = () => { size='lg' primary iconToLeft - onClick={() => navigate('../skills')} + onClick={function nextPage() { + navigate('../skills') + }} > next diff --git a/src/apps/onboarding/src/pages/works/index.tsx b/src/apps/onboarding/src/pages/works/index.tsx index 2cb5cb950..7c2ffd213 100644 --- a/src/apps/onboarding/src/pages/works/index.tsx +++ b/src/apps/onboarding/src/pages/works/index.tsx @@ -1,31 +1,28 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable unicorn/no-null */ import { FC, useEffect, useState } from 'react' import { connect } from 'react-redux' import { useNavigate } from 'react-router-dom' -import classNames from 'classnames' import _ from 'lodash' +import classNames from 'classnames' -import { Button, PageDivider } from '~/libs/ui' +import { Button, IconOutline, PageDivider } from '~/libs/ui' -import { ProgressBar } from '../../components/progress-bar' -import styles from './styles.module.scss' -import WorkInfo from '../../models/WorkInfo' -import ModalAddWork from '../../components/modal-add-work' -import { ReactComponent as IconBackGreen } from '../../assets/images/back-green.svg' import { createMemberWorks, updateMemberWorks } from '../../redux/actions/member' +import { ProgressBar } from '../../components/progress-bar' import CardItem from '../../components/card-item' +import ModalAddWork from '../../components/modal-add-work' +import WorkInfo from '../../models/WorkInfo' + +import styles from './styles.module.scss' export const PageWorksContent: FC<{ - reduxWorks: WorkInfo[] | null + reduxWorks: WorkInfo[] | undefined updateMemberWorks: (works: WorkInfo[]) => void createMemberWorks: (works: WorkInfo[]) => void loadingMemberTraits: boolean }> = props => { const navigate: any = useNavigate() - const [editingWork, setEditingWork] = useState(null) - const [works, setWorks] = useState(null) + const [editingWork, setEditingWork] = useState(undefined) + const [works, setWorks] = useState(undefined) const [workId, setWorkId] = useState(10) const [showAddWorkModal, setShowAddWorkModal] = useState(false) const [loading, setLoading] = useState(false) @@ -79,21 +76,25 @@ export const PageWorksContent: FC<{ title={work.position || ''} subTitle={work.city || ''} description={work.dateDescription || ''} - onEdit={() => { + onEdit={function onEdit() { setEditingWork(work) setShowAddWorkModal(true) }} - onDelete={() => setWorks(_.filter(works, w => w.id !== work.id))} + onDelete={function onDelete() { + setWorks(_.filter(works, w => w.id !== work.id)) + }} /> ))}
- ) : null} + ) : undefined} @@ -130,24 +135,24 @@ export const PageWorksContent: FC<{ {showAddWorkModal ? ( { + onClose={function onClose() { setShowAddWorkModal(false) - setEditingWork(null) + setEditingWork(undefined) }} - onAdd={newWork => { + onAdd={function onAdd(newWork: WorkInfo) { setWorks([...(works || []), { ...newWork, id: workId + 1, }]) setWorkId(workId + 1) }} - onEdit={editWork => { + onEdit={function onEdit(editWork: WorkInfo) { setWorks( (works || []).map(w => (w.id !== editWork.id ? w : editWork)), ) }} /> - ) : null} + ) : undefined}
) } diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index 61077b5c7..2965a59d0 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -1,12 +1,8 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable sort-keys */ -/* eslint-disable unicorn/no-null */ -import moment from 'moment' import _ from 'lodash' +import moment from 'moment' -import { getAsync as getAsyncToken } from '~/libs/core/lib/auth/token-functions/token.functions' import { TokenModel } from '~/libs/core' +import { getAsync as getAsyncToken } from '~/libs/core/lib/auth/token-functions/token.functions' import { createMemberTraits, updateMemberTraits, @@ -14,55 +10,55 @@ import { import { ACTIONS } from '../../config' import { getMemberInfo, getMemberTraits, putMemberInfo } from '../../services/members' -import WorkInfo from '../../models/WorkInfo' +import ConnectInfo from '../../models/ConnectInfo' import EducationInfo from '../../models/EducationInfo' -import PersonalizationInfo from '../../models/PersonalizationInfo' import MemberAddress from '../../models/MemberAddress' import MemberInfo from '../../models/MemberInfo' -import ConnectInfo from '../../models/ConnectInfo' +import PersonalizationInfo from '../../models/PersonalizationInfo' +import WorkInfo from '../../models/WorkInfo' export const updateMemberInfo: any = (memberInfo: MemberInfo) => ({ - type: ACTIONS.MEMBER.GET_MEMBER, payload: memberInfo, + type: ACTIONS.MEMBER.GET_MEMBER, }) export const updateWorks: any = (works: WorkInfo[]) => ({ - type: ACTIONS.MEMBER.SET_WORKS, payload: works, + type: ACTIONS.MEMBER.SET_WORKS, }) export const updateEducations: any = (educations: EducationInfo[]) => ({ - type: ACTIONS.MEMBER.SET_EDUCATIONS, payload: educations, + type: ACTIONS.MEMBER.SET_EDUCATIONS, }) export const updatePersonalization: any = (personalization: PersonalizationInfo) => ({ - type: ACTIONS.MEMBER.SET_PERSONALIZATION, payload: personalization, + type: ACTIONS.MEMBER.SET_PERSONALIZATION, }) export const updateConnectInfo: any = (connectInfo: ConnectInfo) => ({ - type: ACTIONS.MEMBER.SET_CONNECT_INFO, payload: connectInfo, + type: ACTIONS.MEMBER.SET_CONNECT_INFO, }) export const updateAddress: any = (address: MemberAddress) => ({ - type: ACTIONS.MEMBER.SET_ADDRESS, payload: address, + type: ACTIONS.MEMBER.SET_ADDRESS, }) export const updateLoadingMemberTraits: any = (loading: boolean) => ({ - type: ACTIONS.MEMBER.SET_LOADING_MEMBER_TRAITS, payload: loading, + type: ACTIONS.MEMBER.SET_LOADING_MEMBER_TRAITS, }) export const updateLoadingMemberInfo: any = (loading: boolean) => ({ - type: ACTIONS.MEMBER.SET_LOADING_MEMBER_INFO, payload: loading, + type: ACTIONS.MEMBER.SET_LOADING_MEMBER_INFO, }) export const fetchMemberInfo: any = () => async (dispatch: any) => { let tokenInfo: TokenModel - let memberInfo: MemberInfo | null = null + let memberInfo: MemberInfo | undefined dispatch(updateLoadingMemberInfo(true)) try { tokenInfo = await getAsyncToken() @@ -77,10 +73,10 @@ export const fetchMemberInfo: any = () => async (dispatch: any) => { if (memberInfo.addresses) { const addresses: MemberAddress[] = memberInfo.addresses.map(address => ({ ...address, - streetAddr1: address.streetAddr1, - streetAddr2: address.streetAddr2, city: address.city, stateCode: address.stateCode, + streetAddr1: address.streetAddr1, + streetAddr2: address.streetAddr2, zip: address.zip, })) const matchAddress: MemberAddress = _.find(addresses, { type: 'HOME' }) as MemberAddress @@ -126,15 +122,15 @@ export const fetchMemberTraits: any = () => async (dispatch: any) => { startDate || endDate ) ? `${startDateString}${endDateString}` : '' return ({ - company: j.company, - position: j.position, - industry: j.industry, city: j.cityTown, - startDate, - endDate, + company: j.company, currentlyWorking: j.working, dateDescription, + endDate, id: index + 1, + industry: j.industry, + position: j.position, + startDate, }) }) dispatch(updateWorks(works)) @@ -163,11 +159,11 @@ export const fetchMemberTraits: any = () => async (dispatch: any) => { ) ? `${startDateString}${endDateString}` : '' return ({ collegeName: e.schoolCollegeName, - major: e.major, - startDate, - endDate, dateDescription, + endDate, id: index + 1, + major: e.major, + startDate, }) }) dispatch(updateEducations(educations)) @@ -180,10 +176,10 @@ export const fetchMemberTraits: any = () => async (dispatch: any) => { if (personalizationExpValue) { const personalizations: PersonalizationInfo[] = personalizationExpValue.map((e: any) => ({ ...e, - referAs: e.referAs, + availableForGigs: e.availableForGigs, profileSelfTitle: e.profileSelfTitle, + referAs: e.referAs, shortBio: e.shortBio, - availableForGigs: e.availableForGigs, })) dispatch(updatePersonalization(personalizations[0])) } @@ -214,10 +210,10 @@ const createWorksPayloadData: any = (works: WorkInfo[]) => { currentlyWorking, }: any = work return { + cityTown: city, company, industry, position, - cityTown: city, timePeriodFrom: startDate ? startDate.toISOString() : '', timePeriodTo: endDate ? endDate.toISOString() : '', working: currentlyWorking, @@ -269,8 +265,8 @@ const createEducationsPayloadData: any = (educations: EducationInfo[]) => { endDate, }: any = education return { - schoolCollegeName: collegeName, major, + schoolCollegeName: collegeName, timePeriodFrom: startDate ? startDate.toISOString() : '', timePeriodTo: endDate ? endDate.toISOString() : '', } @@ -322,10 +318,10 @@ const createPersonalizationsPayloadData: any = (personalizations: Personalizatio }: any = personalization return { ...personalization, - referAs, + availableForGigs, profileSelfTitle, + referAs, shortBio, - availableForGigs, } }) @@ -415,8 +411,8 @@ export const createMemberConnectInfos: any = (connectInfos: ConnectInfo[]) => as } export const setMemberPhotoUrl: any = (photoUrl: string) => ({ - type: ACTIONS.MEMBER.UPDATE_MEMBER_PHOTO_URL, payload: photoUrl, + type: ACTIONS.MEMBER.UPDATE_MEMBER_PHOTO_URL, }) export const updateMemberHomeAddresss: any = (addresses: MemberAddress[]) => async (dispatch: any) => { @@ -425,12 +421,12 @@ export const updateMemberHomeAddresss: any = (addresses: MemberAddress[]) => asy await putMemberInfo(tokenInfo.handle || '', { addresses: addresses.map(address => ({ ...address, - streetAddr1: address.streetAddr1, - streetAddr2: address.streetAddr2, city: address.city, stateCode: address.stateCode, - zip: address.zip, + streetAddr1: address.streetAddr1, + streetAddr2: address.streetAddr2, type: 'HOME', + zip: address.zip, })), }) dispatch(updateAddress(addresses[0])) diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index 2cb28dc3f..630389a23 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -1,13 +1,11 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable default-param-last */ import { Member } from '~/apps/talent-search/src/lib/models' + import { ACTIONS } from '../../config' -import WorkInfo from '../../models/WorkInfo' +import ConnectInfo from '../../models/ConnectInfo' import EducationInfo from '../../models/EducationInfo' -import PersonalizationInfo from '../../models/PersonalizationInfo' import MemberAddress from '../../models/MemberAddress' -import ConnectInfo from '../../models/ConnectInfo' +import PersonalizationInfo from '../../models/PersonalizationInfo' +import WorkInfo from '../../models/WorkInfo' const initialState: { memberInfo?: Member @@ -21,7 +19,13 @@ const initialState: { } = { } -const memberReducer: any = (state = initialState, action: { type: any; payload: any; }) => { +const memberReducer: any = ( + state = initialState, + action: { type: any; payload: any; } = { + payload: undefined, + type: '', + }, +) => { switch (action.type) { case ACTIONS.MEMBER.GET_MEMBER: return { diff --git a/src/apps/onboarding/src/redux/store.ts b/src/apps/onboarding/src/redux/store.ts index d87a843fb..742c7b3e7 100644 --- a/src/apps/onboarding/src/redux/store.ts +++ b/src/apps/onboarding/src/redux/store.ts @@ -1,13 +1,10 @@ -/* eslint-disable ordered-imports/ordered-imports */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable global-require */ -/* eslint-disable @typescript-eslint/no-var-requires */ /** * Configure Redux Store */ import { applyMiddleware, compose, createStore } from 'redux' import { createPromise } from 'redux-promise-middleware' import thunk from 'redux-thunk' + import rootReducer from './reducers' const middlewares: any = [ @@ -20,6 +17,7 @@ const middlewares: any = [ // enable Redux Logger in in DEV environment if (process.env.APPMODE !== 'production') { + /* eslint-disable-next-line global-require, @typescript-eslint/no-var-requires */ const { createLogger }: any = require('redux-logger') const logger: any = createLogger() diff --git a/src/apps/onboarding/src/services/members.ts b/src/apps/onboarding/src/services/members.ts index 8c3bf26b1..74356b83a 100644 --- a/src/apps/onboarding/src/services/members.ts +++ b/src/apps/onboarding/src/services/members.ts @@ -1,6 +1,6 @@ -/* eslint-disable ordered-imports/ordered-imports */ import { getAsync, putAsync } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' import { profile } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config' + import MemberInfo from '../models/MemberInfo' export async function getMemberInfo(handle: string): Promise {