Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion nextstep-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"type": "git",
"url": "git+https://github.com/NextStepFinalProject/NextStep.git"
},
"author": "Mevorah Berrebi & Tal Jacob & Lina Elman & Liav Tibi",
"author": "Tal Jacob & Lina Elman & Liav Tibi",
"license": "ISC",
"bugs": {
"url": "https://github.com/NextStepFinalProject/NextStep/issues"
Expand All @@ -39,6 +39,7 @@
"jest-junit": "^16.0.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"linkedin-jobs-api": "^1.0.6",
"mammoth": "^1.9.0",
"mongoose": "^8.8.2",
"multer": "^1.4.5-lts.1",
Expand Down
2 changes: 2 additions & 0 deletions nextstep-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import loadOpenApiFile from "./openapi/openapi_loader";
import resource_routes from './routes/resources_routes';
import resume_routes from './routes/resume_routes';
import githubRoutes from './routes/github_routes';
import linkedinJobsRoutes from './routes/linkedin_jobs_routes';

const specs = swaggerJsdoc(options);

Expand Down Expand Up @@ -76,5 +77,6 @@ app.use('/resource', resource_routes);
app.use('/room', roomsRoutes);
app.use('/resume', resume_routes);
app.use('/github', githubRoutes);
app.use('/linkedin-jobs', linkedinJobsRoutes);

export { app, corsOptions };
71 changes: 71 additions & 0 deletions nextstep-backend/src/controllers/linkedin_jobs_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Request, Response } from 'express';
import linkedIn from 'linkedin-jobs-api';
import { getCompanyLogo } from '../services/company_logo_service';

export const getJobsBySkillsAndRole = async (req: Request, res: Response) => {
try {
const skillsParam = String(req.query.skills || '').trim();
const role = String(req.query.role || '').trim();
const location = String(req.query.location || 'Israel').trim();
const dateSincePosted = String(req.query.dateSincePosted || 'past week').trim();
const jobType = String(req.query.jobType || 'full time').trim();
const experienceLevel = String(req.query.experienceLevel || 'entry level').trim();

if (!skillsParam || !role) {
return res.status(400).json({ error: 'Skills and role are required' });
}

// Split skills by comma and trim whitespace
const skillsArray = skillsParam
.split(',')
.map(skill => skill.trim())
.filter(Boolean);

// Construct keyword by combining role and skills
const keyword = `${role} ${skillsArray.join(' ')}`.trim();

const queryOptions = {
keyword,
location,
dateSincePosted,
jobType,
experienceLevel,
limit: '10',
page: '0',
};

const jobs = await linkedIn.query(queryOptions);

// Fetch company logos for each job
const jobsWithLogos = await Promise.all(
jobs.map(async (job: any) => {
const companyLogo = await getCompanyLogo(job.company);
return {
...job,
companyLogo,
position: job.position // Add position field
};
})
);

res.status(200).json(jobsWithLogos);
} catch (error: any) {
console.error('Error fetching jobs from LinkedIn Jobs API:', error.message);
res.status(500).json({ error: 'Failed to fetch jobs from LinkedIn Jobs API' });
}
};

export const viewJobDetails = async (req: Request, res: Response) => {
try {
const jobId = req.params.id;
if (!jobId) {
return res.status(400).json({ error: 'Job ID is required' });
}

const jobDetails = await linkedIn.query({ keyword: jobId, limit: '1' });
res.status(200).json(jobDetails);
} catch (error: any) {
console.error('Error fetching job details:', error.message);
res.status(500).json({ error: 'Failed to fetch job details' });
}
};
131 changes: 131 additions & 0 deletions nextstep-backend/src/openapi/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ tags:
description: Operations related to chat rooms
- name: Resume
description: Operations related to resume ATS scoring
- name: LinkedIn Jobs
description: Operations related to LinkedIn job postings

paths:
/post:
Expand Down Expand Up @@ -1114,6 +1116,135 @@ paths:
'400':
description: Bad Request

/linkedin-jobs/jobs:
get:
tags:
- LinkedIn Jobs
summary: Retrieve jobs from LinkedIn based on skills and role
parameters:
- name: skills
in: query
required: true
schema:
type: string
description: Comma-separated list of skills (maximum 3)
- name: role
in: query
required: true
schema:
type: string
description: Desired role
- name: location
in: query
required: false
schema:
type: string
description: Job location
- name: dateSincePosted
in: query
required: false
schema:
type: string
description: Date range for job postings (e.g., "past day", "past week", "past month")
- name: jobType
in: query
required: false
schema:
type: string
description: Type of job (e.g., "full time", "part time", "contract")
- name: experienceLevel
in: query
required: false
schema:
type: string
description: Experience level (e.g., "entry level", "mid level", "senior level", "all")
responses:
'200':
description: List of jobs retrieved successfully
content:
application/json:
schema:
type: array
items:
type: object
properties:
position:
type: string
description: Job position
company:
type: string
description: Company name
location:
type: string
description: Job location
jobUrl:
type: string
description: Job posting URL
companyLogo:
type: string
description: URL of the company logo
date:
type: string
description: Date the job was posted
salary:
type: string
description: Salary information
'400':
description: Bad request - Missing skills or role
'500':
description: Internal server error

/linkedin-jobs/jobs/{id}:
get:
tags:
- LinkedIn Jobs
summary: Retrieve details of a specific job by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Job ID
responses:
'200':
description: Job details retrieved successfully
content:
application/json:
schema:
type: object
properties:
position:
type: string
description: Job position
company:
type: string
description: Company name
location:
type: string
description: Job location
description:
type: string
description: Detailed job description
jobUrl:
type: string
description: Job posting URL
companyLogo:
type: string
description: URL of the company logo
date:
type: string
description: Date the job was posted
salary:
type: string
description: Salary information
'400':
description: Bad request - Missing job ID
'404':
description: Job not found
'500':
description: Internal server error

components:
schemas:
Post:
Expand Down
9 changes: 9 additions & 0 deletions nextstep-backend/src/routes/linkedin_jobs_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from 'express';
import { getJobsBySkillsAndRole, viewJobDetails } from '../controllers/linkedin_jobs_controller';

const router = express.Router();

router.get('/jobs', getJobsBySkillsAndRole);
router.get('/jobs/:id', viewJobDetails);

export default router;
15 changes: 15 additions & 0 deletions nextstep-backend/src/services/company_logo_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from 'axios';

export const getCompanyLogo = async (companyName: string): Promise<string | null> => {
try {
const response = await axios.get(`https://logo.clearbit.com/${encodeURIComponent(companyName)}.com`);
return response.status === 200 ? response.config.url ?? null : null;
} catch (error) {
if (error instanceof Error) {
console.error(`Failed to fetch logo for company: ${companyName}`, error.message);
} else {
console.error(`Failed to fetch logo for company: ${companyName}`, error);
}
return null;
}
};
26 changes: 26 additions & 0 deletions nextstep-backend/src/types/linkedin-jobs-api.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
declare module 'linkedin-jobs-api' {
interface Job {
position: string;
company: string;
location: string;
date: string;
// Add other relevant fields as needed
}

interface QueryOptions {
keyword: string;
location?: string;
dateSincePosted?: string;
jobType?: string;
remoteFilter?: string;
salary?: string;
experienceLevel?: string;
limit?: string;
page?: string;
}

function query(options: QueryOptions): Promise<Job[]>;

export = { query };
}

3 changes: 2 additions & 1 deletion nextstep-backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"paths": {
"*": ["node_modules/*"],
"types/*": ["src/types/*"]
}
},
"typeRoots": ["src/types", "./node_modules/@types"]
},
"include": ["src/**/*.ts", "index.ts", "src/*.ts", "src/**/*"],
"exclude": ["node_modules", "dist"]
Expand Down
27 changes: 27 additions & 0 deletions nextstep-frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,30 @@
.read-the-docs {
color: #888;
}

.job-card {
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
height: 200px; /* Fixed height for all cards */
display: flex;
flex-direction: column;
justify-content: space-between; /* Align content and button */
overflow-y: auto; /* Enable vertical scrolling for overflow */
}

.job-card img {
display: inline-block;
margin: 0;
max-width: 20px;
height: 20px;
}

.job-card .company-name {
font-weight: bold; /* Make company name bold */
}

.job-card .location {
margin-top: 8px; /* Add separation between company name and location */
display: block; /* Ensure it appears on a new line */
}
Loading
Loading