diff --git a/package.json b/package.json index 2ef92fbc4..32742bd9b 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "react-redux": "^8.0.4", "react-redux-toastr": "^7.6.10", "react-responsive": "^9.0.0-beta.5", + "react-responsive-masonry": "^2.1.7", "react-responsive-modal": "^6.2.0", "react-router-dom": "^6.4.2", "react-scripts": "5.0.1", @@ -159,6 +160,7 @@ "@types/react-gtm-module": "^2.0.1", "@types/react-helmet": "^6.1.6", "@types/react-redux-toastr": "^7.6.2", + "@types/react-responsive-masonry": "^2.1.1", "@types/react-router-dom": "^5.3.3", "@types/redux-actions": "2.6.2", "@types/redux-logger": "^3.0.9", diff --git a/src/apps/onboarding/src/models/MemberInfo.ts b/src/apps/onboarding/src/models/MemberInfo.ts index 41e301d5e..34181f86b 100644 --- a/src/apps/onboarding/src/models/MemberInfo.ts +++ b/src/apps/onboarding/src/models/MemberInfo.ts @@ -1,6 +1,5 @@ import { MemberMaxRating } from '~/apps/talent-search/src/lib/models' -import { MemberStats } from '~/libs/core' -import { Skill } from '~/libs/shared' +import { MemberStats, UserSkill } from '~/libs/core' import MemberAddress from './MemberAddress' @@ -14,7 +13,7 @@ export default interface MemberInfo { email: string accountAge: number maxRating: MemberMaxRating - emsiSkills: Array + skills: Array stats: Array addresses?: MemberAddress[] country: string diff --git a/src/apps/profiles/src/member-profile/MemberProfile.context.tsx b/src/apps/profiles/src/member-profile/MemberProfile.context.tsx index a27f799d0..19bcd1325 100644 --- a/src/apps/profiles/src/member-profile/MemberProfile.context.tsx +++ b/src/apps/profiles/src/member-profile/MemberProfile.context.tsx @@ -1,11 +1,11 @@ import { createContext, FC, ReactNode, useContext, useMemo } from 'react' -import { Skill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' export interface MemberProfileContextValue { isTalentSearch?: boolean skillsRenderer?: ( - skills: Pick[] + skills: Pick[] ) => ReactNode } diff --git a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx index 37328a1fc..4631b9678 100644 --- a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx +++ b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx @@ -32,6 +32,8 @@ export function renderLinkIcon(linkName: string): JSX.Element { return case 'Twitter': return + case 'X / Twitter': + return case 'LinkedIn': return case 'Instagram': diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx index a9a35ced3..f6fea3acb 100644 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx +++ b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx @@ -1,10 +1,19 @@ import { trim } from 'lodash' -import { FC, forwardRef, ForwardRefExoticComponent, SVGProps, useEffect, useImperativeHandle, useState } from 'react' +import { + FC, + forwardRef, + ForwardRefExoticComponent, + SVGProps, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react' import classNames from 'classnames' import { Button, IconOutline, InputSelect, InputText } from '~/libs/ui' -import { linkTypes } from '../link-types.config' +import { additionalLinkTypes } from '../link-types.config' import { isValidURL } from '../../../../lib' import { renderLinkIcon } from '../../MemberLinks' @@ -24,7 +33,6 @@ interface LinkFormProps { onRemove?: () => void removeIcon?: FC> hideRemoveIcon?: boolean - allowEmptyUrl?: boolean labelUrlField?: string disabled?: boolean } @@ -41,6 +49,8 @@ const LinkForm: ForwardRefExoticComponent< const [selectedLinkType, setSelectedLinkType] = useState() const [selectedLinkURL, setSelectedLinkURL] = useState() const [shouldValidateForm, setShouldValidateForm] = useState(false) + const canShowTypeError = useRef(false) + const canShowUrlError = useRef(false) useEffect(() => { if (shouldValidateForm) { @@ -52,37 +62,51 @@ const LinkForm: ForwardRefExoticComponent< resetForm() { setShouldValidateForm(false) setFormErrors({}) + canShowTypeError.current = false + canShowUrlError.current = false }, validateForm() { + canShowTypeError.current = true + canShowUrlError.current = true handleFormAction() }, })) function handleSelectedLinkTypeChange(event: React.ChangeEvent): void { + canShowTypeError.current = true setSelectedLinkType(event.target.value) setShouldValidateForm(true) } function handleURLChange(event: React.ChangeEvent): void { + canShowUrlError.current = true setSelectedLinkURL(event.target.value) setShouldValidateForm(true) } - function handleFormAction(): void { + function getFormError(): boolean { setFormErrors({}) + let isError = false if (!selectedLinkType) { - setFormErrors({ selectedLinkType: 'Please select a link type' }) - return + isError = true + if (canShowTypeError.current) { + setFormErrors({ selectedLinkType: 'Please select a link type' }) + } } - if (!props.allowEmptyUrl && !trim(selectedLinkURL)) { - setFormErrors({ url: 'Please enter a URL' }) - return + if (selectedLinkURL && trim(selectedLinkURL) && !isValidURL(selectedLinkURL as string)) { + isError = true + if (canShowUrlError.current) { + setFormErrors({ url: 'Invalid URL' }) + } } - if (selectedLinkURL && !isValidURL(selectedLinkURL as string)) { - setFormErrors({ url: 'Invalid URL' }) + return isError + } + + function handleFormAction(): void { + if (getFormError()) { return } @@ -91,14 +115,14 @@ const LinkForm: ForwardRefExoticComponent< if (absoluteURL.indexOf('://') > 0 || absoluteURL.indexOf('//') === 0) { props.onSave({ - name: selectedLinkType, + name: selectedLinkType ?? '', url: absoluteURL, }) } else { absoluteURL = absoluteURL ? `https://${absoluteURL}` : '' props.onSave({ - name: selectedLinkType, + name: selectedLinkType ?? '', url: absoluteURL, }) } @@ -124,7 +148,7 @@ const LinkForm: ForwardRefExoticComponent<
{props.allowEditType ? ( = (props: ModifyMe const inputRef = useRef() const [isSaving, setIsSaving] = useState(false) - const [hasChanges, setHasChanges] = useState(false) const [currentMemberLinks, setCurrentMemberLinks] = useState( [], ) @@ -44,16 +43,33 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe name: 'Instagram', url: '', }) - const [newLink, setNewLink] = useState({ + const [defaultLink, setDefaultLink] = useState({ name: '', url: '', }) + const updatedLinks = useMemo(() => uniqBy( + [ + defaultLinkedIn, + defaultGitHub, + defaultInstagram, + defaultLink, + ...currentMemberLinks, + ].filter( + l => l.name && l.url, + ), + e => `${e.name}-${e.url}`, + ) + .map( + item => omit(item, ['id']), + ), [defaultLinkedIn, defaultGitHub, defaultInstagram, defaultLink, currentMemberLinks]) + const hasChanges = useMemo(() => !isEqual(updatedLinks, props.memberLinks), [updatedLinks]) + const addNewLinkRef = useRef(null) useEffect(() => { const memberLinks = [ - ...(props.memberLinks ?? []), + ...cloneDeep(props.memberLinks ?? []), ] const firstLinkedInIndex = findIndex(memberLinks, { name: 'LinkedIn', @@ -76,6 +92,10 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe setDefaultInstagram(memberLinks.splice(firstInstagramIndex, 1)[0]) } + if (memberLinks.length > 0) { + setDefaultLink(memberLinks.splice(0, 1)[0]) + } + setCurrentMemberLinks(memberLinks.map((item: UserTrait, index: number) => ({ ...item, id: `id-${index}-${(new Date()) @@ -85,30 +105,16 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe }, [props.memberLinks]) function handleAddAdditional(): void { - if (newLink.url && newLink.name) { - const updatedLinks: UserTrait[] = uniqBy([ - defaultLinkedIn, - defaultGitHub, - defaultInstagram, - ...currentMemberLinks, - ].filter(l => l.name && l.url), e => `${e.name}-${e.url}`) - if (!find(updatedLinks, newLink)) { - setCurrentMemberLinks(links => [...links, { - ...newLink, - id: `id-${(new Date()) - .getTime()}`, - }]) - } - - addNewLinkRef.current?.resetForm() - setNewLink({ - name: '', - url: '', - }) - setHasChanges(true) - } else { - addNewLinkRef.current?.validateForm() - } + setCurrentMemberLinks(links => [...links, { + id: `id-${(new Date()) + .getTime()}`, + ...defaultLink, + }]) + setDefaultLink({ + name: '', + url: '', + }) + addNewLinkRef.current?.resetForm() } function handleRemoveLink(index: number): void { @@ -118,15 +124,12 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe ...currentMemberLinks, ], ) - setHasChanges(true) } function handleSaveLink(link: UserTrait, index: number): void { setCurrentMemberLinks(links => (links ?? []).map((l, i) => ( i === index ? link : l ))) - - setHasChanges(true) } function handleLinksSave(): void { @@ -135,21 +138,6 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe const updatedPersonalizationTraits: UserTrait[] = reject(props.memberPersonalizationTraitsFullData, (trait: UserTrait) => trait.links) - const updatedLinks: UserTrait[] = uniqBy( - [ - defaultLinkedIn, - defaultGitHub, - defaultInstagram, - ...currentMemberLinks, - ].filter( - l => l.name && l.url, - ), - e => `${e.name}-${e.url}`, - ) - .map( - item => omit(item, ['id']), - ) - updateOrCreateMemberTraitsAsync(props.profile.handle, [{ categoryName: UserTraitCategoryNames.personalization, traitId: UserTraitIds.personalization, @@ -204,19 +192,16 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe link={defaultLinkedIn as UserLink} onSave={function onSave(link: UserLink) { setDefaultLinkedIn(link) - setHasChanges(true) }} onRemove={function onRemove() { setDefaultLinkedIn({ ...defaultLinkedIn, url: '', }) - setHasChanges(true) }} placeholder='Add URL' removeIcon={IconOutline.XCircleIcon} hideRemoveIcon={!defaultLinkedIn.url} - allowEmptyUrl disabled={isSaving} labelUrlField='Linkedin' /> @@ -224,19 +209,16 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe link={defaultGitHub as UserLink} onSave={function onSave(link: UserLink) { setDefaultGitHub(link) - setHasChanges(true) }} onRemove={function onRemove() { setDefaultGitHub({ ...defaultGitHub, url: '', }) - setHasChanges(true) }} placeholder='Add URL' removeIcon={IconOutline.XCircleIcon} hideRemoveIcon={!defaultGitHub.url} - allowEmptyUrl disabled={isSaving} labelUrlField='Git' /> @@ -244,19 +226,16 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe link={defaultInstagram as UserLink} onSave={function onSave(link: UserLink) { setDefaultInstagram(link) - setHasChanges(true) }} onRemove={function onRemove() { setDefaultInstagram({ ...defaultInstagram, url: '', }) - setHasChanges(true) }} placeholder='Add URL' removeIcon={IconOutline.XCircleIcon} hideRemoveIcon={!defaultInstagram.url} - allowEmptyUrl disabled={isSaving} labelUrlField='Instagram' /> @@ -265,8 +244,8 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe
button:nth-child(2) { + margin-left: $sp-4; - svg { - margin-left: $sp-1; - width: 16px; - height: 16px; - color: $turq-120; + @include ltemd { + margin-left: 0; } } } } + + .skillsWrap { + display: flex; + flex-wrap: wrap; + column-gap: $sp-1; + row-gap: $sp-2; + } } diff --git a/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx b/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx index cb70e6251..10bd07516 100644 --- a/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx +++ b/src/apps/profiles/src/member-profile/skills/MemberSkillsInfo.tsx @@ -2,8 +2,8 @@ import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'reac import { useSearchParams } from 'react-router-dom' import { orderBy } from 'lodash' -import { UserProfile } from '~/libs/core' -import { ExpandableList, HowSkillsWorkModal, isSkillVerified, Skill, SkillPill } from '~/libs/shared' +import { UserProfile, UserSkill } from '~/libs/core' +import { GroupedSkillsUI, HowSkillsWorkModal, isSkillVerified } from '~/libs/shared' import { Button } from '~/libs/ui' import { AddButton, EditMemberPropertyBtn, EmptySection } from '../../components' @@ -27,11 +27,28 @@ const MemberSkillsInfo: FC = (props: MemberSkillsInfoProp const { skillsRenderer, isTalentSearch }: MemberProfileContextValue = useMemberProfileContext() - const memberSkills: Skill[] = useMemo(() => orderBy( - props.profile.emsiSkills ?? [], + const memberSkills: UserSkill[] = useMemo(() => orderBy( + props.profile.skills ?? [], [isSkillVerified, 'name'], ['desc', 'asc'], - ) as Skill[], [props.profile.emsiSkills]) + ) as UserSkill[], [props.profile.skills]) + + const groupedSkillsByCategory: { [key: string]: UserSkill[] } = useMemo(() => { + const grouped: { [key: string]: UserSkill[] } = {} + + memberSkills.forEach((skill: UserSkill) => { + if (grouped[skill.category.name]) { + grouped[skill.category.name].push(skill) + } else { + grouped[skill.category.name] = [skill] + } + }) + + return grouped + }, [memberSkills]) + + const [skillsCatsCollapsed, setSkillsCatsCollapsed]: [boolean, Dispatch>] + = useState(true) const [isEditMode, setIsEditMode]: [boolean, Dispatch>] = useState(false) @@ -70,8 +87,20 @@ const MemberSkillsInfo: FC = (props: MemberSkillsInfoProp setHowSkillsWorkVisible(false) } + function handleExpandAllClick(): void { + setSkillsCatsCollapsed(!skillsCatsCollapsed) + } + return (
+ { + skillsRenderer && memberSkills.length > 0 && ( +
+ {skillsRenderer(memberSkills)} +
+ ) + } +

Skills

@@ -83,29 +112,32 @@ const MemberSkillsInfo: FC = (props: MemberSkillsInfoProp ) }
-
- {skillsRenderer && memberSkills.length > 0 && skillsRenderer(memberSkills)} - {!skillsRenderer && memberSkills.length > 0 && ( - - { - memberSkills - .map(memberSkill => ( - - )) - } - + {memberSkills.length > 0 && ( + )} {!memberSkills.length && ( = (props: MemberSkillsInfoProp )}
- {canEdit && !memberSkills.length && ( - - )} + { + canEdit && !memberSkills.length && ( + + ) + } { isEditMode && ( diff --git a/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx b/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx index 039cc6a4d..f76593ecf 100644 --- a/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx +++ b/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx @@ -1,6 +1,7 @@ import { FC, useCallback } from 'react' -import { Skill, SkillPill } from '~/libs/shared' +import { SkillPill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' import { SKILL_SEARCH_LIMIT } from '../../config' @@ -8,34 +9,163 @@ import styles from './PopularSkills.module.scss' // TODO: Make this configurable, or read from a service. We need to discuss // how we want to handle this. -const popularSkills: Skill[] = [ - { id: 'f81d2a78-ff52-4c77-8cdb-8863601b87c7', name: 'Java (Programming Language)' }, - { id: '1aabc882-c28d-4b56-8546-5e961b53bf5d', name: 'MySQL' }, - { id: 'b3181231-af8f-4a44-aff2-97fe00c57d76', name: 'Node.js' }, - { id: '4328c534-ba51-4589-a3e7-7b5ba76d2b55', name: 'Cascading Style Sheets (CSS)' }, - { id: 'e3b2b1f1-6bbf-4989-b53d-d8531a10ea5d', name: 'JavaScript (Programming Language)' }, - { id: '41ffc4d5-2e43-45e1-af36-ae7a23b47c21', name: 'Machine Learning' }, - { id: '047203fc-8c85-4be0-be0b-0e2fe11c3a16', name: 'Unit Testing' }, - { id: '8c6703bd-63dd-4f6d-9cf0-5b411e531a9f', name: 'Angular (Web Framework)' }, - { id: '34ec4bf0-0b44-4d04-9f11-e3daa2c045ce', name: '.NET Framework' }, - { id: 'a9bb69aa-edc2-4d5f-8141-de33a139f119', name: 'Python (Programming Language)' }, - { id: '67c623db-09e4-499d-800b-24868b1eb85b', name: 'Android (Operating System)' }, - { id: '36292f61-c359-42a4-89b9-95245ee494ea', name: 'Figma (Design Software)' }, - { id: 'cf39f07c-0e7a-48a2-acec-21834900c437', name: 'Microsoft Azure' }, - { id: 'b33f8342-8015-4244-afea-5fd089bf52a6', name: 'Adobe Illustrator' }, - { id: 'f21aecd2-5c67-4783-97a4-a77c67cf4f67', name: 'Docker (Software)' }, - { id: '43baf79e-3632-4b04-889a-7202cbf62a6c', name: 'React.js' }, +// TODO: update this with the real list of popular skills +const popularSkills: UserSkill[] = [ + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'f81d2a78-ff52-4c77-8cdb-8863601b87c7', + levels: [], + name: 'Java (Programming Language)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '1aabc882-c28d-4b56-8546-5e961b53bf5d', + levels: [], + name: 'MySQL', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'b3181231-af8f-4a44-aff2-97fe00c57d76', + levels: [], + name: 'Node.js', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '4328c534-ba51-4589-a3e7-7b5ba76d2b55', + levels: [], + name: 'Cascading Style Sheets (CSS)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'e3b2b1f1-6bbf-4989-b53d-d8531a10ea5d', + levels: [], + name: 'JavaScript (Programming Language)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '41ffc4d5-2e43-45e1-af36-ae7a23b47c21', + levels: [], + name: 'Machine Learning', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '047203fc-8c85-4be0-be0b-0e2fe11c3a16', + levels: [], + name: 'Unit Testing', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '8c6703bd-63dd-4f6d-9cf0-5b411e531a9f', + levels: [], + name: 'Angular (Web Framework)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '34ec4bf0-0b44-4d04-9f11-e3daa2c045ce', + levels: [], + name: '.NET Framework', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'a9bb69aa-edc2-4d5f-8141-de33a139f119', + levels: [], + name: 'Python (Programming Language)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '67c623db-09e4-499d-800b-24868b1eb85b', + levels: [], + name: 'Android (Operating System)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '36292f61-c359-42a4-89b9-95245ee494ea', + levels: [], + name: 'Figma (Design Software)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'cf39f07c-0e7a-48a2-acec-21834900c437', + levels: [], + name: 'Microsoft Azure', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'b33f8342-8015-4244-afea-5fd089bf52a6', + levels: [], + name: 'Adobe Illustrator', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: 'f21aecd2-5c67-4783-97a4-a77c67cf4f67', + levels: [], + name: 'Docker (Software)', + }, + { + category: { + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + }, + id: '43baf79e-3632-4b04-889a-7202cbf62a6c', + levels: [], + name: 'React.js', + }, ] interface PopularSkillsProps { - onChange: (skills: Skill[]) => void - selectedSkills: Skill[] + onChange: (skills: UserSkill[]) => void + selectedSkills: UserSkill[] } const PopularSkills: FC = props => { - const toggleSkill = useCallback((skill: Skill) => { - let newFilter: Array = [] + const toggleSkill = useCallback((skill: UserSkill) => { + let newFilter: Array = [] let deleted: boolean = false // Either delete the value from the list, if we're toggling one that's already in the list @@ -58,7 +188,7 @@ const PopularSkills: FC = props => { props.onChange.call(undefined, newFilter) }, [props.onChange, props.selectedSkills]) - function isSelected(skill: Skill): boolean { + function isSelected(skill: UserSkill): boolean { return !!props.selectedSkills.find(s => s.id === skill.id) } diff --git a/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.module.scss b/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.module.scss index eaefc285a..d24a1726f 100644 --- a/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.module.scss +++ b/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.module.scss @@ -2,6 +2,11 @@ .wrap { width: 100%; + margin-bottom: $sp-6; + + @include ltemd { + margin-bottom: $sp-2; + } } .highlightWrap { diff --git a/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.tsx b/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.tsx index f18b405d5..642b099da 100644 --- a/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.tsx +++ b/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.tsx @@ -1,7 +1,8 @@ import { FC } from 'react' import classNames from 'classnames' -import { ExpandableList, isSkillVerified, Skill, SkillPill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' +import { isSkillVerified, SkillPill } from '~/libs/shared' import { useIsMatchingSkill } from '../../lib/utils' @@ -9,14 +10,13 @@ import styles from './ProfileSkillsMatch.module.scss' interface ProfileSkillsMatchProps { matchValue: number - profileSkills: Pick[] - queriedSkills: Skill[] + profileSkills: Pick[] + queriedSkills: UserSkill[] } const ProfileSkillsMatch: FC = props => { const isMatchingSkill = useIsMatchingSkill(props.queriedSkills) const matchedSkills = props.profileSkills.filter(isMatchingSkill) - const unMatchedSkills = props.profileSkills.filter(s => !isMatchingSkill(s)) const provenMatched = matchedSkills.filter(isSkillVerified) const selfSkillmatched = matchedSkills.filter(s => !isSkillVerified(s)) const missingSkills = props.queriedSkills.filter(qs => !matchedSkills.find(ms => ms.id === qs.id)) @@ -49,7 +49,7 @@ const ProfileSkillsMatch: FC = props => { <>
{selfSkillmatched.length} - {` matched self selected skill${selfSkillmatched.length > 1 ? 's' : ''}`} + {` matched self proclaimed skill${selfSkillmatched.length > 1 ? 's' : ''}`}
{selfSkillmatched.map(skill => ( @@ -72,21 +72,6 @@ const ProfileSkillsMatch: FC = props => { )}
- {unMatchedSkills.length > 0 && ( -
-
- Additional Skills -
- -
- - {unMatchedSkills.map(skill => ( - - ))} - -
-
- )} ) } 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 e1dcae54e..a44747def 100644 --- a/src/apps/talent-search/src/components/search-input/SearchInput.tsx +++ b/src/apps/talent-search/src/components/search-input/SearchInput.tsx @@ -2,7 +2,8 @@ import { FC, MouseEvent, Ref, useMemo } from 'react' import classNames from 'classnames' import { IconOutline, InputMultiselectOption } from '~/libs/ui' -import { InputSkillSelector, Skill, SkillSources } from '~/libs/shared' +import { InputSkillSelector } from '~/libs/shared' +import { UserSkill } from '~/libs/core' import { SKILL_SEARCH_LIMIT } from '../../config' @@ -11,17 +12,18 @@ import styles from './SearchInput.module.scss' interface SearchInputProps { className?: string readonly autoFocus?: boolean - onChange: (skills: Skill[]) => void - skills: Skill[] + onChange: (skills: Pick[]) => void + skills: UserSkill[] onSearch?: () => void inputRef?: Ref } const SearchInput: FC = props => { - const skills: Skill[] = useMemo(() => props.skills.map(s => ({ + const skills = useMemo(() => props.skills.map(s => ({ + category: s.category, id: s.id, + levels: [], name: s.name, - skillSources: [SkillSources.selfPicked], })), [props.skills]) function onChange(ev: any): void { 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 0ff6a0fee..bf0df7e12 100644 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx +++ b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx @@ -5,7 +5,8 @@ import classNames from 'classnames' import codes from 'country-calling-code' import { IconSolid } from '~/libs/ui' -import { isSkillVerified, ProfilePicture, Skill, SkillPill } from '~/libs/shared' +import { isSkillVerified, ProfilePicture, SkillPill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' import { ProfileMatch } from '../profile-match' import { Member, MemberDisplayName } from '../../lib/models' @@ -30,7 +31,7 @@ function isOverflow(el: HTMLElement): boolean { } interface TalentCardProps { - queriedSkills: Skill[] + queriedSkills: UserSkill[] member: Member match?: number } @@ -44,12 +45,12 @@ const TalentCard: FC = props => { const matchedSkills = useMemo(() => ( orderBy( - props.member.emsiSkills, + props.member.skills, [isSkillVerified, a => a.name], ['desc', 'asc'], ) .filter(isMatchingSkill) - ), [isMatchingSkill, props.member.emsiSkills]) + ), [isMatchingSkill, props.member.skills]) const matchState = useMemo(() => ({ matchValue: props.match, diff --git a/src/apps/talent-search/src/lib/models/Member.ts b/src/apps/talent-search/src/lib/models/Member.ts index 5798a4da3..f1aab22d8 100644 --- a/src/apps/talent-search/src/lib/models/Member.ts +++ b/src/apps/talent-search/src/lib/models/Member.ts @@ -1,4 +1,4 @@ -import { Skill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' import { MemberDisplayName } from './MemberDisplayName' import MemberAddress from './MemberAddress' @@ -13,7 +13,7 @@ export default interface Member { createdAt: number; description: string; email: string; - emsiSkills: Array ; + skills: Array; firstName: string; handle: string; homeCountryCode: string; @@ -24,7 +24,7 @@ export default interface Member { numberOfChallengesWon: number; photoURL: string; skillScore: number; - stats: Array ; + stats: Array; status: string; userId: number; verified: string; diff --git a/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.ts b/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.ts index 4d6f4fe13..348505f4e 100644 --- a/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.ts +++ b/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.ts @@ -3,8 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import useSWR, { SWRResponse } from 'swr' import { EnvironmentConfig } from '~/config' -import { PaginatedResponse, xhrGetPaginatedAsync } from '~/libs/core' -import { Skill } from '~/libs/shared' +import { PaginatedResponse, UserSkill, xhrGetPaginatedAsync } from '~/libs/core' import Member from '@talentSearch/lib/models/Member' export interface TalentMatchesResponse { @@ -18,7 +17,7 @@ export interface TalentMatchesResponse { } export function useFetchTalentMatches( - skills: ReadonlyArray, + skills: ReadonlyArray, page: number, pageSize: number, ): TalentMatchesResponse { @@ -61,7 +60,7 @@ export interface InfiniteTalentMatchesResposne { } export function useInfiniteTalentMatches( - skills: ReadonlyArray, + skills: ReadonlyArray, pageSize: number = 10, ): InfiniteTalentMatchesResposne { const [matches, setMatches] = useState([] as Member[]) diff --git a/src/apps/talent-search/src/lib/utils/search-query.tsx b/src/apps/talent-search/src/lib/utils/search-query.tsx index f93bd9eab..406cafcf9 100644 --- a/src/apps/talent-search/src/lib/utils/search-query.tsx +++ b/src/apps/talent-search/src/lib/utils/search-query.tsx @@ -1,30 +1,32 @@ import { useCallback, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' -import { Skill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' -export const encodeUrlQuerySearch = (skills: Skill[]): string => ( +type PartialUserSkill = Pick + +export const encodeUrlQuerySearch = (skills: PartialUserSkill[]): string => ( skills .map(s => `q=${encodeURIComponent(`${s.name}::${s.id}`)}`) .join('&') ) -export const parseUrlQuerySearch = (params: string[]): Skill[] => ( +export const parseUrlQuerySearch = (params: string[]): UserSkill[] => ( params.map(p => { const [name, id] = p.split('::') - return { id, name } + return { category: { id: '', name: '' }, id, levels: [], name } }) ) export const useUrlQuerySearchParms = (paramName: string): [ - Skill[], - (s: Skill[]) => void + UserSkill[], + (s: PartialUserSkill[]) => void ] => { const [params, updateParams] = useSearchParams() - const [skills, setSkills] = useState([]) + const [skills, setSkills] = useState([]) - const handleUpdateSearch = useCallback((newSkills: Skill[]) => { + const handleUpdateSearch = useCallback((newSkills: PartialUserSkill[]) => { const searchParams = encodeUrlQuerySearch(newSkills) updateParams(`${searchParams}`) }, [updateParams]) diff --git a/src/apps/talent-search/src/lib/utils/skills.utils.tsx b/src/apps/talent-search/src/lib/utils/skills.utils.tsx index 772749cff..8ad98a9c7 100644 --- a/src/apps/talent-search/src/lib/utils/skills.utils.tsx +++ b/src/apps/talent-search/src/lib/utils/skills.utils.tsx @@ -1,11 +1,11 @@ import { useCallback } from 'react' -import { Skill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' -export type IsMatchingSkillFn = (skill: Pick) => boolean +export type IsMatchingSkillFn = (skill: Pick) => boolean -export const useIsMatchingSkill = (skills: Skill[]): IsMatchingSkillFn => { - const isMatchingSkill = useCallback((skill: Pick) => ( +export const useIsMatchingSkill = (skills: UserSkill[]): IsMatchingSkillFn => { + const isMatchingSkill = useCallback((skill: Pick) => ( !!skills.find(s => skill.id === s.id) ), [skills]) 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 501a1e4a6..ba8c8c4e9 100644 --- a/src/apps/talent-search/src/routes/search-page/SearchPage.tsx +++ b/src/apps/talent-search/src/routes/search-page/SearchPage.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useRef, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { ContentLayout, IconOutline } from '~/libs/ui' -import { Skill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' import { SearchInput } from '../../components/search-input' import { PopularSkills } from '../../components/popular-skills' @@ -20,15 +20,15 @@ export const SearchPage: FC = () => { const searchInputRef = useRef() const navigate = useNavigate() - const [skillsFilter, setSkillsFilter] = useState([]) + const [skillsFilter, setSkillsFilter] = useState([]) function navigateToResults(): void { const searchParams = encodeUrlQuerySearch(skillsFilter) navigate(`${TALENT_SEARCH_PATHS.results}?${searchParams}`) } - function handleSelectSkillFilter(filter: Skill[]): void { - setSkillsFilter(filter) + function handleSelectSkillFilter(filter: Pick[]): void { + setSkillsFilter(filter as UserSkill[]) searchInputRef.current?.focus() } @@ -81,7 +81,7 @@ export const SearchPage: FC = () => { diff --git a/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx b/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx index 4bd1d2eb6..d93dae09f 100644 --- a/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx +++ b/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx @@ -2,14 +2,14 @@ import { FC, ReactNode } from 'react' import { Location, useLocation } from 'react-router-dom' import { MemberProfileContext, MemberProfilePage } from '@profiles/member-profile' -import { Skill } from '~/libs/shared' +import { UserSkill } from '~/libs/core' import { ProfileSkillsMatch } from '../../components/profile-skills-match' const TalentPage: FC = () => { const { state }: Location = useLocation() - function skillsRenderer(profileSkills: Pick[]): ReactNode { + function skillsRenderer(profileSkills: Pick[]): ReactNode { return ( firstName: string handle: string handleLower: string @@ -29,6 +28,7 @@ export interface UserProfile { } photoURL?: string roles: Array + skills: Array status: string tracks?: Array updatedAt: number diff --git a/src/libs/core/lib/profile/user-skill.model.ts b/src/libs/core/lib/profile/user-skill.model.ts index 9d454b519..0fa5f6e67 100644 --- a/src/libs/core/lib/profile/user-skill.model.ts +++ b/src/libs/core/lib/profile/user-skill.model.ts @@ -1,20 +1,24 @@ -export enum SkillSources { - selfPicked = 'SelfPicked', - challengeWin = 'ChallengeWin', - tcaCertified = 'TCACertified', +export type UserSkillCategory = { + id: string + name: string +} + +// keep this in sync with the backend +// https://github.com/topcoder-platform/standardized-skills-api/blob/develop/src/config/constants.ts#L23 +export enum UserSkillLevelTypes { + selfDeclared = 'self-declared', + verified = 'verified', +} + +export type UserSkillLevel = { + id: string + name: UserSkillLevelTypes + description: string } export type UserSkill = { id: string name: string - skillCategory?: { - name: string - id: number - } - skillId?: string - skillSources?: SkillSources[] - skillSubcategory?: { - name: string - id: number - } + category: UserSkillCategory + levels: Array } diff --git a/src/libs/shared/lib/components/collapsible-skills-list/CollapsibleSkillsList.module.scss b/src/libs/shared/lib/components/collapsible-skills-list/CollapsibleSkillsList.module.scss new file mode 100644 index 000000000..b5bacf1c0 --- /dev/null +++ b/src/libs/shared/lib/components/collapsible-skills-list/CollapsibleSkillsList.module.scss @@ -0,0 +1,68 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; + background-color: $tc-white; + border-radius: 8px; + margin-bottom: $sp-4; + + &.collapsed { + .title { + font-weight: 400 !important; + } + + .content { + max-height: 0; + overflow: hidden; + padding: 0; + margin-top: 0; + opacity: 0; + } + } + + .header { + display: flex; + justify-content: flex-start; + align-items: flex-start; + cursor: pointer; + + .title { + font-weight: 700; + font-size: 14px; + } + + .btn { + padding-left: 0; + padding-right: $sp-2; + color: $turq-120; + + svg { + flex: 0 0 auto; + color: $turq-120; + @include icon-size(16); + } + } + + .badgeCount { + background-color: $black-10; + border-radius: 50%; + padding: 0 $sp-2; + font-weight: 700; + font-size: 14px; + margin-left: $sp-2; + } + } + + .content { + transition: opacity 0.3s ease-in-out; + max-height: auto; + opacity: 1; + padding-left: $sp-8; + margin-top: $sp-1; + + @include ltemd { + padding-left: $sp-4; + } + } +} \ No newline at end of file diff --git a/src/libs/shared/lib/components/collapsible-skills-list/CollapsibleSkillsList.tsx b/src/libs/shared/lib/components/collapsible-skills-list/CollapsibleSkillsList.tsx new file mode 100644 index 000000000..e51149905 --- /dev/null +++ b/src/libs/shared/lib/components/collapsible-skills-list/CollapsibleSkillsList.tsx @@ -0,0 +1,51 @@ +import { Dispatch, FC, ReactNode, SetStateAction, useEffect, useState } from 'react' +import classNames from 'classnames' + +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline' +import { Button } from '~/libs/ui' + +import styles from './CollapsibleSkillsList.module.scss' + +interface CollapsibleSkillsListProps { + children?: ReactNode[] + header?: ReactNode + containerClass?: string + headerClass?: string + contentClass?: string + isCollapsed: boolean +} + +const CollapsibleSkillsList: FC = (props: CollapsibleSkillsListProps) => { + const [isCollapsed, setIsCollapsed]: [boolean, Dispatch>] + = useState(props.isCollapsed) + + useEffect(() => { + setIsCollapsed(props.isCollapsed) + }, [props.isCollapsed]) + + function toggleCollapse(): void { + setIsCollapsed(!isCollapsed) + } + + return ( +
+
+ { + !isCollapsed + ?
+ +
+ {props.children} +
+
+ ) +} + +export default CollapsibleSkillsList diff --git a/src/libs/shared/lib/components/collapsible-skills-list/index.ts b/src/libs/shared/lib/components/collapsible-skills-list/index.ts new file mode 100644 index 000000000..8e5ba427d --- /dev/null +++ b/src/libs/shared/lib/components/collapsible-skills-list/index.ts @@ -0,0 +1 @@ +export { default as CollapsibleSkillsList } from './CollapsibleSkillsList' diff --git a/src/libs/shared/lib/components/grouped-skills-ui/GroupedSkillsUI.module.scss b/src/libs/shared/lib/components/grouped-skills-ui/GroupedSkillsUI.module.scss new file mode 100644 index 000000000..ccf913e1b --- /dev/null +++ b/src/libs/shared/lib/components/grouped-skills-ui/GroupedSkillsUI.module.scss @@ -0,0 +1,5 @@ +@import '@libs/ui/styles/includes'; + +.skillsCategories { + flex: 1; +} \ No newline at end of file diff --git a/src/libs/shared/lib/components/grouped-skills-ui/GroupedSkillsUI.tsx b/src/libs/shared/lib/components/grouped-skills-ui/GroupedSkillsUI.tsx new file mode 100644 index 000000000..e9a6d87bf --- /dev/null +++ b/src/libs/shared/lib/components/grouped-skills-ui/GroupedSkillsUI.tsx @@ -0,0 +1,47 @@ +import { FC } from 'react' +import Masonry, { ResponsiveMasonry } from 'react-responsive-masonry' + +import { UserSkill } from '~/libs/core' +import { SkillPill } from '~/libs/shared' + +import { CollapsibleSkillsList } from '../collapsible-skills-list' + +import styles from './GroupedSkillsUI.module.scss' + +interface GroupedSkillsUIProps { + groupedSkillsByCategory: { [key: string]: UserSkill[] } + skillsCatsCollapsed: boolean +} +const GroupedSkillsUI: FC = (props: GroupedSkillsUIProps) => ( + + + { + Object.keys(props.groupedSkillsByCategory) + .map((categoryName: string) => ( + + + { + props.groupedSkillsByCategory[categoryName] + .map((skill: UserSkill) => ( + + )) + } + + )) + } + + +) + +export default GroupedSkillsUI diff --git a/src/libs/shared/lib/components/grouped-skills-ui/index.ts b/src/libs/shared/lib/components/grouped-skills-ui/index.ts new file mode 100644 index 000000000..700ccb085 --- /dev/null +++ b/src/libs/shared/lib/components/grouped-skills-ui/index.ts @@ -0,0 +1 @@ +export { default as GroupedSkillsUI } from './GroupedSkillsUI' diff --git a/src/libs/shared/lib/components/index.ts b/src/libs/shared/lib/components/index.ts index 82cde8c0f..016d36443 100644 --- a/src/libs/shared/lib/components/index.ts +++ b/src/libs/shared/lib/components/index.ts @@ -5,3 +5,5 @@ export * from './input-skill-selector' export * from './member-skill-editor' export * from './skill-pill' export * from './expandable-list' +export * from './grouped-skills-ui' +export * from './collapsible-skills-list' 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 a9683575d..6883ab2b9 100644 --- a/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx +++ b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx @@ -2,10 +2,11 @@ import { ChangeEvent, FC, ReactNode, Ref } from 'react' import { noop } from 'lodash' import { InputMultiselect, InputMultiselectOption, InputMultiselectThemes } from '~/libs/ui' +import { UserSkill } from '~/libs/core' -import { autoCompleteSkills, isSkillVerified, Skill } from '../../services/standard-skills' +import { autoCompleteSkills, isSkillVerified } from '../../services/standard-skills' -const mapSkillToInputOption = (skill: Skill): InputMultiselectOption => ({ +const mapSkillToInputOption = (skill: UserSkill): InputMultiselectOption => ({ ...skill, label: skill.name, value: skill.id, @@ -35,7 +36,7 @@ interface InputSkillSelectorProps { readonly loading?: boolean readonly onChange?: (event: ChangeEvent) => void readonly placeholder?: string - readonly value?: Skill[] + readonly value?: UserSkill[] readonly theme?: InputMultiselectThemes readonly useWrapper?: boolean readonly dropdownIcon?: ReactNode diff --git a/src/libs/shared/lib/components/member-skill-editor/use-member-skill-editor.tsx b/src/libs/shared/lib/components/member-skill-editor/use-member-skill-editor.tsx index 613683028..1f49c4e5f 100644 --- a/src/libs/shared/lib/components/member-skill-editor/use-member-skill-editor.tsx +++ b/src/libs/shared/lib/components/member-skill-editor/use-member-skill-editor.tsx @@ -1,14 +1,12 @@ import { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { differenceWith } from 'lodash' -import { profileContext, ProfileContextData } from '~/libs/core' +import { profileContext, ProfileContextData, UserSkill } from '~/libs/core' import { createMemberSkills, fetchMemberSkills, isSkillVerified, - Skill, - SkillSources, updateMemberSkills, } from '../../services/standard-skills' import { InputSkillSelector } from '../input-skill-selector' @@ -37,7 +35,7 @@ export const useMemberSkillEditor = ({ }: {limit?: number} = {}): MemberSkillEditor => { const { profile }: ProfileContextData = useContext(profileContext) const [isInitialized, setIsInitialized] = useState(false) - const [skills, setSkills] = useState([]) + const [skills, setSkills] = useState([]) const [loading, setLoading] = useState(true) const [, setError] = useState() @@ -47,14 +45,13 @@ export const useMemberSkillEditor = ({ return } - const memberSkills = skills.map(s => ({ id: s.id, name: s.name, sources: s.skillSources })) if (!isInitialized) { - await createMemberSkills(profile.userId, memberSkills) + await createMemberSkills(profile.userId, skills) setIsInitialized(true) return } - await updateMemberSkills(profile.userId, memberSkills) + await updateMemberSkills(profile.userId, skills) }, [isInitialized, profile?.userId, skills]) // Handle user changes @@ -78,9 +75,10 @@ export const useMemberSkillEditor = ({ } setSkills([...skills, { + category: skillData.category, id: skillData.value, + levels: [], name: skillData.label, - skillSources: [SkillSources.selfPicked], }]) }, [skills]) diff --git a/src/libs/shared/lib/components/skill-pill/SkillPill.module.scss b/src/libs/shared/lib/components/skill-pill/SkillPill.module.scss index 6e38e1c8c..9740c7b27 100644 --- a/src/libs/shared/lib/components/skill-pill/SkillPill.module.scss +++ b/src/libs/shared/lib/components/skill-pill/SkillPill.module.scss @@ -61,4 +61,11 @@ border-color: $black-20; color: $black-60; } + + &-catList { + border: none; + color: $black-100; + padding: $sp-2 0; + align-items: flex-start; + } } diff --git a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx index 495e2a0dd..725b06373 100644 --- a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx +++ b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx @@ -2,32 +2,34 @@ import { FC, ReactNode, useCallback, useMemo } from 'react' import classNames from 'classnames' import { IconOutline } from '~/libs/ui' +import { UserSkill } from '~/libs/core' -import { isSkillVerified, Skill } from '../../services' +import { isSkillVerified } from '../../services' import styles from './SkillPill.module.scss' export interface SkillPillProps { children?: ReactNode - onClick?: (skill: Skill) => void + onClick?: (skill: UserSkill) => void selected?: boolean - skill: Pick - theme?: 'dark' | 'verified' | 'presentation' | 'etc' + skill: Pick + theme?: 'dark' | 'verified' | 'presentation' | 'etc' | 'catList' } const SkillPill: FC = props => { const isVerified = useMemo(() => ( - isSkillVerified({ skillSources: props.skill.skillSources ?? [] }) + isSkillVerified({ levels: props.skill.levels ?? [] }) ), [props.skill]) const className = classNames( styles.pill, props.onClick && styles.interactive, props.selected && styles.selected, - styles[`theme-${isVerified ? 'verified' : (props.theme ?? 'presentation')}`], + styles[`theme-${isVerified ? 'verified' : ''}`], + styles[`theme-${props.theme ?? ''}`], ) - const handleClick = useCallback(() => props.onClick?.call(undefined, props.skill as Skill), [ + const handleClick = useCallback(() => props.onClick?.call(undefined, props.skill as UserSkill), [ props.onClick, props.skill, ]) diff --git a/src/libs/shared/lib/services/standard-skills/index.ts b/src/libs/shared/lib/services/standard-skills/index.ts index 97cfcafe3..95a09a932 100644 --- a/src/libs/shared/lib/services/standard-skills/index.ts +++ b/src/libs/shared/lib/services/standard-skills/index.ts @@ -1,3 +1,2 @@ -export * from './skill.model' export * from './standard-skills.service' export * from './skills.utils' diff --git a/src/libs/shared/lib/services/standard-skills/skill.model.ts b/src/libs/shared/lib/services/standard-skills/skill.model.ts deleted file mode 100644 index e70eba5e1..000000000 --- a/src/libs/shared/lib/services/standard-skills/skill.model.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SkillSources } from '~/libs/core' -export type { UserSkill as Skill } from '~/libs/core' diff --git a/src/libs/shared/lib/services/standard-skills/skills.utils.ts b/src/libs/shared/lib/services/standard-skills/skills.utils.ts index f64e15054..97d518f1b 100644 --- a/src/libs/shared/lib/services/standard-skills/skills.utils.ts +++ b/src/libs/shared/lib/services/standard-skills/skills.utils.ts @@ -1,5 +1,7 @@ -import { Skill, SkillSources } from './skill.model' +import { find } from 'lodash' -export const isSkillVerified = (skill: Pick): boolean => ( - !!skill.skillSources?.includes(SkillSources.challengeWin) +import { UserSkill, UserSkillLevelTypes } from '~/libs/core' + +export const isSkillVerified = (skill: Pick): boolean => ( + !!find(skill.levels, { name: UserSkillLevelTypes.verified }) ) diff --git a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts index a1bf91e85..f0acd5ea6 100644 --- a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts +++ b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts @@ -1,11 +1,9 @@ import { EnvironmentConfig } from '~/config' -import { xhrGetAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' - -import { Skill } from './skill.model' +import { UserSkill, xhrGetAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' const baseUrl = `${EnvironmentConfig.API.V5}/emsi-skills/member-emsi-skills` -export async function autoCompleteSkills(queryTerm: string): Promise { +export async function autoCompleteSkills(queryTerm: string): Promise { if (!queryTerm) { return Promise.resolve([]) } @@ -20,19 +18,19 @@ export type FetchMemberSkillsConfig = { export async function fetchMemberSkills( userId: string | number | undefined, config: FetchMemberSkillsConfig, -): Promise { +): Promise { const url = `${baseUrl}/${userId}?pageFlag=${!config.skipPagination}` return xhrGetAsync(url) } -export async function createMemberSkills(userId: number, skills: Skill[]): Promise { +export async function createMemberSkills(userId: number, skills: UserSkill[]): Promise { return xhrPostAsync(baseUrl, { skills, userId, }) } -export async function updateMemberSkills(userId: string | number, skills: Skill[]): Promise { +export async function updateMemberSkills(userId: string | number, skills: UserSkill[]): Promise { return xhrPutAsync(`${baseUrl}/${userId}`, { skills, }) diff --git a/yarn.lock b/yarn.lock index 8954e2c9d..6a6a6f881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5402,6 +5402,13 @@ "@types/react" "*" redux "^3.6.0 || ^4.0.0" +"@types/react-responsive-masonry@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/react-responsive-masonry/-/react-responsive-masonry-2.1.1.tgz#694fc86964f37b991c3b85ac99a253a04ecb8f6b" + integrity sha512-sFzNsxYugWO8G4d3ObhpcJCiqiYthWp9Udr9phDzy5BYxW78uMF8yK2IJ8+7bQLbmQz2xaIey1ZogbW3ATIo+A== + dependencies: + "@types/react" "*" + "@types/react-router-dom@^5.3.3": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" @@ -16328,6 +16335,11 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-responsive-masonry@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/react-responsive-masonry/-/react-responsive-masonry-2.1.7.tgz#9b4a8d63b296a6265bea720c9d7da89754548346" + integrity sha512-/jOqnTVNtO8zRJMTl6ZXBex7EFYEIFBLVlkIFjiCT7omfm0CgNF2z4ST8RIr0v2ut15NWAQs7Q3kJJPhMoe3Ew== + react-responsive-modal@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/react-responsive-modal/-/react-responsive-modal-6.2.0.tgz#339226b911d8ffaba4e0afce83387a65221b8e29"