Skip to content

Commit 58f55e8

Browse files
committed
Initial talent search implementation
1 parent 4d67342 commit 58f55e8

38 files changed

+1248
-71
lines changed

craco.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = {
3434
'@learn': resolve('src/apps/learn/src'),
3535
'@devCenter': resolve('src/apps/dev-center/src'),
3636
'@gamificationAdmin': resolve('src/apps/gamification-admin/src'),
37+
'@talentSearch': resolve('src/apps/talent-search/src'),
3738

3839
'@platform': resolve('src/apps/platform/src'),
3940
// aliases used in SCSS files

src/apps/platform/src/platform.routes.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { devCenterRoutes } from '~/apps/dev-center'
44
import { gamificationAdminRoutes } from '~/apps/gamification-admin'
55
import { earnRoutes } from '~/apps/earn'
66
import { selfServiceRoutes } from '~/apps/self-service'
7+
import { talentSearchRoutes } from '~/apps/talent-search'
78

89
const Home: LazyLoadedComponent = lazyLoad(() => import('./routes/home'), 'HomePage')
910

@@ -24,5 +25,6 @@ export const platformRoutes: Array<PlatformRoute> = [
2425
...earnRoutes,
2526
...learnRoutes,
2627
...gamificationAdminRoutes,
27-
...homeRoutes,
28+
...talentSearchRoutes,
29+
...homeRoutes
2830
]

src/apps/talent-search/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
### Talent search
2+
3+
This is an internal for finding members based on skills and other search facets. The main APIs used include:
4+
5+
* Member API
6+
* Match Engine API

src/apps/talent-search/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './src'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FC, useContext } from 'react'
2+
import { Outlet, Routes } from 'react-router-dom'
3+
4+
import { routerContext, RouterContextData } from '~/libs/core'
5+
6+
import { toolTitle } from './talent-search.routes'
7+
8+
const TalentSearchApp: FC<{}> = () => {
9+
10+
const { getChildRoutes }: RouterContextData = useContext(routerContext)
11+
12+
return (
13+
<>
14+
<Outlet />
15+
<Routes>
16+
{getChildRoutes(toolTitle)}
17+
</Routes>
18+
</>
19+
)
20+
}
21+
22+
export default TalentSearchApp

src/apps/talent-search/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './talent-search.routes'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import SkillScore from './SkillScore'
2+
3+
export default interface Member {
4+
userId: number;
5+
handle: string;
6+
firstName: string;
7+
lastName: string;
8+
country: string;
9+
accountAge: number;
10+
numberOfChallengesWon: number;
11+
numberOfChallengesPlaced: number;
12+
skills: Array<SkillScore>;
13+
searchedSkills: Array<SkillScore>;
14+
roles: Array<string>;
15+
domains: Array<string>;
16+
totalSkillScore: number;
17+
searchedSkillScore: number;
18+
}
19+
20+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default interface Skill {
2+
id: string;
3+
skillName: string;
4+
description: string;
5+
}
6+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
export default interface SkillScore {
3+
skill: string;
4+
score: number;
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { EnvironmentConfig } from '~/config'
2+
import { xhrGetAsync } from '~/libs/core'
3+
4+
import Skill from '@talentSearch/lib/models/Skill'
5+
import Member from '@talentSearch/lib/models/Member'
6+
7+
export async function getAllSkills(): Promise<Array<Skill>>{
8+
return xhrGetAsync(`${EnvironmentConfig.API.V1}/match-engine/skills`)
9+
}
10+
11+
export async function retrieveMatchesForSkills(skills:ReadonlyArray<Skill>): Promise<Array<Member>>{
12+
const params = new URLSearchParams()
13+
console.log("Search skills: " + JSON.stringify(skills))
14+
skills.forEach(value => params.append('skill', value.skillName))
15+
params.append('sortBy', 'numberOfChallengesWon')
16+
params.append('sortOrder', 'desc')
17+
18+
const url = `${EnvironmentConfig.API.V1}/match-engine/members?${params.toString()}`
19+
20+
return xhrGetAsync(url)
21+
}
22+
23+
const MatcherService = {
24+
getAllSkills,
25+
retrieveMatchesForSkills,
26+
};
27+
28+
export default MatcherService;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@import "@libs/ui/styles/includes";
2+
@import '@talentSearch/styles/variables';
3+
4+
.contentLayout {
5+
width: 100%;
6+
padding-top: 30px;
7+
8+
.contentLayout-outer {
9+
width: 100%;
10+
11+
.contentLayout-inner {
12+
width: 100%;
13+
overflow: visible;
14+
}
15+
}
16+
}
17+
18+
.header{
19+
padding-bottom: 20px;
20+
}
21+
22+
.options{
23+
max-width: 600px;
24+
padding-bottom: 30px;
25+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {
2+
FC,
3+
useEffect,
4+
useState,
5+
} from 'react'
6+
7+
import { ContentLayout, LoadingSpinner } from '~/libs/ui'
8+
import SkillSearchResults from './components/skill-search-results/SkillSearchResults'
9+
import Skill from '@talentSearch/lib/models/Skill'
10+
import Member from '@talentSearch/lib/models/Member'
11+
import Select from 'react-select';
12+
13+
import MatcherService from '@talentSearch/lib/services/MatcherService'
14+
15+
import styles from './TalentSearch.module.scss'
16+
17+
export const TalentSearch: FC = () => {
18+
const [allSkills, setAllSkills] = useState<Array<Skill>>([]);
19+
const [skillsFilter, setSkillsFilter] = useState<ReadonlyArray<Skill>>([]);
20+
const [searchResults, setSearchResults] = useState<Array<Member>>([]);
21+
const [isLoading, setIsLoading] = useState<boolean>(false);
22+
23+
useEffect(() => {
24+
retrieveAllSkills();
25+
}, []);
26+
27+
const retrieveAllSkills = () => {
28+
MatcherService.getAllSkills()
29+
.then((response: Array<Skill>) => {
30+
setAllSkills(response);
31+
32+
console.log(JSON.stringify(allSkills));
33+
})
34+
.catch((e: Error) => {
35+
console.log(e);
36+
});
37+
};
38+
39+
function retrieveMatches(filter:ReadonlyArray<Skill>){
40+
setIsLoading(true);
41+
setSearchResults([]);
42+
MatcherService.retrieveMatchesForSkills(filter)
43+
.then((response: Array<Member>) => {
44+
if(response){
45+
response.forEach(function (value){
46+
//The service doesn't always return all fields, so clean up the data a bit
47+
if(!value.numberOfChallengesPlaced){
48+
value.numberOfChallengesPlaced = 0
49+
}
50+
if(!value.numberOfChallengesWon){
51+
value.numberOfChallengesWon = 0
52+
}
53+
if(!value.country){
54+
value.country="-"
55+
}
56+
57+
//totalSkillScore holds the total scoe for *all* skills associated with this member, regardless of if
58+
//they are applicable against the searched skills
59+
value.totalSkillScore = 0
60+
61+
//searchedSkillScore holds the total score for all searched skills (Javascript, HTML, CSS, for example)
62+
//for this particular member.
63+
value.searchedSkillScore = 0
64+
value.searchedSkills = []
65+
if(value.skills){
66+
//This isn't super efficient, but should be OK for now
67+
//Here, we summarise the total score and collect an array of *just* the searched
68+
//skills, for aggregation so that the user can differentiate between all skills
69+
//for a particular member and the ones that are actively being searched for
70+
value.skills.forEach(function(skill){
71+
value.totalSkillScore += skill.score
72+
73+
filter.forEach(function(searched){
74+
if(skill.skill == searched.skillName){
75+
value.searchedSkillScore += skill.score
76+
value.searchedSkills.push(skill)
77+
}
78+
})
79+
})
80+
}
81+
})
82+
}
83+
setSearchResults(response);
84+
setIsLoading(false);
85+
})
86+
.catch((e: Error) => {
87+
console.log(e);
88+
});
89+
}
90+
91+
if(!allSkills || allSkills.length==0){
92+
return (<LoadingSpinner />)
93+
}
94+
95+
return (
96+
<ContentLayout
97+
contentClass={styles.contentLayout}
98+
outerClass={styles['contentLayout-outer']}
99+
innerClass={styles['contentLayout-inner']}
100+
>
101+
<div className={styles['header']}>
102+
<h2>Talent search</h2>
103+
</div>
104+
<div className={styles['options']}>
105+
<h4>Select Skills:</h4>
106+
107+
<Select
108+
isMulti
109+
name="skills"
110+
className="basic-multi-select"
111+
classNamePrefix="select"
112+
getOptionLabel={(skill: Skill) => skill.skillName}
113+
getOptionValue={(skill: Skill) => skill.id}
114+
options={allSkills}
115+
onChange={(option: readonly Skill[]) => {
116+
setSkillsFilter(option);
117+
retrieveMatches(option);
118+
}}
119+
/>
120+
</div>
121+
122+
123+
<SkillSearchResults
124+
results={searchResults}
125+
skillsFilter={skillsFilter}
126+
isLoading={isLoading}
127+
/>
128+
129+
</ContentLayout>
130+
)
131+
}
132+
133+
export default TalentSearch

src/apps/talent-search/src/routes/talent-search/components/skill-search-results/SkillSearchResults.module.scss

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
2+
import { Component } from 'react'
3+
import Skill from '@talentSearch/lib/models/Skill'
4+
import Member from '@talentSearch/lib/models/Member'
5+
import { LoadingCircles, Table, TableColumn } from '~/libs/ui'
6+
import MemberHandleRenderer from './renderers/MemberHandleRenderer'
7+
8+
import styles from './SkillSearchResults.module.scss'
9+
import MemberSkillsRenderer from './renderers/MemberSkillsRenderer'
10+
11+
export const memberColumns: ReadonlyArray<TableColumn<Member>> = [
12+
{
13+
label: 'Handle',
14+
propertyName: 'handle',
15+
type: 'element',
16+
renderer: MemberHandleRenderer,
17+
},
18+
{
19+
label: 'Searched Skill Score',
20+
propertyName: 'searchedSkillScore',
21+
type: 'numberElement',
22+
renderer: MemberSkillsRenderer,
23+
defaultSortDirection: 'desc',
24+
isDefaultSort: true,
25+
},
26+
{
27+
label: 'Challenges Won',
28+
propertyName: 'numberOfChallengesWon',
29+
type: 'number',
30+
},
31+
{
32+
label: 'Challenges Placed',
33+
propertyName: 'numberOfChallengesPlaced',
34+
type: 'number',
35+
},
36+
{
37+
label: 'First Name',
38+
propertyName: 'firstName',
39+
type: 'text',
40+
},
41+
{
42+
label: 'Last Name',
43+
propertyName: 'lastName',
44+
type: 'text',
45+
},
46+
{
47+
label: 'Country',
48+
propertyName: 'country',
49+
type: 'text',
50+
},
51+
{
52+
label: 'Total Skill Score',
53+
propertyName: 'totalSkillScore',
54+
type: 'number',
55+
renderer: MemberSkillsRenderer,
56+
}
57+
]
58+
59+
type SkillSearchResultsProps = {
60+
results:ReadonlyArray<Member>
61+
skillsFilter?:ReadonlyArray<Skill>
62+
isLoading?:boolean
63+
}
64+
65+
export default class SkillSearchResult extends Component<SkillSearchResultsProps>{
66+
67+
render() {
68+
console.log("results:" + this.props.results + this.props.results.length)
69+
console.log("skillsFilter:" + this.props.skillsFilter + this.props.skillsFilter!.length)
70+
console.log("Loading:",this.props.isLoading)
71+
//If we searched and have no results, show "No results found", otherwise hide the results table
72+
//until a search has been made
73+
if(this.props.isLoading){
74+
return (<LoadingCircles />)
75+
}
76+
else if(this.props.skillsFilter &&
77+
this.props.skillsFilter.length>0 &&
78+
(!this.props.results ||
79+
this.props.results.length==0)){
80+
//TODO - fill this in with useful no results found
81+
return (<div>No results found</div>)
82+
}
83+
else if(!this.props.skillsFilter ||
84+
this.props.skillsFilter.length==0){
85+
return (<div></div>)
86+
}
87+
88+
return (
89+
<div>
90+
<Table
91+
data={this.props.results}
92+
columns={memberColumns}
93+
/>
94+
</div>
95+
)
96+
}
97+
}
98+

0 commit comments

Comments
 (0)