From 8ba01264b618db6cc6c7aa29cb8560bd1fbaccd0 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Jul 2023 10:52:17 +0300 Subject: [PATCH 1/6] TAL-34 - focus search input on picking popular skill --- .../src/components/search-input/SearchInput.tsx | 4 +++- .../src/routes/search-page/SearchPage.tsx | 14 ++++++++++++-- .../input-skill-selector/InputSkillSelector.tsx | 4 +++- .../input-multiselect/InputMultiselect.tsx | 10 ++++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/apps/talent-search/src/components/search-input/SearchInput.tsx b/src/apps/talent-search/src/components/search-input/SearchInput.tsx index 251da0621..db15f2cb9 100644 --- a/src/apps/talent-search/src/components/search-input/SearchInput.tsx +++ b/src/apps/talent-search/src/components/search-input/SearchInput.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo } from 'react' +import { FC, Ref, useMemo } from 'react' import classNames from 'classnames' import { IconOutline, InputMultiselectOption } from '~/libs/ui' @@ -12,6 +12,7 @@ interface SearchInputProps { onChange: (skills: Skill[]) => void skills: Skill[] onSearch?: () => void + inputRef?: Ref } const SearchInput: FC = props => { @@ -49,6 +50,7 @@ const SearchInput: FC = props => { value={emsiSkills} onChange={onChange} onSubmit={props.onSearch} + inputRef={props.inputRef} /> ) } diff --git a/src/apps/talent-search/src/routes/search-page/SearchPage.tsx b/src/apps/talent-search/src/routes/search-page/SearchPage.tsx index c6424ddf3..840213ba0 100644 --- a/src/apps/talent-search/src/routes/search-page/SearchPage.tsx +++ b/src/apps/talent-search/src/routes/search-page/SearchPage.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react' +import { FC, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ContentLayout } from '~/libs/ui' @@ -12,6 +12,7 @@ import { encodeUrlQuerySearch } from '../../lib/utils/search-query' import styles from './SearchPage.module.scss' export const SearchPage: FC = () => { + const searchInputRef = useRef() const navigate = useNavigate() const [skillsFilter, setSkillsFilter] = useState([]) @@ -20,6 +21,11 @@ export const SearchPage: FC = () => { navigate(`${TALENT_SEARCH_PATHS.results}?${searchParams}`) } + function handleSelectSkillFilter(filter: Skill[]): void { + setSkillsFilter(filter) + searchInputRef.current?.focus() + } + return ( { skills={skillsFilter} onChange={setSkillsFilter} onSearch={navigateToResults} + inputRef={searchInputRef} /> - + ) } diff --git a/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx index a26902cb3..20f539681 100644 --- a/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx +++ b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FC, ReactNode } from 'react' +import { ChangeEvent, FC, ReactNode, Ref } from 'react' import { noop } from 'lodash' import { InputMultiselect, InputMultiselectOption, InputMultiselectThemes } from '~/libs/ui' @@ -41,6 +41,7 @@ interface InputSkillSelectorProps { readonly dropdownIcon?: ReactNode readonly onSubmit?: () => void readonly additionalPlaceholder?: string + readonly inputRef?: Ref } const InputSkillSelector: FC = props => ( @@ -60,6 +61,7 @@ const InputSkillSelector: FC = props => ( dropdownIcon={props.dropdownIcon} onSubmit={props.onSubmit} additionalPlaceholder={props.additionalPlaceholder} + inputRef={props.inputRef} /> ) 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 f2af62416..24225a548 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 @@ -3,10 +3,11 @@ import { FC, KeyboardEvent, ReactNode, + Ref, useMemo, useRef, } from 'react' -import { noop } from 'lodash' +import { get, noop } from 'lodash' import { components, SelectInstance } from 'react-select' import AsyncSelect from 'react-select/async' import classNames from 'classnames' @@ -47,6 +48,7 @@ export interface InputMultiselectProps { readonly useWrapper?: boolean readonly value?: InputMultiselectOption[] readonly onSubmit?: () => void + readonly inputRef?: Ref } const MultiValueRemove: FC = (props: any) => ( @@ -80,7 +82,7 @@ const valueContainer = (additionalPlaceholder: string): FC => (props: any) => ( ) -const InputMultiselect: FC = (props: InputMultiselectProps) => { +const InputMultiselect: FC = props => { const asynSelectRef = useRef() function handleOnChange(options: readonly InputMultiselectOption[]): void { @@ -90,7 +92,7 @@ const InputMultiselect: FC = (props: InputMultiselectProp } function handleKeyPress(ev: KeyboardEvent): void { - const state = (asynSelectRef.current?.state ?? {}) as SelectInstance['state'] + const state = (get(props.inputRef ?? asynSelectRef, 'current.state') ?? {}) as SelectInstance['state'] const isSelectingOptionItem = state.focusedOption const hasValue = state.selectValue?.length > 0 if (ev.key !== 'Enter' || isSelectingOptionItem || !hasValue) { @@ -118,7 +120,7 @@ const InputMultiselect: FC = (props: InputMultiselectProp props.useWrapper === false && styles.multiSelectWrap, ) } - ref={asynSelectRef} + ref={props.inputRef ?? asynSelectRef} classNamePrefix={styles.ms} unstyled isMulti From 77b9bba8a1f81b84d198e84ccedd84403add52f3 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Jul 2023 11:13:17 +0300 Subject: [PATCH 2/6] TAL-35 - refactor talent search results cards --- .../talent-card/TalentCard.module.scss | 30 +++++-- .../src/components/talent-card/TalentCard.tsx | 78 ++++++++++--------- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss b/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss index b6350ba32..65c9fc2f9 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss @@ -1,9 +1,6 @@ @import "@libs/ui/styles/includes"; .wrap { - display: flex; - gap: $sp-10; - padding: $sp-55 $sp-8; border-radius: $sp-4; @@ -13,6 +10,11 @@ color: $black-100; } +.topWrap { + display: flex; + gap: $sp-10; +} + .profilePic { align-self: flex-start; flex: 0 0 36%; @@ -76,7 +78,7 @@ .detailsContainer { flex: 1 1 auto; - padding: $sp-5 $sp-45 $sp-3 0; + padding: $sp-5 $sp-45 0 0; } .talentInfo { @@ -107,7 +109,24 @@ .matchBar { max-width: 330px; - margin-top: $sp-8; + margin-top: $sp-4; +} + +.skillsContainer { + margin-top: $sp-5; + border-top: 1px solid $black-10; + padding-top: $sp-5; + display: flex; + flex-direction: column; + gap: $sp-2; +} + +.unmatchedSkills { + width: 100%; + display: flex; + + color: $black-60; + padding: $sp-2 $sp-3; } .skillsWrap { @@ -115,5 +134,4 @@ align-items: center; gap: $sp-2; flex-wrap: wrap; - margin-top: $sp-5; } diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx index 29eba88f2..f327ff491 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx @@ -32,62 +32,70 @@ const TalentCard: FC = props => { const talentRoute = `${TALENT_SEARCH_PATHS.talent}/${props.member.handle}` const visibleSkills = orderBy( - props.member.emsiSkills, + props.member.emsiSkills.filter(props.isMatchingSkill), [props.isMatchingSkill, isSkillVerified], ['desc', 'desc'], ) .slice(0, 6) as EmsiSkill[] const hiddenSkills = props.member.emsiSkills.length - visibleSkills.length + const unmatchedLabel = `+${hiddenSkills} more unmatched skill${hiddenSkills > 1 ? 's' : ''}` return ( -
- - {props.member.firstName.slice(0, 1)} - {props.member.lastName.slice(0, 1)} - - -
-
-
-
- {props.member.firstName} - {' '} - {props.member.lastName} -
-
- - {props.member.handle} - -
- {props.member.addresses?.map(addr => ( -
- - - {getAddrString(addr.city, getCountry(props.member.homeCountryCode))} +
+
+ + {props.member.firstName.slice(0, 1)} + {props.member.lastName.slice(0, 1)} + + +
+
+
+
+ {props.member.firstName} + {' '} + {props.member.lastName} +
+
+ + {props.member.handle}
- ))} + {props.member.addresses?.map(addr => ( +
+ + + {getAddrString(addr.city, getCountry(props.member.homeCountryCode))} + +
+ ))} +
+
- +
+
+
Matched skills
{visibleSkills.map(skill => ( ))} {hiddenSkills > 0 && ( - +
+ {unmatchedLabel} +
)}
From 0ad2c9772a5d14a83465525ffa3e80d1ecba939e Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Jul 2023 11:29:36 +0300 Subject: [PATCH 3/6] TAL-24 - move profile picture component to shared library, reuse in profile app --- .../profile-header/ProfileHeader.module.scss | 13 +--- .../profile-header/ProfileHeader.tsx | 16 +---- .../talent-card/TalentCard.module.scss | 55 ----------------- .../src/components/talent-card/TalentCard.tsx | 15 +---- src/libs/shared/lib/components/index.ts | 1 + .../ProfilePicture.module.scss | 60 +++++++++++++++++++ .../profile-picture/ProfilePicture.tsx | 26 ++++++++ .../lib/components/profile-picture/index.ts | 1 + 8 files changed, 94 insertions(+), 93 deletions(-) create mode 100644 src/libs/shared/lib/components/profile-picture/ProfilePicture.module.scss create mode 100644 src/libs/shared/lib/components/profile-picture/ProfilePicture.tsx create mode 100644 src/libs/shared/lib/components/profile-picture/index.ts 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 b6ece0b27..741c6b30b 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 @@ -11,7 +11,6 @@ padding-bottom: 150px; flex-direction: column; } - .photoWrap { position: relative; margin-right: 60px; @@ -28,16 +27,6 @@ height: 300px; border: 12px solid $tc-white; border-radius: 50%; - background: linear-gradient(90deg, #075F96, #038664); - display: flex; - justify-content: center; - align-items: center; - - > span { - font-size: 100px; - line-height: 100px; - font-family: $font-barlow; - } @include ltelg { width: 250px; @@ -123,4 +112,4 @@ margin-left: $sp-2; } } -} \ No newline at end of file +} 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 548e2f290..4c82d8ebc 100644 --- a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx @@ -11,7 +11,7 @@ import { UserTraitIds, UserTraits, } from '~/libs/core' -import { useCheckIsMobile } from '~/libs/shared' +import { ProfilePicture, useCheckIsMobile } from '~/libs/shared' import { AddButton, EditMemberPropertyBtn } from '../../components' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' @@ -32,7 +32,6 @@ export type NamesAndHandleAppearance = 'namesOnly' | 'handleOnly' | 'namesAndHan const ProfileHeader: FC = (props: ProfileHeaderProps) => { const isMobile: boolean = useCheckIsMobile() - const photoURL: string | undefined = props.profile.photoURL const hasProfilePicture = !!props.profile.photoURL const canEdit: boolean = props.authProfile?.handle === props.profile.handle @@ -127,18 +126,7 @@ const ProfileHeader: FC = (props: ProfileHeaderProps) => { function renderMemberPhotoWrap(): JSX.Element { return (
- { - photoURL ? ( - Topcoder - Member Profile Avatar - ) : ( -
- - {props.profile.firstName[0]} - {props.profile.lastName[0]} - -
- ) - } + {canEdit && hasProfilePicture && ( = props => { return (
-
- - {props.member.firstName.slice(0, 1)} - {props.member.lastName.slice(0, 1)} - - -
+
diff --git a/src/libs/shared/lib/components/index.ts b/src/libs/shared/lib/components/index.ts index 9299758bc..9781c4bb3 100644 --- a/src/libs/shared/lib/components/index.ts +++ b/src/libs/shared/lib/components/index.ts @@ -1,4 +1,5 @@ export * from './contact-support-form' export * from './modals' +export * from './profile-picture' export * from './input-skill-selector' export * from './member-skill-editor' diff --git a/src/libs/shared/lib/components/profile-picture/ProfilePicture.module.scss b/src/libs/shared/lib/components/profile-picture/ProfilePicture.module.scss new file mode 100644 index 000000000..1a42ebd73 --- /dev/null +++ b/src/libs/shared/lib/components/profile-picture/ProfilePicture.module.scss @@ -0,0 +1,60 @@ +@import "@libs/ui/styles/includes"; + +.profilePic { + align-self: flex-start; + flex: 0 0 36%; + + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 50%; + overflow: hidden; + + display: flex; + align-items: center; + justify-content: center; + + position: relative; + z-index: 1; + background: linear-gradient(90deg, #075f96, #038664); + + &::before { + content: ""; + display: block; + position: absolute; + top: -10%; + left: -10%; + z-index: -1; + width: 120%; + height: 120%; + background-image: var(--background-image-url); + background-position: center; + background-size: cover; + filter: blur(9px); + } + + .profileInitials { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + + font-family: $font-barlow; + font-size: 72px; + color: $tc-white; + } + + img { + display: block; + max-width: 100%; + max-height: 100%; + object-fit: cover; + + aspect-ratio: 1 / 1; + border-radius: 50%; + } +} diff --git a/src/libs/shared/lib/components/profile-picture/ProfilePicture.tsx b/src/libs/shared/lib/components/profile-picture/ProfilePicture.tsx new file mode 100644 index 000000000..1036c84e7 --- /dev/null +++ b/src/libs/shared/lib/components/profile-picture/ProfilePicture.tsx @@ -0,0 +1,26 @@ +import { CSSProperties, FC } from 'react' +import classNames from 'classnames' + +import { UserProfile } from '~/libs/core' + +import styles from './ProfilePicture.module.scss' + +interface ProfilePictureProps { + className?: string + member: Pick +} + +const ProfilePicture: FC = props => ( +
+ + {props.member.firstName.slice(0, 1)} + {props.member.lastName.slice(0, 1)} + + +
+) + +export default ProfilePicture diff --git a/src/libs/shared/lib/components/profile-picture/index.ts b/src/libs/shared/lib/components/profile-picture/index.ts new file mode 100644 index 000000000..c08788856 --- /dev/null +++ b/src/libs/shared/lib/components/profile-picture/index.ts @@ -0,0 +1 @@ +export { default as ProfilePicture } from './ProfilePicture' From b35c4cc2f358dfbd9c7f211954a9e84c781612ca Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Jul 2023 11:40:37 +0300 Subject: [PATCH 4/6] talent card fix layout --- .../src/components/talent-card/TalentCard.module.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss b/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss index dc0a8af1e..9607f3b87 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss @@ -23,7 +23,9 @@ .detailsContainer { flex: 1 1 auto; - padding: $sp-5 $sp-45 0 0; + padding: $sp-5 $sp-45 $sp-45 0; + display: flex; + flex-direction: column; } .talentInfo { @@ -31,6 +33,7 @@ flex-direction: column; align-items: flex-start; gap: $sp-3; + margin-bottom: $sp-45; &Name { font-size: 30px; @@ -54,7 +57,7 @@ .matchBar { max-width: 330px; - margin-top: $sp-4; + margin-top: auto; } .skillsContainer { From 7a79ebc0d028c82a34b968338d6b055f1170639f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 25 Jul 2023 11:42:36 +0300 Subject: [PATCH 5/6] Update talent search results page --- .../talent-card/TalentCard.module.scss | 23 +++++- .../src/components/talent-card/TalentCard.tsx | 73 +++++++++++++------ 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss b/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss index 9607f3b87..3d9630ef7 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss @@ -67,19 +67,34 @@ display: flex; flex-direction: column; gap: $sp-2; + position: relative; + + &Title { + position: absolute; + top: 0; + left: 50%; + background: $tc-white; + transform: translate(-50%, -50%); + padding: $sp-2 $sp-4; + color: $black-60; + } + + &:global(.overline) { + text-transform: uppercase; + } } .unmatchedSkills { - width: 100%; display: flex; - color: $black-60; - padding: $sp-2 $sp-3; + color: $black-100; + padding: 0 $sp-2; + font-size: 14px; } .skillsWrap { display: flex; align-items: center; - gap: $sp-2; + gap: $sp-1; flex-wrap: wrap; } diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx index 45897943b..211dae173 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' -import { orderBy } from 'lodash' import { Link } from 'react-router-dom' +import classNames from 'classnames' import codes from 'country-calling-code' import { IconSolid } from '~/libs/ui' @@ -12,6 +12,7 @@ import { Member } from '../../lib/models' import { TALENT_SEARCH_PATHS } from '../../talent-search.routes' import styles from './TalentCard.module.scss' +import { orderBy } from 'lodash' const getCountry = (countryCode: string): string => ( codes.find(c => c.isoCode3 === countryCode || c.isoCode2 === countryCode)?.country ?? countryCode ?? '' @@ -31,15 +32,24 @@ interface TalentCardProps { const TalentCard: FC = props => { const talentRoute = `${TALENT_SEARCH_PATHS.talent}/${props.member.handle}` - const visibleSkills = orderBy( - props.member.emsiSkills.filter(props.isMatchingSkill), - [props.isMatchingSkill, isSkillVerified], - ['desc', 'desc'], + const matchedSkills = orderBy( + props.member.emsiSkills, + isSkillVerified, + 'desc', ) - .slice(0, 6) as EmsiSkill[] + .filter(props.isMatchingSkill) - const hiddenSkills = props.member.emsiSkills.length - visibleSkills.length - const unmatchedLabel = `+${hiddenSkills} more unmatched skill${hiddenSkills > 1 ? 's' : ''}` + const limitMatchedSkills = matchedSkills.slice(0, 10) + + const provenSkills = limitMatchedSkills.filter(isSkillVerified) + const selfSkills = limitMatchedSkills.filter(s => !isSkillVerified(s)) + const restSkills = matchedSkills.length - limitMatchedSkills.length + + const restLabel = restSkills > 0 && ( +
+ {`+${restSkills} more skill${restSkills > 1 ? 's' : ''}`} +
+ ) return ( @@ -73,22 +83,39 @@ const TalentCard: FC = props => {
-
Matched skills
-
- {visibleSkills.map(skill => ( - - ))} - {hiddenSkills > 0 && ( -
- {unmatchedLabel} +
Matched skills
+ {provenSkills.length > 0 && ( + <> +
Proven skills
+
+ {provenSkills.map(skill => ( + + ))} + {!selfSkills.length && restLabel}
- )} -
+ + )} + {selfSkills.length > 0 && ( + <> +
Self-selected skills
+
+ {selfSkills.map(skill => ( + + ))} + {restLabel} +
+ + )}
) From 6a0253b7d247ef5eb933e8149ce7ecb8e1483d1e Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 25 Jul 2023 11:51:57 +0300 Subject: [PATCH 6/6] lint fix --- .../talent-search/src/components/talent-card/TalentCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx index 211dae173..6cf269eba 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx @@ -1,5 +1,6 @@ import { FC } from 'react' import { Link } from 'react-router-dom' +import { orderBy } from 'lodash' import classNames from 'classnames' import codes from 'country-calling-code' @@ -12,7 +13,6 @@ import { Member } from '../../lib/models' import { TALENT_SEARCH_PATHS } from '../../talent-search.routes' import styles from './TalentCard.module.scss' -import { orderBy } from 'lodash' const getCountry = (countryCode: string): string => ( codes.find(c => c.isoCode3 === countryCode || c.isoCode2 === countryCode)?.country ?? countryCode ?? ''