Skip to content

Commit 2945496

Browse files
authored
Merge pull request #10 from NextStepFinalProject/NXD-14-Linkedin-jobs-integration
added linkedin jobs integration
2 parents 4638866 + 4384cb2 commit 2945496

11 files changed

Lines changed: 633 additions & 5 deletions

File tree

nextstep-backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"type": "git",
1414
"url": "git+https://github.com/NextStepFinalProject/NextStep.git"
1515
},
16-
"author": "Mevorah Berrebi & Tal Jacob & Lina Elman & Liav Tibi",
16+
"author": "Tal Jacob & Lina Elman & Liav Tibi",
1717
"license": "ISC",
1818
"bugs": {
1919
"url": "https://github.com/NextStepFinalProject/NextStep/issues"
@@ -39,6 +39,7 @@
3939
"jest-junit": "^16.0.0",
4040
"js-yaml": "^4.1.0",
4141
"jsonwebtoken": "^9.0.2",
42+
"linkedin-jobs-api": "^1.0.6",
4243
"mammoth": "^1.9.0",
4344
"mongoose": "^8.8.2",
4445
"multer": "^1.4.5-lts.1",

nextstep-backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import loadOpenApiFile from "./openapi/openapi_loader";
1616
import resource_routes from './routes/resources_routes';
1717
import resume_routes from './routes/resume_routes';
1818
import githubRoutes from './routes/github_routes';
19+
import linkedinJobsRoutes from './routes/linkedin_jobs_routes';
1920

2021
const specs = swaggerJsdoc(options);
2122

@@ -76,5 +77,6 @@ app.use('/resource', resource_routes);
7677
app.use('/room', roomsRoutes);
7778
app.use('/resume', resume_routes);
7879
app.use('/github', githubRoutes);
80+
app.use('/linkedin-jobs', linkedinJobsRoutes);
7981

8082
export { app, corsOptions };
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Request, Response } from 'express';
2+
import linkedIn from 'linkedin-jobs-api';
3+
import { getCompanyLogo } from '../services/company_logo_service';
4+
5+
export const getJobsBySkillsAndRole = async (req: Request, res: Response) => {
6+
try {
7+
const skillsParam = String(req.query.skills || '').trim();
8+
const role = String(req.query.role || '').trim();
9+
const location = String(req.query.location || 'Israel').trim();
10+
const dateSincePosted = String(req.query.dateSincePosted || 'past week').trim();
11+
const jobType = String(req.query.jobType || 'full time').trim();
12+
const experienceLevel = String(req.query.experienceLevel || 'entry level').trim();
13+
14+
if (!skillsParam || !role) {
15+
return res.status(400).json({ error: 'Skills and role are required' });
16+
}
17+
18+
// Split skills by comma and trim whitespace
19+
const skillsArray = skillsParam
20+
.split(',')
21+
.map(skill => skill.trim())
22+
.filter(Boolean);
23+
24+
// Construct keyword by combining role and skills
25+
const keyword = `${role} ${skillsArray.join(' ')}`.trim();
26+
27+
const queryOptions = {
28+
keyword,
29+
location,
30+
dateSincePosted,
31+
jobType,
32+
experienceLevel,
33+
limit: '10',
34+
page: '0',
35+
};
36+
37+
const jobs = await linkedIn.query(queryOptions);
38+
39+
// Fetch company logos for each job
40+
const jobsWithLogos = await Promise.all(
41+
jobs.map(async (job: any) => {
42+
const companyLogo = await getCompanyLogo(job.company);
43+
return {
44+
...job,
45+
companyLogo,
46+
position: job.position // Add position field
47+
};
48+
})
49+
);
50+
51+
res.status(200).json(jobsWithLogos);
52+
} catch (error: any) {
53+
console.error('Error fetching jobs from LinkedIn Jobs API:', error.message);
54+
res.status(500).json({ error: 'Failed to fetch jobs from LinkedIn Jobs API' });
55+
}
56+
};
57+
58+
export const viewJobDetails = async (req: Request, res: Response) => {
59+
try {
60+
const jobId = req.params.id;
61+
if (!jobId) {
62+
return res.status(400).json({ error: 'Job ID is required' });
63+
}
64+
65+
const jobDetails = await linkedIn.query({ keyword: jobId, limit: '1' });
66+
res.status(200).json(jobDetails);
67+
} catch (error: any) {
68+
console.error('Error fetching job details:', error.message);
69+
res.status(500).json({ error: 'Failed to fetch job details' });
70+
}
71+
};

nextstep-backend/src/openapi/swagger.yaml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ tags:
1919
description: Operations related to chat rooms
2020
- name: Resume
2121
description: Operations related to resume ATS scoring
22+
- name: LinkedIn Jobs
23+
description: Operations related to LinkedIn job postings
2224

2325
paths:
2426
/post:
@@ -1114,6 +1116,135 @@ paths:
11141116
'400':
11151117
description: Bad Request
11161118

1119+
/linkedin-jobs/jobs:
1120+
get:
1121+
tags:
1122+
- LinkedIn Jobs
1123+
summary: Retrieve jobs from LinkedIn based on skills and role
1124+
parameters:
1125+
- name: skills
1126+
in: query
1127+
required: true
1128+
schema:
1129+
type: string
1130+
description: Comma-separated list of skills (maximum 3)
1131+
- name: role
1132+
in: query
1133+
required: true
1134+
schema:
1135+
type: string
1136+
description: Desired role
1137+
- name: location
1138+
in: query
1139+
required: false
1140+
schema:
1141+
type: string
1142+
description: Job location
1143+
- name: dateSincePosted
1144+
in: query
1145+
required: false
1146+
schema:
1147+
type: string
1148+
description: Date range for job postings (e.g., "past day", "past week", "past month")
1149+
- name: jobType
1150+
in: query
1151+
required: false
1152+
schema:
1153+
type: string
1154+
description: Type of job (e.g., "full time", "part time", "contract")
1155+
- name: experienceLevel
1156+
in: query
1157+
required: false
1158+
schema:
1159+
type: string
1160+
description: Experience level (e.g., "entry level", "mid level", "senior level", "all")
1161+
responses:
1162+
'200':
1163+
description: List of jobs retrieved successfully
1164+
content:
1165+
application/json:
1166+
schema:
1167+
type: array
1168+
items:
1169+
type: object
1170+
properties:
1171+
position:
1172+
type: string
1173+
description: Job position
1174+
company:
1175+
type: string
1176+
description: Company name
1177+
location:
1178+
type: string
1179+
description: Job location
1180+
jobUrl:
1181+
type: string
1182+
description: Job posting URL
1183+
companyLogo:
1184+
type: string
1185+
description: URL of the company logo
1186+
date:
1187+
type: string
1188+
description: Date the job was posted
1189+
salary:
1190+
type: string
1191+
description: Salary information
1192+
'400':
1193+
description: Bad request - Missing skills or role
1194+
'500':
1195+
description: Internal server error
1196+
1197+
/linkedin-jobs/jobs/{id}:
1198+
get:
1199+
tags:
1200+
- LinkedIn Jobs
1201+
summary: Retrieve details of a specific job by ID
1202+
parameters:
1203+
- name: id
1204+
in: path
1205+
required: true
1206+
schema:
1207+
type: string
1208+
description: Job ID
1209+
responses:
1210+
'200':
1211+
description: Job details retrieved successfully
1212+
content:
1213+
application/json:
1214+
schema:
1215+
type: object
1216+
properties:
1217+
position:
1218+
type: string
1219+
description: Job position
1220+
company:
1221+
type: string
1222+
description: Company name
1223+
location:
1224+
type: string
1225+
description: Job location
1226+
description:
1227+
type: string
1228+
description: Detailed job description
1229+
jobUrl:
1230+
type: string
1231+
description: Job posting URL
1232+
companyLogo:
1233+
type: string
1234+
description: URL of the company logo
1235+
date:
1236+
type: string
1237+
description: Date the job was posted
1238+
salary:
1239+
type: string
1240+
description: Salary information
1241+
'400':
1242+
description: Bad request - Missing job ID
1243+
'404':
1244+
description: Job not found
1245+
'500':
1246+
description: Internal server error
1247+
11171248
components:
11181249
schemas:
11191250
Post:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import express from 'express';
2+
import { getJobsBySkillsAndRole, viewJobDetails } from '../controllers/linkedin_jobs_controller';
3+
4+
const router = express.Router();
5+
6+
router.get('/jobs', getJobsBySkillsAndRole);
7+
router.get('/jobs/:id', viewJobDetails);
8+
9+
export default router;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import axios from 'axios';
2+
3+
export const getCompanyLogo = async (companyName: string): Promise<string | null> => {
4+
try {
5+
const response = await axios.get(`https://logo.clearbit.com/${encodeURIComponent(companyName)}.com`);
6+
return response.status === 200 ? response.config.url ?? null : null;
7+
} catch (error) {
8+
if (error instanceof Error) {
9+
console.error(`Failed to fetch logo for company: ${companyName}`, error.message);
10+
} else {
11+
console.error(`Failed to fetch logo for company: ${companyName}`, error);
12+
}
13+
return null;
14+
}
15+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
declare module 'linkedin-jobs-api' {
2+
interface Job {
3+
position: string;
4+
company: string;
5+
location: string;
6+
date: string;
7+
// Add other relevant fields as needed
8+
}
9+
10+
interface QueryOptions {
11+
keyword: string;
12+
location?: string;
13+
dateSincePosted?: string;
14+
jobType?: string;
15+
remoteFilter?: string;
16+
salary?: string;
17+
experienceLevel?: string;
18+
limit?: string;
19+
page?: string;
20+
}
21+
22+
function query(options: QueryOptions): Promise<Job[]>;
23+
24+
export = { query };
25+
}
26+

nextstep-backend/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"paths": {
1313
"*": ["node_modules/*"],
1414
"types/*": ["src/types/*"]
15-
}
15+
},
16+
"typeRoots": ["src/types", "./node_modules/@types"]
1617
},
1718
"include": ["src/**/*.ts", "index.ts", "src/*.ts", "src/**/*"],
1819
"exclude": ["node_modules", "dist"]

nextstep-frontend/src/App.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,30 @@
5454
.read-the-docs {
5555
color: #888;
5656
}
57+
58+
.job-card {
59+
padding: 16px;
60+
border: 1px solid #ddd;
61+
border-radius: 8px;
62+
height: 200px; /* Fixed height for all cards */
63+
display: flex;
64+
flex-direction: column;
65+
justify-content: space-between; /* Align content and button */
66+
overflow-y: auto; /* Enable vertical scrolling for overflow */
67+
}
68+
69+
.job-card img {
70+
display: inline-block;
71+
margin: 0;
72+
max-width: 20px;
73+
height: 20px;
74+
}
75+
76+
.job-card .company-name {
77+
font-weight: bold; /* Make company name bold */
78+
}
79+
80+
.job-card .location {
81+
margin-top: 8px; /* Add separation between company name and location */
82+
display: block; /* Ensure it appears on a new line */
83+
}

0 commit comments

Comments
 (0)