diff --git a/craco.config.js b/craco.config.js index bdc7fa39f..1ff13b064 100644 --- a/craco.config.js +++ b/craco.config.js @@ -41,7 +41,6 @@ module.exports = { '@learn': resolve('src/apps/learn/src'), '@devCenter': resolve('src/apps/dev-center/src'), '@gamificationAdmin': resolve('src/apps/gamification-admin/src'), - '@talentSearch': resolve('src/apps/talent-search/src'), '@profiles': resolve('src/apps/profiles/src'), '@wallet': resolve('src/apps/wallet/src'), '@walletAdmin': resolve('src/apps/wallet-admin/src'), diff --git a/src/apps/customer-portal/src/config/routes.config.ts b/src/apps/customer-portal/src/config/routes.config.ts index 316357915..6cf5ffb2a 100644 --- a/src/apps/customer-portal/src/config/routes.config.ts +++ b/src/apps/customer-portal/src/config/routes.config.ts @@ -8,4 +8,4 @@ export const rootRoute: string ? '' : `/${AppSubdomain.customer}` -export const talentSearchRouteId = 'talent-search' +export const profileCompletionRouteId = 'profile-completion' diff --git a/src/apps/customer-portal/src/customer-portal.routes.tsx b/src/apps/customer-portal/src/customer-portal.routes.tsx index 2ee282745..f4f44f577 100644 --- a/src/apps/customer-portal/src/customer-portal.routes.tsx +++ b/src/apps/customer-portal/src/customer-portal.routes.tsx @@ -6,15 +6,12 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute, - Rewrite, UserRole, } from '~/libs/core' import { rootRoute, - talentSearchRouteId, } from './config/routes.config' -import { customerPortalTalentSearchRoutes } from './pages/talent-search/talent-search.routes' const CustomerPortalApp: LazyLoadedComponent = lazyLoad(() => import('./CustomerPortalApp')) @@ -24,14 +21,6 @@ export const customerPortalRoutes: ReadonlyArray = [ // Customer portal App Root { authRequired: true, - children: [ - { - authRequired: true, - element: , - route: '', - }, - ...customerPortalTalentSearchRoutes, - ], domain: AppSubdomain.customer, element: , id: toolTitle, diff --git a/src/apps/customer-portal/src/lib/components/NavTabs/NavTabs.tsx b/src/apps/customer-portal/src/lib/components/NavTabs/NavTabs.tsx index 5e87e8ea8..278c22d81 100644 --- a/src/apps/customer-portal/src/lib/components/NavTabs/NavTabs.tsx +++ b/src/apps/customer-portal/src/lib/components/NavTabs/NavTabs.tsx @@ -4,23 +4,16 @@ import { MouseEvent, SetStateAction, useCallback, - useContext, useEffect, - useMemo, useRef, useState, } from 'react' import { NavigateFunction, useLocation, useNavigate } from 'react-router-dom' -import { isEmpty } from 'lodash' import classNames from 'classnames' import { useClickOutside } from '~/libs/shared/lib/hooks' import { IconOutline } from '~/libs/ui' -import { CustomerPortalAppContext } from '../../contexts' -import { CustomerPortalAppContextModel } from '../../models' -import { PRIVILEGED_ROLES } from '../../../config/index.config' - import { getTabIdFromPathName, getTabsConfig } from './config' import styles from './NavTabs.module.scss' @@ -30,23 +23,8 @@ const NavTabs: FC = () => { const triggerRef = useRef(null) const { pathname }: { pathname: string } = useLocation() - const { loginUserInfo }: CustomerPortalAppContextModel = useContext(CustomerPortalAppContext) - const isAnonymous = isEmpty(loginUserInfo) - const userRoles = useMemo(() => loginUserInfo?.roles || [], [loginUserInfo?.roles]) - const isUnprivilegedUser = useMemo(() => { - if (!loginUserInfo) return true - - return !userRoles.some(role => PRIVILEGED_ROLES.includes(role)) - }, [loginUserInfo, userRoles]) - const tabs = useMemo( - () => getTabsConfig(userRoles, isAnonymous, isUnprivilegedUser), - [userRoles, isAnonymous, isUnprivilegedUser], - ) - - const activeTabPathName: string = useMemo( - () => getTabIdFromPathName(pathname, userRoles, isAnonymous, isUnprivilegedUser), - [pathname, userRoles, isAnonymous, isUnprivilegedUser], - ) + const tabs = getTabsConfig() + const activeTabPathName: string = getTabIdFromPathName(pathname) const [activeTab, setActiveTab]: [ string, Dispatch> @@ -54,7 +32,7 @@ const NavTabs: FC = () => { useEffect(() => { setActiveTab(activeTabPathName) - }, [activeTabPathName]) + }, [pathname]) const triggerTab = useCallback(() => { setIsOpen(!isOpen) diff --git a/src/apps/customer-portal/src/lib/components/NavTabs/config/tabs-config.ts b/src/apps/customer-portal/src/lib/components/NavTabs/config/tabs-config.ts index c76f23ed4..810e4db43 100644 --- a/src/apps/customer-portal/src/lib/components/NavTabs/config/tabs-config.ts +++ b/src/apps/customer-portal/src/lib/components/NavTabs/config/tabs-config.ts @@ -1,33 +1,15 @@ import _ from 'lodash' import { TabsNavItem } from '~/libs/ui' -import { - talentSearchRouteId, -} from '~/apps/customer-portal/src/config/routes.config' -export function getTabsConfig(userRoles: string[], isAnonymous: boolean, isUnprivilegedUser: boolean): TabsNavItem[] { - - const tabs: TabsNavItem[] = [ - ...(!isUnprivilegedUser ? [{ - id: talentSearchRouteId, - title: 'Talent Search', - }] : []), - ] - - return tabs +export function getTabsConfig(): TabsNavItem[] { + return [] } export function getTabIdFromPathName( pathname: string, - userRoles: string[], - isAnonymous: boolean, - isUnprivilegedUser: boolean, ): string { - const matchItem = _.find(getTabsConfig( - userRoles, - isAnonymous, - isUnprivilegedUser, - ), item => pathname.includes(`/${item.id}`)) + const matchItem = _.find(getTabsConfig(), item => pathname.includes(`/${item.id}`)) if (matchItem) { return matchItem.id diff --git a/src/apps/customer-portal/src/lib/services/profileCompletion.service.ts b/src/apps/customer-portal/src/lib/services/profileCompletion.service.ts new file mode 100644 index 000000000..f0674b639 --- /dev/null +++ b/src/apps/customer-portal/src/lib/services/profileCompletion.service.ts @@ -0,0 +1,142 @@ +import { EnvironmentConfig } from '~/config' +import { UserSkill, xhrGetAsync } from '~/libs/core' + +export type CompletedProfile = { + countryCode?: string + countryName?: string + city?: string + firstName?: string + handle: string + lastName?: string + photoURL?: string + skillCount?: number + userId?: number | string + isOpenToWork?: boolean | null + openToWork?: { + availability?: string + preferredRoles?: string[] + } | null +} + +export type CompletedProfilesResponse = { + data: CompletedProfile[] + page: number + perPage: number + total: number + totalPages: number +} + +export const DEFAULT_PAGE_SIZE = 50 + +function normalizeToList(raw: any): any[] { + if (Array.isArray(raw)) { + return raw + } + + if (Array.isArray(raw?.data)) { + return raw.data + } + + if (Array.isArray(raw?.result?.content)) { + return raw.result.content + } + + if (Array.isArray(raw?.result)) { + return raw.result + } + + return [] +} + +function normalizeCompletedProfilesResponse( + raw: any, + fallbackPage: number, + fallbackPerPage: number, +): CompletedProfilesResponse { + if (raw && Array.isArray(raw.data)) { + const total: number = Number(raw.total ?? raw.data.length) + const perPage: number = Number(raw.perPage ?? fallbackPerPage) + const page: number = Number(raw.page ?? fallbackPage) + const safePerPage = Number.isFinite(perPage) ? Math.max(perPage, 1) : fallbackPerPage + const safeTotal = Number.isFinite(total) ? Math.max(total, 0) : raw.data.length + + return { + data: raw.data, + page: Number.isFinite(page) ? Math.max(page, 1) : fallbackPage, + perPage: safePerPage, + total: safeTotal, + totalPages: Number.isFinite(raw.totalPages) + ? Math.max(Number(raw.totalPages), 1) + : Math.max(Math.ceil(safeTotal / safePerPage), 1), + } + } + + const rows = normalizeToList(raw) + const total = Number(raw?.total ?? rows.length) + const safeTotal = Number.isFinite(total) ? Math.max(total, 0) : rows.length + + return { + data: rows, + page: fallbackPage, + perPage: fallbackPerPage, + total: safeTotal, + totalPages: Math.max(Math.ceil(safeTotal / fallbackPerPage), 1), + } +} + +export type OpenToWorkFilter = 'all' | 'yes' | 'no' + +export async function fetchCompletedProfiles( + countryCode: string | undefined, + page: number, + perPage: number, + openToWorkFilter?: OpenToWorkFilter, + skillIds?: string[], +): Promise { + const queryParams = new URLSearchParams({ + page: String(page), + perPage: String(perPage), + }) + + if (countryCode) { + queryParams.set('countryCode', countryCode) + } + + if (openToWorkFilter === 'yes') { + queryParams.set('openToWork', 'true') + } + + if (openToWorkFilter === 'no') { + queryParams.set('openToWork', 'false') + } + + if (Array.isArray(skillIds) && skillIds.length > 0) { + skillIds.forEach(id => { + if (id) { + queryParams.append('skillId', String(id)) + } + }) + } + + const response = await xhrGetAsync( + `${EnvironmentConfig.REPORTS_API}/topcoder/completed-profiles?${queryParams.toString()}`, + ) + + return normalizeCompletedProfilesResponse(response, page, perPage) +} + +export async function fetchMemberSkillsData(userId: string | number | undefined): Promise { + if (!userId) { + return [] + } + + const baseUrl = `${EnvironmentConfig.API.V5}/standardized-skills` + const url = `${baseUrl}/user-skills/${userId}?disablePagination=true` + + try { + return await xhrGetAsync(url) + } catch { + // If skills API fails, return empty array to not block the page + return [] + } +} diff --git a/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/ProfileCompletionPage.module.scss b/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/ProfileCompletionPage.module.scss new file mode 100644 index 000000000..ec7051428 --- /dev/null +++ b/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/ProfileCompletionPage.module.scss @@ -0,0 +1,218 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; + gap: $sp-4; +} + +.headerRow { + display: flex; + align-items: flex-end; + gap: $sp-4; + justify-content: space-between; + + @include ltemd { + flex-direction: column; + align-items: stretch; + } +} + +.filterWrapper { + display: flex; + gap: $sp-4; + + :global([class*='__value-container']) { + min-height: 18px; + } + + @include ltemd { + flex-direction: column; + align-items: stretch; + } + + .filterWrap { + min-width: 280px; + max-width: 360px; + + @include ltemd { + max-width: unset; + min-width: unset; + width: 100%; + } + } +} + +.counterCard { + border: 1px solid $black-20; + border-radius: $sp-2; + background: $tc-white; + padding: $sp-4; + min-width: 260px; + display: flex; + flex-direction: column; + gap: $sp-1; +} + +.counterLabel { + color: $black-60; + font-size: 12px; + line-height: 16px; + font-weight: 600; + text-transform: uppercase; +} + +.counterValue { + color: $black-100; + font-size: 32px; + line-height: 36px; + font-weight: 700; + font-family: 'Nunito Sans', sans-serif; +} + +.loadingWrap { + position: relative; + height: 90px; + + .spinner { + background: none; + } +} + +.errorMessage { + color: $red-100; + font-size: 14px; + line-height: 20px; + font-weight: 700; +} + +.emptyMessage { + color: $black-60; + font-size: 14px; + line-height: 20px; +} + +.tableWrap { + overflow: auto; + border: 1px solid $black-20; + border-radius: $sp-2; + + table { + width: 100%; + border-collapse: collapse; + min-width: 1120px; + } + + th, + td { + text-align: left; + padding: $sp-3 $sp-4; + border-bottom: 1px solid $black-20; + font-size: 14px; + line-height: 20px; + } + + th { + color: $black-100; + font-weight: 700; + background: $black-5; + } + + td { + color: $black-100; + vertical-align: middle; + } + + tr:last-child td { + border-bottom: 0; + } +} + +.memberCell { + display: flex; + align-items: center; + gap: $sp-2; +} + +.avatar { + width: 28px; + height: 28px; + border-radius: 50%; + object-fit: cover; + border: 1px solid $black-20; +} + +.paginationRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: $sp-3; + + @include ltemd { + flex-direction: column; + align-items: flex-start; + } +} + +.paginationInfo { + color: $black-60; + font-size: 14px; + line-height: 20px; +} + +.paginationButtons { + display: flex; + align-items: center; + gap: $sp-2; +} + +.skillsList { + display: flex; + flex-wrap: wrap; + gap: $sp-2; +} + +.skillTag { + display: inline-block; + background: $black-5; + border: 1px solid $black-20; + border-radius: $sp-1; + padding: $sp-1 $sp-2; + font-size: 12px; + line-height: 16px; + color: $black-80; + white-space: nowrap; +} + +.moreIndicator { + display: inline-block; + background: $black-5; + border: 1px solid $black-20; + border-radius: $sp-1; + padding: $sp-1 $sp-2; + font-size: 12px; + line-height: 16px; + color: $black-80; + font-weight: 700; + min-width: 24px; + text-align: center; + cursor: help; +} + +.link { + display: flex; + gap: $sp-1; + text-decoration: underline; + color: $link-blue; + cursor: pointer; +} + +.openToWorkYes { + color: $green-100; + font-weight: 600; +} + +.openToWorkNo { + color: $red-100; + font-weight: 600; +} diff --git a/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/ProfileCompletionPage.tsx b/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/ProfileCompletionPage.tsx new file mode 100644 index 000000000..0434e1826 --- /dev/null +++ b/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/ProfileCompletionPage.tsx @@ -0,0 +1,409 @@ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable complexity */ +import { ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react' +import useSWR, { SWRResponse } from 'swr' + +import { EnvironmentConfig } from '~/config' +import { CountryLookup, useCountryLookup, UserSkill, UserSkillDisplayModes } from '~/libs/core' +import { + Button, + InputMultiselect, + InputMultiselectOption, + InputSelect, + InputSelectOption, + LoadingSpinner, + Tooltip, +} from '~/libs/ui' +import { fetchSkillAutocompleteOptions } from '~/libs/shared' +import { getPreferredRoleLabelByValue } from '~/libs/shared/lib/utils/roles' + +import { PageWrapper } from '../../../lib' +import { + CompletedProfilesResponse, + DEFAULT_PAGE_SIZE, + fetchCompletedProfiles, + fetchMemberSkillsData, + type OpenToWorkFilter, +} from '../../../lib/services/profileCompletion.service' + +import styles from './ProfileCompletionPage.module.scss' + +const DISPLAY_SKILLS_COUNT = 5 + +export const ProfileCompletionPage: FC = () => { + const [selectedCountry, setSelectedCountry] = useState('all') + const [currentPage, setCurrentPage] = useState(1) + const [selectedOpenToWork, setSelectedOpenToWork] = useState('all') + const [selectedSkills, setSelectedSkills] = useState([]) + const [memberSkills, setMemberSkills] = useState>(new Map()) + const [skillOptionsLoading, setSkillOptionsLoading] = useState(false) + const countryLookup: CountryLookup[] | undefined = useCountryLookup() + + const countryCodeFilter = selectedCountry === 'all' ? undefined : selectedCountry + + const loadSkillOptions = useCallback(async (query: string): Promise => { + setSkillOptionsLoading(true) + try { + return await fetchSkillAutocompleteOptions(query) + } catch { + return [] + } finally { + setSkillOptionsLoading(false) + } + }, []) + + const { data, error, isValidating }: SWRResponse = useSWR( + // eslint-disable-next-line max-len + `customer-portal-completed-profiles:${countryCodeFilter || 'all'}:${selectedOpenToWork}:${currentPage}:${DEFAULT_PAGE_SIZE}:${selectedSkills.map(skill => skill.value) + .sort() + .join(',')}`, + () => fetchCompletedProfiles( + countryCodeFilter, + currentPage, + DEFAULT_PAGE_SIZE, + selectedOpenToWork, + selectedSkills.map(skill => skill.value), + ), + { + revalidateOnFocus: false, + }, + ) + + // Fetch member skills for all profiles on the current page + useEffect(() => { + if (!data?.data || data.data.length === 0) return + + const fetchAllMemberSkills = async (): Promise => { + const skillsMap = new Map() + + for (const profile of data.data) { + if (profile.userId && !memberSkills.has(profile.userId)) { + const skills = await fetchMemberSkillsData(profile.userId) + skillsMap.set(profile.userId, skills) + } + } + + if (skillsMap.size > 0) { + setMemberSkills(prevSkills => { + const newMap = new Map(prevSkills) + skillsMap.forEach((skills, userId) => { + newMap.set(userId, skills) + }) + return newMap + }) + } + } + + fetchAllMemberSkills() + }, [data?.data]) + + const countryMap = useMemo(() => { + const map = new Map() + const countries = countryLookup || [] + + countries.forEach((country: CountryLookup) => { + if (country.countryCode) { + map.set(country.countryCode, country.country) + } + }) + + return map + }, [countryLookup]) + + const countryOptions = useMemo(() => { + const staticOptions = (countryLookup || []) + .filter(country => !!country.countryCode) + .map(country => ({ + label: country.country, + value: country.countryCode, + })) + .sort((a, b) => String(a.label) + .localeCompare(String(b.label))) + + const seen = new Set(staticOptions.map(option => option.value)) + const dynamicOptions = (data?.data || []) + .filter(profile => !!profile.countryCode && !seen.has(String(profile.countryCode))) + .map(profile => ({ + label: ( + countryMap.get(String(profile.countryCode)) + || profile.countryName + || String(profile.countryCode) + ), + value: String(profile.countryCode), + })) + .sort((a, b) => String(a.label) + .localeCompare(String(b.label))) + + return [ + { + label: 'All Countries', + value: 'all', + }, + ...staticOptions, + ...dynamicOptions, + ] + }, [countryLookup, countryMap, data?.data]) + + const profiles = data?.data || [] + const totalProfiles = data?.total || 0 + const totalPages = data?.totalPages || 1 + + const displayedRows = useMemo(() => profiles + .map(profile => { + const userSkills = profile.userId ? (memberSkills.get(profile.userId) || []) : [] + + // Prioritize principal skills, then add additional skills + const principalSkills = [ + ...userSkills.filter(skill => skill.displayMode?.name === UserSkillDisplayModes.principal), + ] + + const displayedSkills = principalSkills.slice(0, DISPLAY_SKILLS_COUNT) + const remainingSkillsText = principalSkills.slice(DISPLAY_SKILLS_COUNT) + .map(skill => skill.name) + .filter(Boolean) + .join(', ') + const additionalSkillsCount = Math.max(0, principalSkills.length - DISPLAY_SKILLS_COUNT) + + const isOpenToWork = profile.isOpenToWork === true + const openToWorkLabel = isOpenToWork ? 'Yes' : 'No' + const openToWorkRolesText = profile.openToWork?.preferredRoles && profile.openToWork.preferredRoles.length + ? profile.openToWork.preferredRoles.map(getPreferredRoleLabelByValue) + .filter(Boolean) + .join(', ') + : 'No role preferences set' + + return { + ...profile, + additionalSkillsCount, + countryLabel: profile.countryCode + ? countryMap.get(profile.countryCode) || profile.countryName || profile.countryCode + : profile.countryName || '-', + displayedSkills, + fullName: [profile.firstName, profile.lastName].filter(Boolean) + .join(' ') + .trim(), + isOpenToWork, + locationLabel: [profile.city, profile.countryCode + ? countryMap.get(profile.countryCode) || profile.countryName || profile.countryCode + : profile.countryName] + .filter(Boolean) + .join(', '), + openToWorkLabel, + openToWorkRolesText, + remainingSkillsText, + } + }) + .sort((a, b) => a.handle.localeCompare(b.handle)), [profiles, countryMap, memberSkills]) + + const isPreviousDisabled = currentPage <= 1 || isValidating + const isNextDisabled = isValidating || currentPage >= totalPages + + return ( + +
+
+
+ ) => { + setSelectedCountry(event.target.value || 'all') + setCurrentPage(1) + }} + placeholder='Select country' + /> +
+
+ ) => { + setSelectedOpenToWork((event.target.value || 'all') as OpenToWorkFilter) + setCurrentPage(1) + }} + placeholder='Select' + /> +
+
+ ) => { + const value = (event.target.value || []) as InputMultiselectOption[] + setSelectedSkills(value) + setCurrentPage(1) + }} + /> +
+
+
+ Fully Completed Profiles + {totalProfiles} +
+
+ + {isValidating && !data && ( +
+ +
+ )} + + {!isValidating && !!error && ( +
+ Failed to load profile completion data. +
+ )} + + {!error && !isValidating && displayedRows.length === 0 && ( +
+ No fully completed profiles found for the selected country. +
+ )} + + {!error && displayedRows.length > 0 && ( + <> +
+ + + + + + + + + + + + + {displayedRows.map(profile => ( + + + + + + + + + ))} + +
MemberHandleLocationOpen to WorkPrincipal Skills{' '}
+
+ {profile.photoURL && ( + {profile.handle} + )} + {profile.fullName || '-'} +
+
+ + {profile.handle} + + {profile.locationLabel || profile.countryLabel} + { + profile.openToWorkLabel === 'Yes' ? ( + + + {profile.openToWorkLabel} + + + ) : ( + + {profile.openToWorkLabel} + + ) + } + + {profile.displayedSkills && profile.displayedSkills.length > 0 ? ( +
+ {profile.displayedSkills.map(skill => ( + + {skill.name} + + ))} + {profile.additionalSkillsCount > 0 && ( + + + + + {profile.additionalSkillsCount} + {' '} + skills + + + )} +
+ ) : ( + '-' + )} +
+ + Go to profile + +
+
+
+ + Page + {' '} + {currentPage} + {' '} + of + {' '} + {totalPages} + +
+ + +
+
+ + )} +
+ ) +} + +export default ProfileCompletionPage diff --git a/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/index.ts b/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/index.ts new file mode 100644 index 000000000..4d99c8c31 --- /dev/null +++ b/src/apps/customer-portal/src/pages/profile-completion/ProfileCompletionPage/index.ts @@ -0,0 +1 @@ +export { default as ProfileCompletionPage } from './ProfileCompletionPage' diff --git a/src/apps/customer-portal/src/pages/profile-completion/index.ts b/src/apps/customer-portal/src/pages/profile-completion/index.ts new file mode 100644 index 000000000..73dcadd92 --- /dev/null +++ b/src/apps/customer-portal/src/pages/profile-completion/index.ts @@ -0,0 +1 @@ +export * from './ProfileCompletionPage' diff --git a/src/apps/customer-portal/src/pages/profile-completion/profile-completion.routes.tsx b/src/apps/customer-portal/src/pages/profile-completion/profile-completion.routes.tsx new file mode 100644 index 000000000..42042bc0f --- /dev/null +++ b/src/apps/customer-portal/src/pages/profile-completion/profile-completion.routes.tsx @@ -0,0 +1,26 @@ +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core' + +import { profileCompletionRouteId } from '../../config/routes.config' + +const ProfileCompletionPage: LazyLoadedComponent = lazyLoad( + () => import('./ProfileCompletionPage'), + 'ProfileCompletionPage', +) + +export const profileCompletionChildRoutes = [ + { + authRequired: true, + element: , + id: 'profile-completion-page', + route: '', + }, +] + +export const customerPortalProfileCompletionRoutes = [ + { + children: [...profileCompletionChildRoutes], + element: getRoutesContainer(profileCompletionChildRoutes), + id: profileCompletionRouteId, + route: profileCompletionRouteId, + }, +] diff --git a/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/TalentSearchPage.module.scss b/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/TalentSearchPage.module.scss deleted file mode 100644 index 22116c969..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/TalentSearchPage.module.scss +++ /dev/null @@ -1,409 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - display: flex; - flex-direction: column; -} - -:global([class*='ContentLayout-module_content-outer']) { - margin: 0 auto 0 !important; -} - -:global([class*='ContentLayout-module_content__']) { - padding-bottom: 0 !important; -} - -:global([class*='BreadCrumb-module_breadcrumb']) { - display: none; -} - -.pageArea { - position: relative; - @include substractPagePaddings; - background: $black-5; -} - -.pageHero { - width: 100%; - height: 280px; - background-image: url('../../../lib/assets/talent-search-header.png'); - background-position: center; - background-repeat: no-repeat; - background-size: cover; -} - -.pageBody { - display: grid; - grid-template-columns: 443px 1fr; - gap: 24px; - margin-top: -232px; - padding: $sp-2 $sp-8 $sp-10 $sp-8; - position: relative; - z-index: 1; - font-family: $font-roboto; - - @include ltemd { - grid-template-columns: 1fr; - margin-top: -232px; - padding: $sp-3 $sp-3 0; - } -} - -.sidebar { - display: flex; - flex-direction: column; - gap: 0; - - @include ltemd { - margin-left: 0; - position: relative; - z-index: 2; - } -} - -.panel { - background: $tc-white; - border: 0; - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; - gap: $sp-2; -} - -.sidebar .panel + .panel { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-top: 0; - padding-top: 14px; -} - -.panelTitle { - color: $black-100; - font-size: 16px; - line-height: 24px; - font-weight: 500; - margin: 0; -} - -.searchTabs { - display: flex; - gap: 40px; - border-bottom: 0; - padding-bottom: 0; - border-bottom: 1px solid $black-20; -} - -.tabButton { - border: 0; - background: transparent; - padding: 12px 0; - color: $black-60; - font-family: $font-roboto; - font-size: 16px; - line-height: 24px; - font-weight: 500; -} - -.activeTab { - color: $black-100; -} - -.jobDescriptionField { - margin-top: 0; - :global(textarea) { - min-height: 170px; - resize: vertical; - color: $black-100; - font-size: 14px; - line-height: 22px; - } -} - -.aiActions { - display: flex; - gap: $sp-1; - - :global(button) { - font-size: 14px; - } -} - -.errorMessage { - color: $red-100; - font-size: 13px; - line-height: 18px; - margin: 0; -} - -.filterBlock { - margin-bottom: 2px; - :global([class*='__value-container']) { - min-height: 18px; - } -} - -.skillsMultiselect { - :global([class*='__value-container']) { - gap: 6px; - } - - :global([class*='__multi-value']) { - margin: 0; - border-radius: 12px; - background: $black-10; - } - - :global([class*='__multi-value__label']) { - padding: 3px 8px; - color: $black-100; - font-size: 12px; - line-height: 14px; - font-weight: 500; - } - - :global([class*='__multi-value__remove']) { - margin: 0 6px 0 0; - padding: 2px; - color: $black-60; - - svg { - width: 14px; - height: 14px; - } - } -} - -.checkboxRow { - display: flex; - align-items: center; - gap: $sp-2; - color: $black-100; - font-size: 15px; - line-height: 21px; - font-weight: 400; - position: relative; - cursor: pointer; -} - -.checkboxLabelWithInfo { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.infoIconButton { - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border: 0; - border-radius: 50%; - padding: 0; - background: transparent; - color: $black-60; - cursor: pointer; - - :global(svg) { - width: 18px; - height: 18px; - } -} - -.checkboxInput { - position: absolute; - opacity: 0; - pointer-events: none; -} - -.toggleControl { - width: 36px; - height: 20px; - border-radius: 10px; - background: $turq-160; - position: relative; - flex-shrink: 0; - - &::after { - content: ''; - position: absolute; - top: 3px; - left: 20px; - width: 14px; - height: 14px; - border-radius: 50%; - background: $tc-white; - } -} - -.checkboxInput:not(:checked) + .toggleControl { - background: $black-40; - - &::after { - left: 2px; - } -} - -.clearFiltersWrap { - margin-top: 6px; - align-self: flex-start; - display: flex; - gap: $sp-2; - - :global(button) { - font-size: 14px; - } -} - -.resultsPanel { - background: transparent; - border: 0; - border-radius: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0; - overflow: visible; - min-height: 620px; -} - -.resultsPanelEmpty { - background: transparent; - border: 0; - border-radius: 0; -} - -.resultsContent { - display: flex; - flex-direction: column; - gap: $sp-4; - padding: 0; -} - -.resultsTop { - display: flex; - align-items: center; - justify-content: space-between; - gap: $sp-3; - padding-top: 0; - - @include ltemd { - flex-direction: column; - align-items: stretch; - } -} - -.matchingIndexSelect { - width: 184px; - - :global(.input-el) { - padding: 4px 10px 1px 10px; - } -} - -.sortControl { - display: flex; - align-items: center; - gap: 12px; - width: 300px; - justify-content: flex-end; - - :global([class*='__control']) { - min-height: 40px; - } - - :global(.input-el) { - margin-bottom: 0; - } -} - -.sortLabel { - color: $tc-white; - font-size: 16px; - line-height: 22px; - white-space: nowrap; - - @include ltemd { - color: $black-100; - } -} - -.foundText { - margin: 0; - color: $tc-white; - font-size: 16px; - line-height: 20px; - - @include ltemd { - color: $black-100; - } -} - -.foundTextCount { - font-weight: 700; -} - -.emptyState { - min-height: 470px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: $sp-6; - margin-top: 20vh; - text-align: center; - color: $black-60; - - h4 { - margin: 0 0 $sp-2; - color: $black-100; - } - - p { - margin: 0 0 $sp-1; - max-width: 500px; - } -} - -.emptyStateTitle { - color: $black-100; - font-size: 24px; - line-height: 32px; - font-weight: 700; - margin: 0 0 $sp-2; -} - -.emptyStateDescription { - color: $black-100; - font-size: 16px; - line-height: 24px; - font-weight: 400; - margin: 0; -} - -.emptyStateSearchText { - font-weight: 700; -} - -.emptyIcon { - width: 80px; - height: 80px; - margin-bottom: $sp-3; -} - -.cardsGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 20px; - - @include ltemd { - grid-template-columns: 1fr; - } -} - -.loadMoreWrap { - display: flex; - justify-content: center; - margin-top: 8px; -} diff --git a/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/TalentSearchPage.tsx b/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/TalentSearchPage.tsx deleted file mode 100644 index b45a923d7..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/TalentSearchPage.tsx +++ /dev/null @@ -1,578 +0,0 @@ -/* eslint-disable complexity */ -/* eslint-disable react/jsx-no-bind */ -import { ChangeEvent, FC, FocusEvent, useCallback, useMemo, useRef, useState } from 'react' -import classNames from 'classnames' - -import { CountryLookup, useCountryLookup } from '~/libs/core' -import { - Button, - IconOutline, - InputMultiselect, - InputMultiselectOption, - InputTextarea, - Tooltip, -} from '~/libs/ui' -import { extractSkillsFromText, fetchSkillAutocompleteOptions } from '~/libs/shared' - -import { TalentResultCard } from '../components/TalentResultCard' -import { - MemberSearchPayload, - MEMBER_SEARCH_LIMIT, - PageWrapper, - searchMembers, - SearchTalent, -} from '../../../lib' - -import styles from './TalentSearchPage.module.scss' - -export const TalentSearchPage: FC = () => { - const searchGenerationRef = useRef(0) - - const [lastSearchedDescription, setLastSearchedDescription] = useState('') - const countryLookup: CountryLookup[] | undefined = useCountryLookup() - const [jobDescription, setJobDescription] = useState('') - const [isExtractingSkills, setIsExtractingSkills] = useState(false) - const [errorMessage, setErrorMessage] = useState('') - const [hasSearched, setHasSearched] = useState(false) - const [skillOptionsLoading, setSkillOptionsLoading] = useState(false) - const [selectedSkills, setSelectedSkills] = useState([]) - const [selectedCountries, setSelectedCountries] = useState([]) - const [onlyProfileComplete, setOnlyProfileComplete] = useState(false) - const [onlyOpenToWork, setOnlyOpenToWork] = useState(true) - const [onlyActive, setOnlyActive] = useState(true) - const [isSearchingMembers, setIsSearchingMembers] = useState(false) - const [isLoadingMore, setIsLoadingMore] = useState(false) - const [results, setResults] = useState([]) - const [totalResults, setTotalResults] = useState(0) - const [currentPage, setCurrentPage] = useState(1) - const [lastAppliedSearchSignature, setLastAppliedSearchSignature] = useState('') - const [showSkillMatchOnCards, setShowSkillMatchOnCards] = useState(false) - const countryNameByCode = useMemo((): Map => new Map( - (countryLookup || []) - .filter(country => country.countryCode && country.country) - .map(country => [country.countryCode.toUpperCase(), country.country]), - ), [countryLookup]) - const countryFilterOptions = useMemo( - (): InputMultiselectOption[] => (countryLookup || []) - .map(country => ({ - label: country.country, - value: country.countryCode, - })) - .filter(option => option.label && option.value) - .sort((a, b) => String(a.label) - .localeCompare(String(b.label))), - [countryLookup], - ) - const selectedCountryCodesList = useMemo( - (): string[] => selectedCountries - .map(country => String(country.value || '') - .trim() - .toUpperCase()) - .filter(Boolean), - [selectedCountries], - ) - - const shouldShowIntroState = !hasSearched - const currentSearchSignature = useMemo( - (): string => JSON.stringify({ - countries: selectedCountryCodesList - .slice() - .sort(), - openToWork: onlyOpenToWork, - profileComplete: onlyProfileComplete, - recentlyActive: onlyActive, - skills: selectedSkills - .map(skill => String(skill.value || '') - .trim()) - .filter(Boolean) - .sort(), - }), - [onlyActive, onlyOpenToWork, onlyProfileComplete, selectedCountryCodesList, selectedSkills], - ) - - // Order comes from reports-api (sortBy/sortOrder on each request) so pagination stays globally consistent. - const displayedResults = results - - const foundMembersCount = totalResults || displayedResults.length - const displayedResultsWithCountryName = useMemo( - () => displayedResults.map(talent => { - const code = String(talent.location || '') - .trim() - .toUpperCase() - const countryName = countryNameByCode.get(code) - - if (!countryName) { - return talent - } - - return { - ...talent, - location: countryName, - } - }), - [countryNameByCode, displayedResults], - ) - const hasMoreResults = results.length < totalResults - - const loadSkillOptions = useCallback(async (query: string): Promise => { - setSkillOptionsLoading(true) - try { - return await fetchSkillAutocompleteOptions(query) - } catch { - return [] - } finally { - setSkillOptionsLoading(false) - } - }, []) - const loadCountryOptions = useCallback(async (query: string): Promise => { - const normalizedQuery = query.trim() - .toLowerCase() - if (!normalizedQuery) { - return countryFilterOptions - } - - return countryFilterOptions.filter(option => String(option.label || '') - .toLowerCase() - .includes(normalizedQuery)) - }, [countryFilterOptions]) - - const runMemberSearch = useCallback(async ( - skillsToSearch: InputMultiselectOption[], - overrides?: { - append?: boolean - countries?: string[] - generation?: number - openToWork?: boolean - page?: number - profileComplete?: boolean - recentlyActive?: boolean - }, - ): Promise => { - const append = overrides?.append === true - - const countries = (overrides?.countries ?? selectedCountryCodesList) - .filter(Boolean) - const generation = overrides?.generation - const openToWork = overrides?.openToWork ?? onlyOpenToWork - const page = overrides?.page ?? 1 - const profileComplete = overrides?.profileComplete ?? onlyProfileComplete - const recentlyActive = overrides?.recentlyActive ?? onlyActive - const hasSkills = skillsToSearch.length > 0 - const payload: MemberSearchPayload = { - limit: MEMBER_SEARCH_LIMIT, - page, - skills: skillsToSearch - .map(skill => String(skill.value || '') - .trim()) - .filter(Boolean) - .map(skillId => ({ - id: skillId, - wins: 1, - })), - skillSearchType: 'OR', - sortBy: hasSkills ? 'matchIndex' : 'handle', - sortOrder: hasSkills ? 'desc' : 'asc', - } - - if (countries.length > 0) { - payload.countries = countries - } - - if (openToWork) { - payload.openToWork = true - } - - if (profileComplete) { - payload.profileComplete = true - } - - if (recentlyActive) { - payload.recentlyActive = true - } - - if (append) { - setIsLoadingMore(true) - } else { - setIsSearchingMembers(true) - } - - setErrorMessage('') - try { - const response = await searchMembers(payload) - // If generation was provided and has changed, discard stale results - if (generation !== undefined && searchGenerationRef.current !== generation) { - return false - } - - const fetchedData = Array.isArray(response?.data) ? response.data : [] - setResults(prevResults => { - if (!append) { - return fetchedData - } - - const seen = new Set(prevResults.map(item => item.id)) - const merged = [...prevResults] - fetchedData.forEach(item => { - if (!seen.has(item.id)) { - seen.add(item.id) - merged.push(item) - } - }) - return merged - }) - setTotalResults(Number(response?.total || 0)) - setCurrentPage(Number(response?.page || page)) - return true - } catch { - if (!append) { - setResults([]) - setTotalResults(0) - setCurrentPage(1) - setLastSearchedDescription('') - } - - setErrorMessage('Failed to search matching members. Please try again.') - return false - } finally { - if (append) { - setIsLoadingMore(false) - } else { - setIsSearchingMembers(false) - } - } - }, [onlyActive, onlyOpenToWork, onlyProfileComplete, selectedCountryCodesList]) - - const clearAllFilters = useCallback((): void => { - setSelectedCountries([]) - setOnlyProfileComplete(false) - setOnlyOpenToWork(true) - setOnlyActive(true) - setSelectedSkills([]) - setErrorMessage('') - setLastSearchedDescription('') - }, []) - - const handleAiSearch = useCallback(async (): Promise => { - const normalizedDescription = jobDescription.trim() - if (!normalizedDescription || isExtractingSkills) { - return - } - - const generation = searchGenerationRef.current - - setErrorMessage('') - setIsExtractingSkills(true) - - try { - const extractedSkillsResult = await extractSkillsFromText(normalizedDescription) - if (searchGenerationRef.current !== generation) return - - const extractedSkills = Array.isArray(extractedSkillsResult?.matches) - ? extractedSkillsResult.matches - : [] - const skillById = new Map() - - extractedSkills.forEach((skill: { id?: unknown; name?: unknown }) => { - const skillId = String(skill.id ?? '') - .trim() - const skillName = String(skill.name ?? '') - .trim() - - if (!skillId || !skillName) { - return - } - - skillById.set(skillId, { - label: skillName, - value: skillId, - }) - }) - - const extractedOptions = Array.from(skillById.values()) - setSelectedSkills(extractedOptions) - - if (extractedOptions.length === 0) { - setErrorMessage('No skills were extracted from the job description.') - return - } - - setLastSearchedDescription(normalizedDescription) - } catch { - if (searchGenerationRef.current !== generation) return - setErrorMessage('Failed to extract skills. Please try again.') - } finally { - setIsExtractingSkills(false) - - } - }, [isExtractingSkills, jobDescription]) - - const handleSearch = useCallback(async (): Promise => { - if (isSearchingMembers || selectedSkills.length === 0) { - return - } - - setHasSearched(true) - const hadSkills = selectedSkills.length > 0 - const searchSucceeded = await runMemberSearch(selectedSkills, { - countries: selectedCountryCodesList, - openToWork: onlyOpenToWork, - page: 1, - profileComplete: onlyProfileComplete, - recentlyActive: onlyActive, - }) - if (searchSucceeded) { - setLastAppliedSearchSignature(currentSearchSignature) - setShowSkillMatchOnCards(hadSkills) - } - }, [ - currentSearchSignature, - isSearchingMembers, - onlyActive, - onlyOpenToWork, - onlyProfileComplete, - runMemberSearch, - selectedCountryCodesList, - selectedSkills, - ]) - - const handleLoadMore = useCallback((): void => { - if (isLoadingMore || isSearchingMembers || !hasMoreResults) { - return - } - - runMemberSearch(selectedSkills, { - append: true, - page: currentPage + 1, - }) - }, [currentPage, hasMoreResults, isLoadingMore, isSearchingMembers, runMemberSearch, selectedSkills]) - const isAiExtractButtonDisabled = useMemo( - () => isExtractingSkills - || !jobDescription.trim() - || jobDescription.trim() === lastSearchedDescription, - [isExtractingSkills, jobDescription, lastSearchedDescription], - ) - return ( - -
-
-
- - -
- {shouldShowIntroState && ( -
-

- Paste a job description to AI-extract skills, or enter skills manually - to find talents -

-
- )} - - {!shouldShowIntroState && ( -
- {!isSearchingMembers && ( -
-

- We have found  - - {`${foundMembersCount} members`} - -  that match your search. -

-
- )} - {isSearchingMembers && ( -
-

Searching talent...

-
- )} - {!isSearchingMembers && displayedResults.length === 0 && ( -
-

No matching talent found

-

Try changing filters or using a different job description.

-
- )} - {!isSearchingMembers && displayedResults.length > 0 && ( - <> -
- {displayedResultsWithCountryName.map(talent => ( - - ))} -
- {hasMoreResults && ( -
- -
- )} - - )} -
- )} -
-
-
- - ) -} - -export default TalentSearchPage diff --git a/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/index.ts b/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/index.ts deleted file mode 100644 index ecd3857a2..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/TalentSearchPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TalentSearchPage } from './TalentSearchPage' diff --git a/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/TalentResultCard.module.scss b/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/TalentResultCard.module.scss deleted file mode 100644 index 2cea74382..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/TalentResultCard.module.scss +++ /dev/null @@ -1,298 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.talentCard { - background: $tc-white; - border: 0; - border-radius: 12px; - padding: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.cardMain { - padding: 24px; -} - -.topRow { - display: flex; - gap: 16px; - align-items: flex-start; - width: 100%; -} - -.avatarWrap { - position: relative; - flex-shrink: 0; -} - -.profilePic { - width: 80px; - height: 80px; - - :global(span) { - font-size: 30px !important; - letter-spacing: 0.04em; - text-transform: uppercase; - } -} - -.verifiedBadge { - position: absolute; - right: 2px; - top: 2px; - width: 20px; - height: 20px; - background: $tc-white; - color: $turq-120; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 50%; - z-index: 2; - - :global(svg) { - width: 18px; - height: 18px; - display: block; - } -} - -.headContent { - display: flex; - flex-direction: column; - gap: 8px; - flex: 1; - min-width: 0; -} - -.cardHeader { - display: flex; - justify-content: space-between; - gap: $sp-2; - align-items: center; - min-width: 0; -} - -.handleText { - margin: 0; - color: $black-100; - font-size: 20px; - line-height: 26px; - font-weight: 700; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - @include ltemd { - font-size: 16px; - line-height: 22px; - } -} - -.nameText { - margin: 0; - color: $black-100; - font-size: 14px; - line-height: 20px; - font-weight: 500; -} - -.matchPill { - background: $teal-120; - color: $tc-white; - border-radius: 999px; - padding: 3px 10px; - font-size: 14px; - line-height: 22px; - font-weight: 500; - white-space: nowrap; - align-self: flex-start; - flex-shrink: 0; -} - -.locationRow { - display: flex; - align-items: flex-start; - gap: 6px; -} - -.locationIcon { - flex-shrink: 0; - width: 20px; - height: 20px; - margin-top: 1px; - color: $turq-160; -} - -.locationText { - margin: 0; - color: $black-100; - font-size: 14px; - line-height: 22px; - font-weight: 400; -} - -.statusRow { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 16px; -} - -.statusPill { - border-radius: 999px; - padding: 2px 10px; - font-size: 14px; - line-height: 22px; - font-weight: 500; -} - -.statusPillActive { - background: $green-25; - color: $turq-180; -} - -.statusPillInactive { - background: $black-10; - color: $black-80; -} - -.availability { - display: inline-flex; - align-items: center; - gap: 2px; - font-size: 14px; - line-height: 22px; - font-weight: 500; -} - -.availabilityYes { - color: $turq-160; -} - -.availabilityNo { - color: $black-80; -} - -.availabilityIcon { - flex-shrink: 0; - width: 18px; - height: 18px; -} - -.cardFooter { - display: flex; - align-items: center; - justify-content: space-between; - gap: $sp-2; - padding: 16px 24px 20px; - border-top: 1px solid $black-10; - font-size: 14px; - line-height: 22px; - font-weight: 400; -} - -.cardFooterWithoutMatch { - justify-content: flex-end; -} - -.footerMatched { - display: flex; - align-items: center; - gap: 6px; - min-width: 0; -} - -.experienceLink { - display: inline-flex; - align-items: center; - gap: 4px; - flex-shrink: 0; - color: $black-100; - text-decoration: none; - - &:hover { - color: $turq-160; - } - - &:focus-visible { - outline: 2px solid $link-blue; - outline-offset: 2px; - border-radius: 2px; - } -} - -.experienceLinkIcon { - width: 16px; - height: 16px; - color: $turq-160; -} - -.matchedSkillsText { - margin: 0; - color: $black-100; - font-weight: 500; -} - -.infoButton { - display: inline-flex; - align-items: center; - justify-content: center; - margin: 0; - padding: 0; - border: 0; - background: transparent; - cursor: pointer; - color: $black-60; - border-radius: 50%; - - &:hover { - color: $black-80; - } - - &:focus-visible { - outline: 2px solid $link-blue; - outline-offset: 2px; - } -} - -.infoIcon { - width: 18px; - height: 18px; - display: block; -} - -.tooltipBody { - text-align: left; -} - -.tooltipTitle { - margin: 0 0 8px; - font-weight: 700; - font-size: 13px; - line-height: 18px; -} - -.tooltipLines { - margin: 0; - padding-left: 0; - list-style-type: disc; - list-style-position: inside; - font-size: 12px; - line-height: 16px; - font-weight: 400; -} - -.tooltipSkillLine { - display: list-item; - - & + & { - margin-top: 4px; - } -} - -.tooltipSkillName { - font-weight: 700; -} diff --git a/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/TalentResultCard.tsx b/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/TalentResultCard.tsx deleted file mode 100644 index 2b37a9c8b..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/TalentResultCard.tsx +++ /dev/null @@ -1,205 +0,0 @@ -/* eslint-disable complexity */ -import { FC, ReactElement, useMemo } from 'react' -import classNames from 'classnames' - -import { EnvironmentConfig } from '~/config' -import { ProfilePicture } from '~/libs/shared' -import { IconOutline, IconSolid, Tooltip } from '~/libs/ui' - -import styles from './TalentResultCard.module.scss' - -interface MatchedSkill { - id: string - name: string - wins: number - submitted: number -} - -interface TalentResultCardTalent { - id: string - handle: string - isVerified: boolean - isRecentlyActive: boolean - location: string - matchIndex: number - matchedSkills: MatchedSkill[] - name: string - openToWork?: boolean - photoUrl?: string -} - -interface TalentResultCardProps { - talent: TalentResultCardTalent - showSkillMatch: boolean -} - -function getUniqueMatchedSkills(talent: TalentResultCardTalent): TalentResultCardTalent['matchedSkills'] { - const seen = new Set() - return talent.matchedSkills.filter((skill: MatchedSkill) => { - const key = `${skill.id}-${skill.name}` - if (seen.has(key)) { - return false - } - - seen.add(key) - return true - }) -} - -function matchedSkillStatsLabel(skill: MatchedSkill): string { - const parts: string[] = [] - if (skill.wins > 0) { - parts.push(`${skill.wins} wins`) - } - - if (skill.submitted > 0) { - parts.push(`${skill.submitted} submissions`) - } - - return parts.length > 0 ? `: ${parts.join(', ')}` : '' -} - -function buildMatchedSkillsTooltipContent( - count: number, - skills: MatchedSkill[], -): ReactElement { - return ( -
-

- {`${count} Matched Skills:`} -

-
    - {skills.map((skill: MatchedSkill) => ( -
  • - {skill.name} - {matchedSkillStatsLabel(skill)} -
  • - ))} -
-
- ) -} - -export const TalentResultCard: FC = (props: TalentResultCardProps) => { - const talent: TalentResultCardTalent = props.talent - const showSkillMatch = props.showSkillMatch - const uniqueSkills = useMemo(() => getUniqueMatchedSkills(talent), [talent]) - const isVerifiedProfile = talent.isVerified === true - const displayName = String(talent.name || '') - .trim() - const [firstName, ...lastNameParts] = displayName.split(/\s+/) - const lastName = lastNameParts.join(' ') - .trim() - - const isActive = talent.isRecentlyActive === true - const openToWork = talent.openToWork - const profileUrl = `${EnvironmentConfig.USER_PROFILE_URL}/${encodeURIComponent(talent.handle)}` - const displayHandle = String(talent.handle || '') - .trim() - const matchedSkillLabel = uniqueSkills.length === 1 ? 'matched skill' : 'matched skills' - - return ( -
-
-
-
- - {isVerifiedProfile && ( - - - - )} -
-
-
- {displayHandle} - {showSkillMatch && ( - - {`${talent.matchIndex}% Match`} - - )} -
-

{talent.name}

-
- - {talent.location} -
-
- - {isActive ? 'Active' : 'Inactive'} - - {openToWork !== undefined && ( - - {openToWork ? ( - - ) : ( - - )} - {openToWork ? 'Available' : 'Unavailable'} - - )} -
-
-
-
-
- {showSkillMatch && ( -
- - {`${uniqueSkills.length} ${matchedSkillLabel}`} - - {uniqueSkills.length > 0 && ( - - - - )} -
- )} - - Experience Match - - -
-
- ) -} - -export default TalentResultCard diff --git a/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/index.ts b/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/index.ts deleted file mode 100644 index 4dd223e55..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/components/TalentResultCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TalentResultCard } from './TalentResultCard' diff --git a/src/apps/customer-portal/src/pages/talent-search/talent-search.routes.tsx b/src/apps/customer-portal/src/pages/talent-search/talent-search.routes.tsx deleted file mode 100644 index c09998492..000000000 --- a/src/apps/customer-portal/src/pages/talent-search/talent-search.routes.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core' - -import { talentSearchRouteId } from '../../config/routes.config' - -const TalentSearchPage: LazyLoadedComponent = lazyLoad( - () => import('./TalentSearchPage'), - 'TalentSearchPage', -) - -export const talentSearchChildRoutes = [ - { - authRequired: true, - element: , - id: 'talent-search-page', - route: '', - }, -] - -export const customerPortalTalentSearchRoutes = [ - { - children: [...talentSearchChildRoutes], - element: getRoutesContainer(talentSearchChildRoutes), - id: talentSearchRouteId, - route: talentSearchRouteId, - }, -] diff --git a/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md b/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md index 291640e86..bf9e09688 100644 --- a/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md +++ b/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md @@ -159,11 +159,6 @@ Application that allows users to manage their own profile data, and allows visit Located `src/apps/profiles`. -#### Talent Search App -This is an internal app for finding members based on skills and other search facets. - -Located `src/apps/talent-search`. - #### Skills Manager Admin app that allows one to manage the standardized skills. diff --git a/src/apps/onboarding/src/models/MemberInfo.ts b/src/apps/onboarding/src/models/MemberInfo.ts index 23de4ea8a..236abe13b 100644 --- a/src/apps/onboarding/src/models/MemberInfo.ts +++ b/src/apps/onboarding/src/models/MemberInfo.ts @@ -1,8 +1,11 @@ -import { MemberMaxRating } from '~/apps/talent-search/src/lib/models' import { MemberStats, UserSkill } from '~/libs/core' import MemberAddress from './MemberAddress' +export type MemberMaxRating = { + rating?: number +} + export default interface MemberInfo { userId: number handle: string diff --git a/src/apps/onboarding/src/pages/account-details/index.tsx b/src/apps/onboarding/src/pages/account-details/index.tsx index 36e17f2bb..438c6c734 100644 --- a/src/apps/onboarding/src/pages/account-details/index.tsx +++ b/src/apps/onboarding/src/pages/account-details/index.tsx @@ -7,7 +7,7 @@ import classNames from 'classnames' 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 { UserProfile } from '~/libs/core' import { ProgressBar } from '../../components/progress-bar' import { validatePhonenumber } from '../../utils/validation' @@ -28,7 +28,7 @@ const blankConnectInfo: ConnectInfo = emptyConnectInfo() const PageAccountDetailsContent: FC<{ reduxAddress: MemberAddress | undefined reduxConnectInfo: ConnectInfo | undefined - reduxMemberInfo: Member | undefined + reduxMemberInfo: UserProfile | undefined updateMemberConnectInfos: (infos: ConnectInfo[]) => void createMemberConnectInfos: (infos: ConnectInfo[]) => void updateMemberHomeAddresss: (infos: MemberAddress[]) => void diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 9ac5a1606..81c127ddd 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -4,7 +4,7 @@ import { connect } from 'react-redux' import classNames from 'classnames' import { Button, PageDivider } from '~/libs/ui' -import { Member } from '~/apps/talent-search/src/lib/models' +import { UserProfile } from '~/libs/core' import { MemberSkillEditor, useMemberSkillEditor } from '~/libs/shared' import { ProgressBar } from '../../components/progress-bar' @@ -12,7 +12,7 @@ import { ProgressBar } from '../../components/progress-bar' import styles from './styles.module.scss' export const PageSkillsContent: FC<{ - reduxMemberInfo: Member | undefined + reduxMemberInfo: UserProfile | undefined }> = props => { const navigate: any = useNavigate() const [loading, setLoading] = useState(false) diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index 0bc933be7..14b912d9d 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -3,7 +3,6 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' import { learnRoutes } from '~/apps/learn' import { devCenterRoutes } from '~/apps/dev-center' import { profilesRoutes } from '~/apps/profiles' -import { talentSearchRoutes } from '~/apps/talent-search' import { accountsRoutes } from '~/apps/accounts' import { onboardingRoutes } from '~/apps/onboarding' import { walletRoutes } from '~/apps/wallet' @@ -38,7 +37,6 @@ export const platformRoutes: Array = [ ...devCenterRoutes, ...copilotsRoutes, ...learnRoutes, - ...talentSearchRoutes, ...profilesRoutes, ...walletRoutes, ...walletAdminRoutes, diff --git a/src/apps/profiles/src/member-profile/MemberProfilePage.tsx b/src/apps/profiles/src/member-profile/MemberProfilePage.tsx index b8054c8ac..d5a85cf72 100644 --- a/src/apps/profiles/src/member-profile/MemberProfilePage.tsx +++ b/src/apps/profiles/src/member-profile/MemberProfilePage.tsx @@ -3,9 +3,9 @@ import { Params, useNavigate, useParams } from 'react-router-dom' import { AxiosError } from 'axios' import { profileContext, ProfileContextData, profileGetPublicAsync, UserProfile } from '~/libs/core' -import { TALENT_SEARCH_PATHS } from '~/apps/talent-search' import { LoadingSpinner } from '~/libs/ui' +import { rootRoute } from '../profiles.routes' import { notifyUniNavi } from '../lib' import { ProfilePageLayout } from './page-layout' @@ -38,7 +38,7 @@ const MemberProfilePage: FC<{}> = () => { }) .catch((e: AxiosError) => { if (e.code === AxiosError.ERR_BAD_REQUEST && e.response?.status === 404) { - window.location.href = `${TALENT_SEARCH_PATHS.absoluteUrl}?memberNotFound` + navigate(rootRoute) } }) } diff --git a/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx b/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx index 08cac5dba..0039ee3ec 100644 --- a/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx +++ b/src/apps/profiles/src/profiles-landing-page/ProfilesLandingPage.tsx @@ -2,23 +2,20 @@ import { FC, useContext, useEffect } from 'react' import { NavigateFunction, useNavigate } from 'react-router-dom' import { profileContext, ProfileContextData } from '~/libs/core' -import { TALENT_SEARCH_PATHS } from '~/apps/talent-search' import { rootRoute } from '../profiles.routes' const ProfilesLandingPage: FC = () => { const navigate: NavigateFunction = useNavigate() - const { profile: authProfile, initialized }: ProfileContextData = useContext(profileContext) + const { profile: authProfile }: ProfileContextData = useContext(profileContext) // redirect to profile page if logged in useEffect(() => { if (authProfile) { navigate(`${rootRoute}/${authProfile.handle}`) - } else if (initialized) { - window.location.href = `${TALENT_SEARCH_PATHS.absoluteUrl}` } - }, [authProfile, navigate, initialized]) + }, [authProfile, navigate]) return ( // TODO: no profile specified - redirect to talent search or dedicated page diff --git a/src/apps/talent-search/README.md b/src/apps/talent-search/README.md deleted file mode 100644 index e801afbbf..000000000 --- a/src/apps/talent-search/README.md +++ /dev/null @@ -1,6 +0,0 @@ -### Talent search - -This is an internal for finding members based on skills and other search facets. The main APIs used include: - -* Member API -* Match Engine API \ No newline at end of file diff --git a/src/apps/talent-search/index.ts b/src/apps/talent-search/index.ts deleted file mode 100644 index 6f39cd49b..000000000 --- a/src/apps/talent-search/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src' diff --git a/src/apps/talent-search/src/TalentSearchApp.tsx b/src/apps/talent-search/src/TalentSearchApp.tsx deleted file mode 100644 index a4f33350e..000000000 --- a/src/apps/talent-search/src/TalentSearchApp.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { FC, useContext } from 'react' -import { Outlet, Routes } from 'react-router-dom' - -import { routerContext, RouterContextData } from '~/libs/core' -import { SharedSwrConfig } from '~/libs/shared' - -import { toolTitle } from './talent-search.routes' - -const TalentSearchApp: FC<{}> = () => { - - const { getChildRoutes }: RouterContextData = useContext(routerContext) - - return ( - - - - {getChildRoutes(toolTitle)} - - - ) -} - -export default TalentSearchApp diff --git a/src/apps/talent-search/src/assets/background-m.webp b/src/apps/talent-search/src/assets/background-m.webp deleted file mode 100644 index 25b669c60..000000000 Binary files a/src/apps/talent-search/src/assets/background-m.webp and /dev/null differ diff --git a/src/apps/talent-search/src/assets/background.jpg b/src/apps/talent-search/src/assets/background.jpg deleted file mode 100644 index 8ee73d550..000000000 Binary files a/src/apps/talent-search/src/assets/background.jpg and /dev/null differ diff --git a/src/apps/talent-search/src/assets/background.webp b/src/apps/talent-search/src/assets/background.webp deleted file mode 100644 index fb2165c64..000000000 Binary files a/src/apps/talent-search/src/assets/background.webp and /dev/null differ diff --git a/src/apps/talent-search/src/assets/verified-icon-white.svg b/src/apps/talent-search/src/assets/verified-icon-white.svg deleted file mode 100644 index 78b59c15c..000000000 --- a/src/apps/talent-search/src/assets/verified-icon-white.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/apps/talent-search/src/components/popular-skills/PopularSkills.module.scss b/src/apps/talent-search/src/components/popular-skills/PopularSkills.module.scss deleted file mode 100644 index 7079a4aad..000000000 --- a/src/apps/talent-search/src/components/popular-skills/PopularSkills.module.scss +++ /dev/null @@ -1,33 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.wrap { - margin-top: $sp-4; - margin-left: auto; - margin-right: auto; - text-align: center; - - :global(.body-medium-bold) { - margin-bottom: $sp-4; - } - - @include ltemd { - margin-top: $sp-8; - } -} - -.wrapTitle { - font-size: 20px; - line-height: 26px; - font-family: $font-roboto; - font-weight: $font-weight-bold; - margin-bottom: $sp-4; -} - -.pills { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: center; - gap: $sp-2; - max-width: 680px; -} diff --git a/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx b/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx deleted file mode 100644 index c7df349bb..000000000 --- a/src/apps/talent-search/src/components/popular-skills/PopularSkills.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { FC, useCallback } from 'react' - -import { SkillPill } from '~/libs/shared' -import { SearchUserSkill } from '~/libs/core' - -import { SKILL_SEARCH_LIMIT } from '../../config' - -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. -// TODO: update this with the real list of popular skills -const popularSkills: SearchUserSkill[] = [ - { - id: '63bb7cfc-b0d4-4584-820a-18c503b4b0fe', - name: 'Java', - }, - { - id: '0d86f8cb-e509-4ca8-b9f8-e65d909cc6eb', - name: 'MySQL', - }, - { - id: '32899253-5989-4c33-9948-cad29c9e0ab0', - name: 'Node.js', - }, - { - id: '9c42c728-47ff-4b20-916c-560739bad1ee', - name: 'Cascading Style Sheets (CSS)', - }, - { - id: '16ee1403-8e73-497d-a766-623eefd3c806', - name: 'JavaScript', - }, - { - id: '99e5fc45-5fc0-4794-a578-f42dfabcbf74', - name: 'Machine Learning', - }, - { - id: 'a0da6acf-2cf8-48f0-ba4a-30d18bc75052', - name: 'Unit Testing', - }, - { - id: '7e8641e5-e5c1-4ab6-a8f4-1fd6a8686dbe', - name: 'Angular', - }, - { - id: 'f0597e53-9a6d-40d6-8639-4d5a9ead190f', - name: '.NET Framework', - }, - { - id: 'fcbac194-35ab-4a31-aa7c-a2867fff9c4b', - name: 'Python', - }, - { - id: 'adf9d7b9-d639-4a73-8772-673b3d4f41b0', - name: 'Android', - }, - { - id: '130323ce-7d88-4141-9e2b-904994f026a1', - name: 'Figma (Design Software)', - }, - { - id: '9eaf6049-402a-481c-ac82-87a0826128c7', - name: 'Microsoft Azure', - }, - { - id: 'ced0b36c-6057-48e1-a263-2588fb91296b', - name: 'Adobe Illustrator', - }, - { - id: 'be85b096-b841-45b4-a5cb-1d3ee7ce1126', - name: 'Docker', - }, - { - id: '4458454c-9a97-4332-a545-6546e240dab6', - name: 'React.js', - }, -] - -interface PopularSkillsProps { - onChange: (skills: SearchUserSkill[]) => void - selectedSkills: SearchUserSkill[] -} - -const PopularSkills: FC = props => { - - const toggleSkill = useCallback((skill: SearchUserSkill) => { - 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 - // Or add the new item to the list - props.selectedSkills.forEach(filterSkill => { - if (filterSkill.id === skill.id) { - deleted = true - } else { - newFilter.push(filterSkill) - } - }) - if (deleted === false && props.selectedSkills.length >= SKILL_SEARCH_LIMIT) { - return - } - - if (deleted === false) { - newFilter = props.selectedSkills.concat(skill) - } - - props.onChange.call(undefined, newFilter) - }, [props.onChange, props.selectedSkills]) - - function isSelected(skill: SearchUserSkill): boolean { - return !!props.selectedSkills.find(s => s.id === skill.id) - } - - return ( -
-
Popular Skills
- -
- {popularSkills.map(skill => ( - - ))} -
-
- ) -} - -export default PopularSkills diff --git a/src/apps/talent-search/src/components/popular-skills/index.ts b/src/apps/talent-search/src/components/popular-skills/index.ts deleted file mode 100644 index 497b5f7d3..000000000 --- a/src/apps/talent-search/src/components/popular-skills/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PopularSkills } from './PopularSkills' diff --git a/src/apps/talent-search/src/components/profile-match/ProfileMatch.module.scss b/src/apps/talent-search/src/components/profile-match/ProfileMatch.module.scss deleted file mode 100644 index 8360b454f..000000000 --- a/src/apps/talent-search/src/components/profile-match/ProfileMatch.module.scss +++ /dev/null @@ -1,52 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.wrap { - display: flex; - align-items: center; - justify-content: center; - color: $tc-white; - font-family: $font-roboto; - flex-direction: column; - - border-radius: 100px; - padding-bottom: 2px; - width: 100%; - aspect-ratio: 1 / 1; - - background: rgb(130,234,207); - background: linear-gradient( - 180deg, - $turq-50 0%, - $turq-50 45%, - $turq-100 50%, - $turq-100 67%, - $turq-120 70%, - $turq-140 80%, - $turq-180 90% - ); - background-size: 100% 3000px; - background-position: 0 0; - - font-size: 16px; - line-height: 26px; - font-weight: $font-weight-bold; - - &:global(.dark) { - color: $black-100; - } - - strong { - font-size: 23px; - line-height: 27px; - font-weight: 900; - } - - @include ltemd { - font-size: 10px; - line-height: 14px; - strong { - font-size: 12px; - line-height: 14px; - } - } -} diff --git a/src/apps/talent-search/src/components/profile-match/ProfileMatch.tsx b/src/apps/talent-search/src/components/profile-match/ProfileMatch.tsx deleted file mode 100644 index c7a7c36c2..000000000 --- a/src/apps/talent-search/src/components/profile-match/ProfileMatch.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FC } from 'react' -import classNames from 'classnames' - -import styles from './ProfileMatch.module.scss' - -interface ProfileMatchProps { - className?: string - percent?: number -} - -const ProfileMatch: FC = props => { - const value = Math.round((props.percent ?? 0) * 100) - - return ( -
- - {value} - % - - Match -
- ) -} - -export default ProfileMatch diff --git a/src/apps/talent-search/src/components/profile-match/index.ts b/src/apps/talent-search/src/components/profile-match/index.ts deleted file mode 100644 index 6508c84e9..000000000 --- a/src/apps/talent-search/src/components/profile-match/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ProfileMatch } from './ProfileMatch' 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 deleted file mode 100644 index d24a1726f..000000000 --- a/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.module.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.wrap { - width: 100%; - margin-bottom: $sp-6; - - @include ltemd { - margin-bottom: $sp-2; - } -} - -.highlightWrap { - - display: flex; - align-items: center; - gap: $sp-4; - padding: $sp-6; - - border-radius: $sp-2; - border: 2px solid $turq-100; - background: $turq-15; - - color: #073D2E; - - @include ltemd { - flex-direction: column; - padding: $sp-4 $sp-2; - } -} - -.matchPerc { - width: 128px; - aspect-ratio: 1 / 1; - border-radius: 50%; - background: $turq-75; - color: #073D2E; - flex: 0 0 auto; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - - strong { - font-weight: 900; - font-size: 32px; - line-height: 38px; - } - - span { - font-weight: 900; - font-size: 20px; - line-height: 24px; - } -} - -.infoWrap { - display: flex; - flex-direction: column; - gap: $sp-1; - @include ltemd { - gap: $sp-2; - } -} - -.skillsList { - display: flex; - align-items: center; - gap: $sp-2; - - flex-wrap: wrap; - @include ltemd { - gap: $sp-1; - } -} - -.missingSkills { - margin-top: $sp-1; - > span:last-child { - color: $turq-180; - margin-left: $sp-1; - } -} - -.additionalSkills { - margin-top: $sp-6; - - .skillsList { - margin-top: $sp-6; - } - - @include ltemd { - .skillsList { - margin-top: $sp-3; - } - } -} 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 deleted file mode 100644 index 642b099da..000000000 --- a/src/apps/talent-search/src/components/profile-skills-match/ProfileSkillsMatch.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { FC } from 'react' -import classNames from 'classnames' - -import { UserSkill } from '~/libs/core' -import { isSkillVerified, SkillPill } from '~/libs/shared' - -import { useIsMatchingSkill } from '../../lib/utils' - -import styles from './ProfileSkillsMatch.module.scss' - -interface ProfileSkillsMatchProps { - matchValue: number - profileSkills: Pick[] - queriedSkills: UserSkill[] -} - -const ProfileSkillsMatch: FC = props => { - const isMatchingSkill = useIsMatchingSkill(props.queriedSkills) - const matchedSkills = props.profileSkills.filter(isMatchingSkill) - 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)) - - return ( -
-
-
- - {Math.round(props.matchValue * 100)} - % - - Match -
-
- {provenMatched.length > 0 && ( - <> -
- {provenMatched.length} - {` matched proven skill${provenMatched.length > 1 ? 's' : ''}`} -
-
- {provenMatched.map(skill => ( - - ))} -
- - )} - {selfSkillmatched.length > 0 && ( - <> -
- {selfSkillmatched.length} - {` matched self proclaimed skill${selfSkillmatched.length > 1 ? 's' : ''}`} -
-
- {selfSkillmatched.map(skill => ( - - ))} -
- - )} - {missingSkills.length > 0 && ( -
- Missing skills: - - { - missingSkills - .map(s => s.name) - .join(', ') - } - -
- )} -
-
-
- ) -} - -export default ProfileSkillsMatch diff --git a/src/apps/talent-search/src/components/profile-skills-match/index.ts b/src/apps/talent-search/src/components/profile-skills-match/index.ts deleted file mode 100644 index 5f03a8f70..000000000 --- a/src/apps/talent-search/src/components/profile-skills-match/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ProfileSkillsMatch } from './ProfileSkillsMatch' diff --git a/src/apps/talent-search/src/components/search-input/SearchInput.module.scss b/src/apps/talent-search/src/components/search-input/SearchInput.module.scss deleted file mode 100644 index 880ad8bcc..000000000 --- a/src/apps/talent-search/src/components/search-input/SearchInput.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.searchIcon{ - margin-right: 10px; - - position: relative; - z-index: 1; - - cursor: pointer; - color: $black-40; - transition: 0.15s ease-in-out; - - &:hover { - color: $black-100; - } - - &.disabled { - pointer-events: none; - opacity: 0.4; - } - - &:before { - content: ""; - display: block; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: $sp-12; - height: $sp-12; - } - - svg { - width: $sp-6; - height: $sp-6; - stroke: currentColor; - color: inherit; - } -} - -.maxLimit { - font-size: 14px; - color: $tc-white; -} diff --git a/src/apps/talent-search/src/components/search-input/SearchInput.tsx b/src/apps/talent-search/src/components/search-input/SearchInput.tsx deleted file mode 100644 index d8f17c4e2..000000000 --- a/src/apps/talent-search/src/components/search-input/SearchInput.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { FC, MouseEvent, Ref, useMemo } from 'react' -import classNames from 'classnames' - -import { IconOutline, InputMultiselectOption } from '~/libs/ui' -import { InputSkillSelector } from '~/libs/shared' -import { UserSkill } from '~/libs/core' - -import { - SKILL_SEARCH_LIMIT, - SKILL_SEARCH_MINIMUM, -} from '../../config' - -import styles from './SearchInput.module.scss' - -interface SearchInputProps { - className?: string - readonly autoFocus?: boolean - onChange: (skills: Pick[]) => void - skills: UserSkill[] - onSearch?: () => void - inputRef?: Ref -} - -const SearchInput: FC = props => { - const skills = useMemo(() => props.skills.map(s => ({ - category: s.category, - id: s.id, - levels: [], - name: s.name, - })), [props.skills]) - const canSearch = skills.length >= SKILL_SEARCH_MINIMUM - - function onChange(ev: any): void { - const options = (ev.target.value as unknown) as InputMultiselectOption[] - props.onChange(options.map(v => ({ - id: v.value, - name: v.label as string, - }))) - } - - function handleSearchSubmit(): void { - if (!canSearch) { - return - } - - props.onSearch?.() - } - - function handleSearchClick(ev: MouseEvent): void { - ev.preventDefault() - ev.stopPropagation() - - handleSearchSubmit() - } - - const searchIcon = ( -
- -
- ) - - return ( -
- - {skills.length > 0 && skills.length < SKILL_SEARCH_MINIMUM && ( -
- {`Please select at least ${SKILL_SEARCH_MINIMUM} skills to search`} -
- )} - {skills.length >= SKILL_SEARCH_LIMIT && ( -
- {`You can only search up to ${SKILL_SEARCH_LIMIT} skills at one time`} -
- )} -
- ) -} - -export default SearchInput diff --git a/src/apps/talent-search/src/components/search-input/index.ts b/src/apps/talent-search/src/components/search-input/index.ts deleted file mode 100644 index 73dcba1b6..000000000 --- a/src/apps/talent-search/src/components/search-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as SearchInput } from './SearchInput' 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 deleted file mode 100644 index 18277632e..000000000 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.module.scss +++ /dev/null @@ -1,171 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.wrap { - padding: $sp-6; - border-radius: $sp-4; - - background: $tc-white; - - font-family: $font-barlow; - color: $black-100; - - @include ltelg { - width: 100%; - } - - @include ltemd { - padding: $sp-3; - } -} - -.topWrap { - display: flex; - gap: $sp-6; - @include ltemd { - gap: $sp-3; - } -} - -.profilePic { - width: 188px; - max-width: 188px; - - @media (max-width: 1140px) { - width: 120px; - max-width: 120px; - } - - @include ltemd { - width: 94px; - max-width: 94px; - } -} - -.detailsContainer { - flex: 1 1 auto; - - padding: $sp-5 0 $sp-45; - display: flex; - gap: $sp-4; - - @include ltemd { - padding: 0; - gap: $sp-3; - } -} - -.talentInfo { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: $sp-3; - margin-bottom: $sp-45; - flex: 1 1 auto; - - @include ltemd { - gap: $sp-1; - margin-bottom: $sp-2; - } - - &Name { - font-size: 30px; - line-height: 32px; - color: $black-100; - - @include ltemd { - font-size: 14px; - line-height: 20px; - font-weight: 500; - } - } - - &Handle { - color: $black-60; - > span { - display: block; - } - } - - &Location { - color: $black-100; - - display: flex; - align-items: center; - gap: $sp-2; - - @include ltemd { - margin-right: -58px; - } - } - -} - -.profileMatch { - width: 83px; - flex: 0 0 auto; - - @include ltemd { - width: 46px; - } -} - -.skillsContainer { - margin-top: $sp-5; - border-top: 1px solid $black-10; - padding-top: $sp-5; - 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; - white-space: nowrap; - } - - &:global(.overline) { - text-transform: uppercase; - } - - @include ltemd { - padding-top: $sp-4; - margin-top: $sp-4; - } -} - -.unmatchedSkills { - display: flex; - - color: $black-100; - padding: 7px $sp-3; - font-size: 14px; - line-height: 16px; - - border: 1px solid $black-20; - border-radius: $sp-6; - - order: 999; -} - -.skillsWrap { - display: flex; - align-items: center; - gap: $sp-1; - flex-wrap: wrap; - position: relative; - - &:global(.init) { - max-height: 116px; - overflow: hidden; - - .unmatchedSkills { - order: -1; - } - } -} diff --git a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx b/src/apps/talent-search/src/components/talent-card/TalentCard.tsx deleted file mode 100644 index d1ccb3067..000000000 --- a/src/apps/talent-search/src/components/talent-card/TalentCard.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { FC, useEffect, useMemo, useRef } from 'react' -import { Link } from 'react-router-dom' -import { orderBy } from 'lodash' -import classNames from 'classnames' -import codes from 'country-calling-code' - -import { IconSolid } from '~/libs/ui' -import { isSkillVerified, ProfilePicture, SkillPill } from '~/libs/shared' -import { NamesAndHandleAppearance, UserSkill } from '~/libs/core' - -import { ProfileMatch } from '../profile-match' -import { Member } from '../../lib/models' -import { TALENT_SEARCH_PATHS } from '../../talent-search.routes' -import { useIsMatchingSkill } from '../../lib/utils' - -import styles from './TalentCard.module.scss' - -const getCountry = (countryCode: string): string => ( - codes.find(c => c.isoCode3 === countryCode || c.isoCode2 === countryCode)?.country ?? countryCode ?? '' -) - -const getAddrString = (city: string, country: string): string => ( - [city, country].filter(Boolean) - .join(', ') -) - -function isOverflow(el: HTMLElement): boolean { - const parentHeight = el.parentElement?.offsetHeight ?? 0 - const bottom = el.offsetTop + el.offsetHeight - return bottom > parentHeight -} - -interface TalentCardProps { - queriedSkills: UserSkill[] - member: Member - match?: number -} - -const TalentCard: FC = props => { - const restLabelRef = useRef(null) - const skillsWrapRef = useRef(null) - const talentRoute = `${TALENT_SEARCH_PATHS.talent}/${props.member.handle}` - const isMatchingSkill = useIsMatchingSkill(props.queriedSkills) - const address = props.member.addresses?.[0] ?? {} - - const matchedSkills = useMemo(() => ( - orderBy( - props.member.skills, - [isSkillVerified, a => a.name], - ['desc', 'asc'], - ) - .filter(isMatchingSkill) - ), [isMatchingSkill, props.member.skills]) - - const matchState = useMemo(() => ({ - matchValue: props.match, - queriedSkills: props.queriedSkills, - }), [props.match, props.queriedSkills]) - - // after initial render, check and limit to 3 rows of skills - useEffect(() => { - if (!skillsWrapRef.current) { - return - } - - // check if there are more than 3 rows of skills displayed initially, and hide them - const skillPillEls = [].slice.call(skillsWrapRef.current.childNodes) - const hiddenEls: HTMLElement[] = skillPillEls.filter(isOverflow) - - if (!hiddenEls.length && restLabelRef.current?.innerText.match(/^\+0/)) { - restLabelRef.current.style.display = 'none' - return - } - - if (hiddenEls.length > 1) { - const firstHidden = skillPillEls[skillPillEls.findIndex(s => s === hiddenEls[0]) - 1] - hiddenEls.push(firstHidden) - } - - hiddenEls.forEach(c => { c.style.display = 'none' }) - - // remove css height limit from the skillsWrap el - skillsWrapRef.current.classList.toggle('init', false) - - // if there are hidden skill pills, show the "+N more matched skills" pill - if (hiddenEls.length && restLabelRef.current) { - restLabelRef.current.innerText = `+${hiddenEls.length} more matched skill${hiddenEls.length > 1 ? 's' : ''}` - } - }, [matchedSkills]) - - return ( - -
- -
-
- {props.member.namesAndHandleAppearance !== NamesAndHandleAppearance.handleOnly && ( -
- {props.member.firstName} - {' '} - {props.member.lastName?.slice(0, 1) || ''} -
- )} - {props.member.namesAndHandleAppearance !== NamesAndHandleAppearance.nameOnly && ( -
- - {props.member.handle} - -
- )} - {(!!props.member.addresses?.length || !!props.member.homeCountryCode) && ( -
- - - {getAddrString(address.city, getCountry(props.member.homeCountryCode))} - -
- )} -
-
- -
-
-
-
-
- {`${matchedSkills.length} Matched skills`} -
-
-
+0 more matched skill
- {matchedSkills.length > 0 && matchedSkills.map(skill => ( - - ))} -
-
- - ) -} - -export default TalentCard diff --git a/src/apps/talent-search/src/components/talent-card/index.ts b/src/apps/talent-search/src/components/talent-card/index.ts deleted file mode 100644 index 2982b7c8d..000000000 --- a/src/apps/talent-search/src/components/talent-card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TalentCard } from './TalentCard' diff --git a/src/apps/talent-search/src/config/constants.ts b/src/apps/talent-search/src/config/constants.ts deleted file mode 100644 index b2716a613..000000000 --- a/src/apps/talent-search/src/config/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const SKILL_SEARCH_LIMIT = 7 -export const SKILL_SEARCH_MINIMUM = 2 diff --git a/src/apps/talent-search/src/config/index.ts b/src/apps/talent-search/src/config/index.ts deleted file mode 100644 index f87cf0102..000000000 --- a/src/apps/talent-search/src/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './constants' diff --git a/src/apps/talent-search/src/index.ts b/src/apps/talent-search/src/index.ts deleted file mode 100644 index 19db451f9..000000000 --- a/src/apps/talent-search/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { talentSearchRoutes, TALENT_SEARCH_PATHS } from './talent-search.routes' diff --git a/src/apps/talent-search/src/lib/models/Member.ts b/src/apps/talent-search/src/lib/models/Member.ts deleted file mode 100644 index b29e61658..000000000 --- a/src/apps/talent-search/src/lib/models/Member.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NamesAndHandleAppearance, UserSkill } from '~/libs/core' - -import MemberAddress from './MemberAddress' -import MemberMaxRating from './MemberMaxRating' -import MemberStats from './MemberStats' - -export default interface Member { - addresses: MemberAddress[]; - accountAge: number; - competitionCountryCode: string; - country: string; - createdAt: number; - description: string; - email: string; - skills: Array; - firstName: string; - handle: string; - homeCountryCode: string; - lastName: string; - namesAndHandleAppearance: NamesAndHandleAppearance - maxRating: MemberMaxRating; - numberOfChallengesPlaced: number; - numberOfChallengesWon: number; - photoURL: string; - skillScore: number; - stats: Array; - status: string; - userId: number; - verified: string; -} diff --git a/src/apps/talent-search/src/lib/models/MemberAddress.ts b/src/apps/talent-search/src/lib/models/MemberAddress.ts deleted file mode 100644 index 2137a2523..000000000 --- a/src/apps/talent-search/src/lib/models/MemberAddress.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface MemberAddress { - streetAddr1: string - streetAddr2: string - city: string - stateCode: string - zip: string -} diff --git a/src/apps/talent-search/src/lib/models/MemberDisplayName.ts b/src/apps/talent-search/src/lib/models/MemberDisplayName.ts deleted file mode 100644 index 4c9469275..000000000 --- a/src/apps/talent-search/src/lib/models/MemberDisplayName.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum MemberDisplayName { - both = 'namesAndHandle', - handleOnly = 'handleOnly', - nameOnly = 'namesOnly', -} diff --git a/src/apps/talent-search/src/lib/models/MemberMaxRating.ts b/src/apps/talent-search/src/lib/models/MemberMaxRating.ts deleted file mode 100644 index 69e70bb4a..000000000 --- a/src/apps/talent-search/src/lib/models/MemberMaxRating.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface MemberMaxRating { - rating:number; - track:string; - subTrack:string; - ratingColor:string; -} diff --git a/src/apps/talent-search/src/lib/models/MemberStats.ts b/src/apps/talent-search/src/lib/models/MemberStats.ts deleted file mode 100644 index 474bc9988..000000000 --- a/src/apps/talent-search/src/lib/models/MemberStats.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface MemberStats { - challenges:number; - wins:number; - mostRecentEventDate:number; - mostRecentEventName:string; - mostRecentSubmission:number; -} diff --git a/src/apps/talent-search/src/lib/models/index.ts b/src/apps/talent-search/src/lib/models/index.ts deleted file mode 100644 index 7ac93e125..000000000 --- a/src/apps/talent-search/src/lib/models/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { default as Member } from './Member' -export type { default as MemberMaxRating } from './MemberMaxRating' -export type { default as MemberStats } from './MemberStats' diff --git a/src/apps/talent-search/src/lib/services/index.ts b/src/apps/talent-search/src/lib/services/index.ts deleted file mode 100644 index cb5327857..000000000 --- a/src/apps/talent-search/src/lib/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './use-fetch-talent-matches' diff --git a/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.spec.ts b/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.spec.ts deleted file mode 100644 index 1f4f9f9a0..000000000 --- a/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { SKILL_SEARCH_MINIMUM } from '../../config' - -import { - canSearchTalentMatches, - isTalentSearchLoading, -} from './use-fetch-talent-matches' - -jest.mock('~/config', () => ({ - EnvironmentConfig: { - API: { - V6: 'https://api.topcoder.com/v6', - }, - }, -}), { virtual: true }) - -jest.mock('~/libs/core', () => ({ - xhrGetPaginatedAsync: jest.fn(), -}), { virtual: true }) - -jest.mock('swr', () => jest.fn()) - -const createSkills = (count: number): Array<{ id: string; name: string }> => ( - Array.from({ length: count }, (_, index) => ({ - id: `skill-${index + 1}`, - name: `Skill ${index + 1}`, - })) -) - -describe('useFetchTalentMatches', () => { - it('requires at least the configured minimum number of skills to search', () => { - expect(canSearchTalentMatches(createSkills(SKILL_SEARCH_MINIMUM - 1) as any)) - .toBe(false) - expect(canSearchTalentMatches(createSkills(SKILL_SEARCH_MINIMUM) as any)) - .toBe(true) - }) - - it('shows loading only when search can run and no data or error exists', () => { - expect(isTalentSearchLoading(true, undefined, undefined)) - .toBe(true) - expect(isTalentSearchLoading(false, undefined, undefined)) - .toBe(false) - expect(isTalentSearchLoading(true, { data: [] } as any, undefined)) - .toBe(false) - }) - - it('exits loading state when the search request fails', () => { - expect(isTalentSearchLoading(true, undefined, new Error('request failed'))) - .toBe(false) - }) -}) 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 deleted file mode 100644 index 5583abc85..000000000 --- a/src/apps/talent-search/src/lib/services/use-fetch-talent-matches.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { uniqBy } from 'lodash' -import { useCallback, useEffect, useMemo, useState } from 'react' -import useSWR, { SWRResponse } from 'swr' - -import type { PaginatedResponse, UserSkill } from '~/libs/core' -import { EnvironmentConfig } from '~/config' -import { xhrGetPaginatedAsync } from '~/libs/core' -import type Member from '@talentSearch/lib/models/Member' - -import { SKILL_SEARCH_MINIMUM } from '../../config' - -export interface TalentMatchesResponse { - error: boolean, - loading: boolean, - matches: Member[], - page: number, - ready: boolean, - total: number, - totalPages: number, -} - -export function canSearchTalentMatches(skills: ReadonlyArray): boolean { - return skills.length >= SKILL_SEARCH_MINIMUM -} - -export function isTalentSearchLoading( - canSearch: boolean, - data: PaginatedResponse | undefined, - error: unknown, -): boolean { - return canSearch && !error && !data?.data -} - -export function useFetchTalentMatches( - skills: ReadonlyArray, - page: number, - pageSize: number, -): TalentMatchesResponse { - const canSearch = canSearchTalentMatches(skills) - const searchParams = [ - ...skills.map(s => `id=${s.id}`), - 'sortBy=skillScore', - 'includeStats=false', - `page=${page}`, - `perPage=${pageSize}`, - ].join('&') - - const url = `${EnvironmentConfig.API.V6}/members/searchBySkills?${searchParams}` - - const { data, error }: SWRResponse, unknown> = useSWR( - url, - xhrGetPaginatedAsync, - { - isPaused: () => !canSearch, - refreshInterval: 0, - revalidateOnFocus: false, - }, - ) - - const matches = useMemo(() => data?.data ?? [], [data]) - - return { - error: !!error, - loading: isTalentSearchLoading(canSearch, data, error), - matches: matches ?? [], - page: data?.page ?? 0, - ready: !!data?.data, - total: data?.total ?? 0, - totalPages: data?.totalPages ?? 0, - } -} - -export interface InfiniteTalentMatchesResposne { - fetchNext: () => void - hasNext: boolean - matches: Member[] - page: number - loading: boolean - total: number -} - -export function useInfiniteTalentMatches( - skills: ReadonlyArray, - pageSize: number = 10, -): InfiniteTalentMatchesResposne { - const [matches, setMatches] = useState([] as Member[]) - const [page, setPage] = useState(1) - const matchResponse = useFetchTalentMatches(skills, page, pageSize) - - const fetchNext = useCallback(() => { - setPage(p => p + 1) - }, []) - - // clear matches when skills array is updated - useEffect(() => { - setMatches([]) - setPage(1) - }, [skills]) - - // when we have new matches, concatenate the response to the matches array - useEffect(() => { - setMatches(m => uniqBy([...m, ...matchResponse.matches], 'userId')) - }, [matchResponse.matches]) - - return { - fetchNext, - hasNext: matchResponse.page < matchResponse.totalPages, - loading: matchResponse.loading && skills.length > 0, - matches, - page, - total: matchResponse.total, - } -} diff --git a/src/apps/talent-search/src/lib/utils/index.ts b/src/apps/talent-search/src/lib/utils/index.ts deleted file mode 100644 index 68cf4184f..000000000 --- a/src/apps/talent-search/src/lib/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './skills.utils' diff --git a/src/apps/talent-search/src/lib/utils/search-query.tsx b/src/apps/talent-search/src/lib/utils/search-query.tsx deleted file mode 100644 index d55764fca..000000000 --- a/src/apps/talent-search/src/lib/utils/search-query.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import { useSearchParams } from 'react-router-dom' - -import { SearchUserSkill, UserSkill } from '~/libs/core' - -export const encodeUrlQuerySearch = (skills: SearchUserSkill[]): string => ( - skills - .map(s => `q=${encodeURIComponent(`${s.name}::${s.id}`)}`) - .join('&') -) - -export const parseUrlQuerySearch = (params: string[]): SearchUserSkill[] => ( - params.map(p => { - const [name, id] = p.split('::') - return { id, name } - }) -) - -export const useUrlQuerySearchParms = (paramName: string): [ - UserSkill[], - (s: SearchUserSkill[]) => void -] => { - const [params, updateParams] = useSearchParams() - - const [skills, setSkills] = useState([]) - - const handleUpdateSearch = useCallback((newSkills: SearchUserSkill[]) => { - const searchParams = encodeUrlQuerySearch(newSkills) - updateParams(`${searchParams}`) - }, [updateParams]) - - // update search input whenever the url data changes - useEffect(() => { - setSkills(parseUrlQuerySearch(params.getAll(paramName)) as UserSkill[]) - }, [params, paramName]) - - return [skills, handleUpdateSearch] -} diff --git a/src/apps/talent-search/src/lib/utils/skills.utils.tsx b/src/apps/talent-search/src/lib/utils/skills.utils.tsx deleted file mode 100644 index 8ad98a9c7..000000000 --- a/src/apps/talent-search/src/lib/utils/skills.utils.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useCallback } from 'react' - -import { UserSkill } from '~/libs/core' - -export type IsMatchingSkillFn = (skill: Pick) => boolean - -export const useIsMatchingSkill = (skills: UserSkill[]): IsMatchingSkillFn => { - const isMatchingSkill = useCallback((skill: Pick) => ( - !!skills.find(s => skill.id === s.id) - ), [skills]) - - return isMatchingSkill -} diff --git a/src/apps/talent-search/src/routes/search-page/SearchPage.module.scss b/src/apps/talent-search/src/routes/search-page/SearchPage.module.scss deleted file mode 100644 index 533fc2cba..000000000 --- a/src/apps/talent-search/src/routes/search-page/SearchPage.module.scss +++ /dev/null @@ -1,89 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.contentLayout { - background-image: url("../../assets/background.webp"); - background-size: cover; - background-repeat: no-repeat; - - @include ltemd { - background-image: url("../../assets/background-m.webp"); - } -} - -.contentLayout-inner { - padding: 86px $sp-12 $sp-12; - color: $tc-white; - @include ltemd { - padding: $sp-9 0 0; - } -} - -.searchHeader{ - text-align: center; -} - -.searchHeaderText{ - font-family: $font-roboto; - font-weight: 500; - font-size: 30px; - line-height: 32px; - letter-spacing: 0.5px; -} - -.subHeader{ - margin-top: 49px; - text-align: center; -} - -.subHeaderText{ - font-family: $font-barlow; - font-weight: 400; - font-size: 25px; - line-height: 30px; - color: $tc-white; - -} -.searchPrompt{ - font-family: $font-roboto; - font-weight: 700; - font-size: 16px; - line-height: 48px; - color: $tc-white; - display: block; - - @include ltemd { - line-height: 24px; - margin-bottom: $sp-2; - } -} - -.searchOptions{ - margin-left: auto; - margin-right: auto; - padding: 70px 32px 32px 32px; - max-width: 931px; - width: 100%; - - @include ltemd { - padding: 49px 0 0; - } -} - -.headerErrorWrap { - display: flex; - justify-content: center; -} - -.headerError { - display: flex; - align-items: center; - gap: $sp-3; - - padding: $sp-4 $sp-8; - border-radius: 10px; - background: $purple-140; - - @include ltemd { - padding: $sp-4; - } -} diff --git a/src/apps/talent-search/src/routes/search-page/SearchPage.tsx b/src/apps/talent-search/src/routes/search-page/SearchPage.tsx deleted file mode 100644 index d129fcc80..000000000 --- a/src/apps/talent-search/src/routes/search-page/SearchPage.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { FC, useRef, useState } from 'react' -import { useNavigate, useSearchParams } from 'react-router-dom' - -import type { UserSkill } from '~/libs/core' -import { ContentLayout, IconOutline } from '~/libs/ui' - -import { SearchInput } from '../../components/search-input' -import { PopularSkills } from '../../components/popular-skills' -import { TALENT_SEARCH_PATHS } from '../../talent-search.routes' -import { SKILL_SEARCH_MINIMUM } from '../../config' -import { encodeUrlQuerySearch } from '../../lib/utils/search-query' - -import styles from './SearchPage.module.scss' - -export const SearchPage: FC = () => { - const [params] = useSearchParams() - const isMissingProfileRoute = params.get('memberNotFound') !== null - - const searchInputRef = useRef() - const navigate = useNavigate() - const [skillsFilter, setSkillsFilter] = useState([]) - - function navigateToResults(): void { - if (skillsFilter.length < SKILL_SEARCH_MINIMUM) { - return - } - - const searchParams = encodeUrlQuerySearch(skillsFilter) - navigate(`${TALENT_SEARCH_PATHS.results}?${searchParams}`) - } - - function handleSelectSkillFilter(filter: Pick[]): void { - setSkillsFilter(filter as UserSkill[]) - searchInputRef.current?.focus() - } - - function renderHeader(): JSX.Element { - return isMissingProfileRoute ? ( - <> -
-
- - We were unable to locate that profile -
-
-
-
- You can also try finding members through our Talent Search: -
-
- - ) : ( - <> -
- Looking for a technology expert? -
-
-
- Search thousands of skills to match with our global experts. -
-
- - ) - } - - return ( - - {renderHeader()} -
- Search by skills - -
- -
- ) -} - -export default SearchPage diff --git a/src/apps/talent-search/src/routes/search-page/index.ts b/src/apps/talent-search/src/routes/search-page/index.ts deleted file mode 100644 index 983e5d359..000000000 --- a/src/apps/talent-search/src/routes/search-page/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as SearchPage } from './SearchPage' diff --git a/src/apps/talent-search/src/routes/search-results-page/SearchResultsPage.module.scss b/src/apps/talent-search/src/routes/search-results-page/SearchResultsPage.module.scss deleted file mode 100644 index 6364f7c45..000000000 --- a/src/apps/talent-search/src/routes/search-results-page/SearchResultsPage.module.scss +++ /dev/null @@ -1,127 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.headerWrap { - min-height: 156px; -} - -.headerContentLayout { - height: 100%; - background-image: url("../../assets/background.jpg"); - background-size: cover; - background-repeat: no-repeat; - position: relative; - z-index: 2; - - &Inner { - padding: 0 $sp-8; - } - - @include ltemd { - &Inner { - padding: 0; - } - } -} - -.summaryText { - margin: $sp-4 0 $sp-5; - text-align: center; - color: $black-60; - - display: flex; - align-items: center; - justify-content: center; - - > span { - display: block; - margin: 0 auto; - flex: 1 1 auto; - } - - :global(.highlighting) { - color: $black-100; - } - - @include ltemd { - flex-direction: column; - gap: $sp-2; - } -} - -.skillsPill { - flex: 0 0 auto; - - &:first-child { - opacity: 0; - visibility: hidden; - pointer-events: none; - } - - @include ltemd { - &:first-child { - display: none; - } - } -} - -.searchInput { - margin-top: $sp-10; - @include ltemd { - margin-top: $sp-4; - } -} - -.contentLayout { - background: $black-5; - padding: $sp-2; - position: relative; - z-index: 1; - - .contentLayout-inner { - padding-bottom: $sp-12; - } - - @include ltemd { - padding: $sp-2 0; - } -} - -.resultsWrap { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: $sp-9; - - @include ltelg { - display: flex; - flex-wrap: wrap; - } - @include ltemd { - gap: $sp-5; - } -} - -.moreBtn { - margin-top: $sp-8; - display: flex; - justify-content: center; -} - -.noResultsNormal { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - font-size: 20px; - line-height: 23px; - text-align: center; -} - -.noResultsBold { - font-family: 'Roboto'; - font-style: normal; - font-weight: 700; - font-size: 20px; - line-height: 23px; - text-align: center; - margin-top: 20px; - margin-bottom: 40px; -} \ No newline at end of file diff --git a/src/apps/talent-search/src/routes/search-results-page/SearchResultsPage.tsx b/src/apps/talent-search/src/routes/search-results-page/SearchResultsPage.tsx deleted file mode 100644 index e96405d10..000000000 --- a/src/apps/talent-search/src/routes/search-results-page/SearchResultsPage.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { FC, useCallback, useEffect, useState } from 'react' -import classNames from 'classnames' - -import { EnvironmentConfig } from '~/config' -import { Button, ContentLayout, LinkButton, LoadingCircles } from '~/libs/ui' -import { HowSkillsWorkModal } from '~/libs/shared' - -import { TalentCard } from '../../components/talent-card' -import { SearchInput } from '../../components/search-input' -import { - InfiniteTalentMatchesResposne, - useInfiniteTalentMatches, -} from '../../lib/services' -import { useUrlQuerySearchParms } from '../../lib/utils/search-query' -import { SKILL_SEARCH_MINIMUM } from '../../config' - -import { getLetsTalkUrl } from './letsTalkUrl' -import styles from './SearchResultsPage.module.scss' - -const SearchResultsPage: FC = () => { - const [showSkillsModal, setShowSkillsModal] = useState(false) - - const [currentPage, setCurrentPage] = useState(1) - const itemsPerPage = 10 - - const [skills, setSkills] = useUrlQuerySearchParms('q') - const { - loading, - matches, - fetchNext, - hasNext, - total, - }: InfiniteTalentMatchesResposne = useInfiniteTalentMatches(skills) - const paginatedMatches = matches.slice(0, currentPage * itemsPerPage) - - useEffect(() => { - const handleScroll: () => void = () => { - const scrollY = window.scrollY - const visibleHeight = window.innerHeight - const fullHeight = document.body.scrollHeight - const footerElem = document.getElementById('footer-nav-el') - const footerHeight = (footerElem && footerElem.offsetHeight) || 650 - if (scrollY + visibleHeight >= fullHeight - (footerHeight + 100)) { - // Scroll near bottom - setCurrentPage(prev => { - const maxPages = Math.ceil(matches.length / itemsPerPage) - return prev < maxPages ? prev + 1 : prev - }) - } - } - - window.addEventListener('scroll', handleScroll) - return () => window.removeEventListener('scroll', handleScroll) - }, [matches]) - - const toggleSkillsModal = useCallback(() => setShowSkillsModal(s => !s), []) - - const skillsModalTriggerBtn = ( -
-
- ) - - return ( - <> -
- - - -
- -
- {loading ? ( - <> - Finding experts that match your search... - - ) : !skills.length ? ( - - Search thousands of skills to match with our global experts. - - ) : skills.length < SKILL_SEARCH_MINIMUM ? ( - - {`Please select at least ${SKILL_SEARCH_MINIMUM} skills to search`} - - ) : !total ? ( - -
- Topcoder is supported by amazing talent from all over the world. -
-
- Please contact us, so we can connect you to experts with these skills. -
-
- -
-
- ) : ( - <> - {skillsModalTriggerBtn} - - We found  - - {total} -  Experts - -  that match your search - - {skillsModalTriggerBtn} - - )} -
-
- {paginatedMatches.map(member => ( - - ))} -
- {loading && } - - {hasNext && ( -
- -
- )} - {showSkillsModal && ( - - )} -
- - ) -} - -export default SearchResultsPage diff --git a/src/apps/talent-search/src/routes/search-results-page/index.ts b/src/apps/talent-search/src/routes/search-results-page/index.ts deleted file mode 100644 index 2f36f2509..000000000 --- a/src/apps/talent-search/src/routes/search-results-page/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as SearchResultsPage } from './SearchResultsPage' diff --git a/src/apps/talent-search/src/routes/search-results-page/letsTalkUrl.spec.ts b/src/apps/talent-search/src/routes/search-results-page/letsTalkUrl.spec.ts deleted file mode 100644 index 76e6110a3..000000000 --- a/src/apps/talent-search/src/routes/search-results-page/letsTalkUrl.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getLetsTalkUrl } from './letsTalkUrl' - -describe('getLetsTalkUrl', () => { - it('returns the lets-talk url for dev domain', () => { - expect(getLetsTalkUrl('https://www.topcoder-dev.com')) - .toBe('https://www.topcoder-dev.com/lets-talk') - }) - - it('returns the lets-talk url for prod domain', () => { - expect(getLetsTalkUrl('https://www.topcoder.com')) - .toBe('https://www.topcoder.com/lets-talk') - }) -}) diff --git a/src/apps/talent-search/src/routes/search-results-page/letsTalkUrl.ts b/src/apps/talent-search/src/routes/search-results-page/letsTalkUrl.ts deleted file mode 100644 index 381043919..000000000 --- a/src/apps/talent-search/src/routes/search-results-page/letsTalkUrl.ts +++ /dev/null @@ -1 +0,0 @@ -export const getLetsTalkUrl = (topcoderUrl: string): string => `${topcoderUrl}/lets-talk` diff --git a/src/apps/talent-search/src/routes/talent-page/TalentPage.module.scss b/src/apps/talent-search/src/routes/talent-page/TalentPage.module.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx b/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx deleted file mode 100644 index ac77bc9c8..000000000 --- a/src/apps/talent-search/src/routes/talent-page/TalentPage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FC, ReactNode } from 'react' -import { Location, useLocation } from 'react-router-dom' - -import { MemberProfileContext, MemberProfilePage } from '@profiles/member-profile' -import { UserSkill } from '~/libs/core' - -import { ProfileSkillsMatch } from '../../components/profile-skills-match' -import { getTalentStatsRoute } from '../../talent-search.routes' - -const TalentPage: FC = () => { - const { state }: Location = useLocation() - - function skillsRenderer(profileSkills: Pick[]): ReactNode { - return ( - - ) - } - - return ( - - - - ) -} - -export default TalentPage diff --git a/src/apps/talent-search/src/routes/talent-page/index.ts b/src/apps/talent-search/src/routes/talent-page/index.ts deleted file mode 100644 index a58ff9dfc..000000000 --- a/src/apps/talent-search/src/routes/talent-page/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TalentPage } from './TalentPage' diff --git a/src/apps/talent-search/src/styles/main.vendor.scss b/src/apps/talent-search/src/styles/main.vendor.scss deleted file mode 100644 index be351bb49..000000000 --- a/src/apps/talent-search/src/styles/main.vendor.scss +++ /dev/null @@ -1,5 +0,0 @@ -// This file can import 3rd parties css/scss files globally -// without applying CSS Modules - -@import "~react-redux-toastr/src/styles/index"; -@import "~react-responsive-modal/styles"; diff --git a/src/apps/talent-search/src/talent-search.routes.tsx b/src/apps/talent-search/src/talent-search.routes.tsx deleted file mode 100644 index 021eaca94..000000000 --- a/src/apps/talent-search/src/talent-search.routes.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config' -import { lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core' - -import './styles/main.vendor.scss' - -const TalentSearchAppRoot: LazyLoadedComponent = lazyLoad(() => import('./TalentSearchApp')) -const SearchPage: LazyLoadedComponent = lazyLoad(() => import('./routes/search-page'), 'SearchPage') -const SearchResultsPage: LazyLoadedComponent = lazyLoad( - () => import('./routes/search-results-page'), - 'SearchResultsPage', -) -const TalentPage: LazyLoadedComponent = lazyLoad( - () => import('./routes/talent-page'), - 'TalentPage', -) -const MemberBadgesPage: LazyLoadedComponent = lazyLoad( - () => import('@profiles/member-badges'), - 'MemberBadgesPage', -) - -const isOnAppSubdomain = EnvironmentConfig.SUBDOMAIN === AppSubdomain.talentSearch -export const rootRoute: string = ( - isOnAppSubdomain ? '' : `/${AppSubdomain.talentSearch}` -) - -export const TALENT_SEARCH_PATHS = { - absoluteUrl: `//${AppSubdomain.talentSearch}.${EnvironmentConfig.TC_DOMAIN}`, - results: `${rootRoute}/results`, - root: rootRoute, - talent: `${rootRoute}/talent`, -} - -export const toolTitle: string = ToolTitle.talentSearch - -const isAdminRestricted = EnvironmentConfig.RESTRICT_TALENT_SEARCH - -export const getTalentRoute = (userHandle: string): string => ( - `${rootRoute}/talent/${userHandle.toLowerCase()}` -) - -export const getTalentStatsRoute = ( - userHandle: string, - track?: string, - subTrack?: string, -): string => ( - `${getTalentRoute(userHandle)}${track ? `/stats/${track}` : ''}${!(track && subTrack) ? '' : `/${subTrack}`}` -) - -export const talentSearchRoutes: ReadonlyArray = [ - { - authRequired: isAdminRestricted, - children: [ - { - element: , - route: '/', - }, - { - element: , - route: '/results', - }, - { - element: , - route: '/talent/:memberHandle', - }, - { - element: , - id: 'MemberProfilePageSub', - route: '/talent/:memberHandle/stats/*', - }, - { - element: , - id: 'MemberBadgesPage', - route: '/talent/:memberHandle/badges', - }, - ], - domain: AppSubdomain.talentSearch, - element: , - id: toolTitle, - navConfig: { - showSalesCta: true, - }, - rolesRequired: isAdminRestricted ? [ - UserRole.administrator, - ] : undefined, - route: rootRoute, - }, -] diff --git a/src/config/constants.ts b/src/config/constants.ts index 4287114b3..e81b79395 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -6,7 +6,6 @@ export enum AppSubdomain { tcAcademy = 'academy', onboarding = 'onboarding', work = 'work', - talentSearch = 'talent-search', wallet = 'wallet', walletAdmin = 'wallet-admin', copilots = 'copilots', @@ -27,7 +26,6 @@ export enum ToolTitle { selfService = 'Self Service Challenges', onboarding = ' ', work = 'Work', - talentSearch = 'Expert Talent', wallet = 'Wallet', walletAdmin = 'Wallet Admin', copilots = 'Copilots', diff --git a/tsconfig.paths.json b/tsconfig.paths.json index 2e0fc3ec9..54e6a260e 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -21,9 +21,6 @@ "@platform/*": [ "./src/apps/platform/src/*" ], - "@talentSearch/*": [ - "./src/apps/talent-search/src/*" - ], "@profiles/*": [ "./src/apps/profiles/src/*" ],