From 299b9b0db29a89ba90e3ae76656016e8af42bcf9 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 17:56:44 +0300 Subject: [PATCH 01/10] added linkedin jobs integration --- nextstep-backend/package.json | 3 +- nextstep-backend/src/app.ts | 2 + .../controllers/linkedin_jobs_controller.ts | 68 ++++++++++ nextstep-backend/src/openapi/swagger.yaml | 98 ++++++++++++++ .../src/routes/linkedin_jobs_routes.ts | 9 ++ .../src/services/company_logo_service.ts | 15 +++ .../src/types/linkedin-jobs-api.d.ts | 26 ++++ nextstep-backend/tsconfig.json | 3 +- nextstep-frontend/src/App.css | 27 ++++ .../src/components/LinkedInIntegration.tsx | 122 ++++++++++++++++++ nextstep-frontend/src/pages/MainDashboard.tsx | 47 ++++++- 11 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 nextstep-backend/src/controllers/linkedin_jobs_controller.ts create mode 100644 nextstep-backend/src/routes/linkedin_jobs_routes.ts create mode 100644 nextstep-backend/src/services/company_logo_service.ts create mode 100644 nextstep-backend/src/types/linkedin-jobs-api.d.ts create mode 100644 nextstep-frontend/src/components/LinkedInIntegration.tsx diff --git a/nextstep-backend/package.json b/nextstep-backend/package.json index 9d37754..bd313f5 100644 --- a/nextstep-backend/package.json +++ b/nextstep-backend/package.json @@ -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" @@ -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", diff --git a/nextstep-backend/src/app.ts b/nextstep-backend/src/app.ts index fb7f02a..6cfd964 100644 --- a/nextstep-backend/src/app.ts +++ b/nextstep-backend/src/app.ts @@ -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); @@ -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 }; diff --git a/nextstep-backend/src/controllers/linkedin_jobs_controller.ts b/nextstep-backend/src/controllers/linkedin_jobs_controller.ts new file mode 100644 index 0000000..f1a4053 --- /dev/null +++ b/nextstep-backend/src/controllers/linkedin_jobs_controller.ts @@ -0,0 +1,68 @@ +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(); + + 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} backend developer`.trim(); + + const queryOptions = { + keyword, + location, + dateSincePosted: 'past week', + jobType: 'full time', + experienceLevel: 'entry level', + 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' }); + } +}; diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index f724bcd..5b1aa7b 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -19,6 +19,8 @@ tags: description: Operations related to chat rooms - name: Resume description: Operations related to resume ATS scoring + - name: LinkedIn + description: Operations related to LinkedIn jobs paths: /post: @@ -1114,6 +1116,102 @@ paths: '400': description: Bad Request + /linkedin/jobs: + get: + tags: + - LinkedIn + 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 + - name: role + in: query + required: true + schema: + type: string + description: Desired role + responses: + '200': + description: List of jobs retrieved successfully + content: + application/json: + schema: + type: array + items: + type: object + properties: + title: + type: string + description: Job title + company: + type: string + description: Company name + location: + type: string + description: Job location + url: + type: string + description: Job posting URL + '400': + description: Bad request - Missing skills or role + '500': + description: Internal server error + + /google-jobs/jobs: + get: + tags: + - Google Jobs + summary: Retrieve jobs from Google Jobs based on skills and role + parameters: + - name: skills + in: query + required: true + schema: + type: string + description: Comma-separated list of skills + - name: role + in: query + required: true + schema: + type: string + description: Desired role + - name: location + in: query + required: false + schema: + type: string + description: Job location (default: remote) + responses: + '200': + description: List of jobs retrieved successfully + content: + application/json: + schema: + type: array + items: + type: object + properties: + title: + type: string + description: Job title + company: + type: string + description: Company name + location: + type: string + description: Job location + url: + type: string + description: Job posting URL + '400': + description: Bad request - Missing skills or role + '500': + description: Internal server error + components: schemas: Post: diff --git a/nextstep-backend/src/routes/linkedin_jobs_routes.ts b/nextstep-backend/src/routes/linkedin_jobs_routes.ts new file mode 100644 index 0000000..625a6b6 --- /dev/null +++ b/nextstep-backend/src/routes/linkedin_jobs_routes.ts @@ -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; diff --git a/nextstep-backend/src/services/company_logo_service.ts b/nextstep-backend/src/services/company_logo_service.ts new file mode 100644 index 0000000..0fbef50 --- /dev/null +++ b/nextstep-backend/src/services/company_logo_service.ts @@ -0,0 +1,15 @@ +import axios from 'axios'; + +export const getCompanyLogo = async (companyName: string): Promise => { + 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; + } +}; diff --git a/nextstep-backend/src/types/linkedin-jobs-api.d.ts b/nextstep-backend/src/types/linkedin-jobs-api.d.ts new file mode 100644 index 0000000..186782d --- /dev/null +++ b/nextstep-backend/src/types/linkedin-jobs-api.d.ts @@ -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; + + export = { query }; +} + diff --git a/nextstep-backend/tsconfig.json b/nextstep-backend/tsconfig.json index 91d9de4..c29761f 100644 --- a/nextstep-backend/tsconfig.json +++ b/nextstep-backend/tsconfig.json @@ -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"] diff --git a/nextstep-frontend/src/App.css b/nextstep-frontend/src/App.css index 683a2cf..77db3ae 100644 --- a/nextstep-frontend/src/App.css +++ b/nextstep-frontend/src/App.css @@ -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 */ +} diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx new file mode 100644 index 0000000..fc46b07 --- /dev/null +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import { Box, Typography, Button, Grid, CircularProgress, IconButton } from '@mui/material'; +import { ExpandLess, ExpandMore, LinkedIn } from '@mui/icons-material'; // Import LinkedIn icon + +interface Job { + position: string; + company: string; + location: string; + url: string; + companyLogo?: string; +} + +interface LinkedInIntegrationProps { + jobs: Job[]; + loadingJobs: boolean; + fetchJobs: () => Promise; + showJobRecommendations: boolean; + toggleJobRecommendations: () => void; + skills: string[]; + selectedRole: string; +} + +const LinkedInIntegration: React.FC = ({ + jobs, + loadingJobs, + fetchJobs, + showJobRecommendations, + toggleJobRecommendations, + skills, + selectedRole, +}) => { + return ( + + + + + + Job Recommendations + + + + {showJobRecommendations ? : } + + + {showJobRecommendations && ( + <> + + + + {jobs.length > 0 && ( + + {jobs.map((job, index) => ( + + + + + {job.companyLogo && ( + {`${job.company} + )} + + {job.company} + + + {job.position.toLowerCase()} +
+ + {job.location} + +
+ +
+
+ ))} +
+ )} + + )} +
+ ); +}; + +export default LinkedInIntegration; diff --git a/nextstep-frontend/src/pages/MainDashboard.tsx b/nextstep-frontend/src/pages/MainDashboard.tsx index 9903df3..d25e7dc 100644 --- a/nextstep-frontend/src/pages/MainDashboard.tsx +++ b/nextstep-frontend/src/pages/MainDashboard.tsx @@ -34,6 +34,7 @@ import { handleGitHubOAuth } from '../handlers/githubAuth'; import api from '../serverApi'; +import LinkedInIntegration from '../components/LinkedInIntegration'; const roles = [ 'Software Engineer', 'Frontend Developer', 'Backend Developer', @@ -65,6 +66,17 @@ const MainDashboard: React.FC = () => { const SKILL_DISPLAY_LIMIT = 6; const shouldShowToggle = skills.length > SKILL_DISPLAY_LIMIT; + // LinkedIn jobs state + const [jobs, setJobs] = useState<{ position: string; company: string; location: string; url: string, companyLogo?: string }[]>([]); + const [loadingJobs, setLoadingJobs] = useState(false); // New state for loading jobs + + // Job Recommendations toggle + const [showJobRecommendations, setShowJobRecommendations] = useState(true); // New state for toggle + + const toggleJobRecommendations = () => { + setShowJobRecommendations(!showJobRecommendations); + }; + // Persist to localStorage useEffect(() => { localStorage.setItem('aboutMe', aboutMe); }, [aboutMe]); useEffect(() => { localStorage.setItem('skills', JSON.stringify(skills)); }, [skills]); @@ -150,11 +162,30 @@ const MainDashboard: React.FC = () => { } }; + // Fetch Linkedin Jobs + const fetchJobs = async () => { + setLoadingJobs(true); + try { + const response = await api.get('/linkedin-jobs/jobs', { + params: { + skills: skills.join(','), + role: selectedRole, + location: 'Israel', // Optional: Add location if needed + }, + }); + setJobs(response.data); + } catch (error) { + console.error('Error fetching jobs:', error); + } finally { + setLoadingJobs(false); + } + }; + return ( {/* Left Column */} - + {/* Adjusted width */} {/* About Me Section */} @@ -301,12 +332,11 @@ const MainDashboard: React.FC = () => { )} - {/* Right Column */} - + @@ -377,6 +407,17 @@ const MainDashboard: React.FC = () => { )} + + {/* Jobs Section */} + From 3fcdbfb02b8516913510bc7ea5fcd1e79158f9a9 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 18:34:25 +0300 Subject: [PATCH 02/10] added changes --- .../controllers/linkedin_jobs_controller.ts | 11 +- .../src/components/LinkedInIntegration.tsx | 126 ++++++++++++++++-- nextstep-frontend/src/pages/MainDashboard.tsx | 9 +- 3 files changed, 128 insertions(+), 18 deletions(-) diff --git a/nextstep-backend/src/controllers/linkedin_jobs_controller.ts b/nextstep-backend/src/controllers/linkedin_jobs_controller.ts index f1a4053..38550fa 100644 --- a/nextstep-backend/src/controllers/linkedin_jobs_controller.ts +++ b/nextstep-backend/src/controllers/linkedin_jobs_controller.ts @@ -7,6 +7,9 @@ export const getJobsBySkillsAndRole = async (req: Request, res: Response) => { 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' }); @@ -19,14 +22,14 @@ export const getJobsBySkillsAndRole = async (req: Request, res: Response) => { .filter(Boolean); // Construct keyword by combining role and skills - const keyword = `${role} backend developer`.trim(); + const keyword = `${role} ${skillsArray.join(' ')}`.trim(); const queryOptions = { keyword, location, - dateSincePosted: 'past week', - jobType: 'full time', - experienceLevel: 'entry level', + dateSincePosted, + jobType, + experienceLevel, limit: '10', page: '0', }; diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index fc46b07..a5e3e86 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Box, Typography, Button, Grid, CircularProgress, IconButton } from '@mui/material'; -import { ExpandLess, ExpandMore, LinkedIn } from '@mui/icons-material'; // Import LinkedIn icon +import { Box, Typography, Button, Grid, CircularProgress, IconButton, TextField, MenuItem, Select, FormControl, InputLabel } from '@mui/material'; +import { ExpandLess, ExpandMore, LinkedIn, Settings } from '@mui/icons-material'; interface Job { position: string; @@ -13,13 +13,21 @@ interface Job { interface LinkedInIntegrationProps { jobs: Job[]; loadingJobs: boolean; - fetchJobs: () => Promise; + fetchJobs: (settings: LinkedInSettings) => Promise; showJobRecommendations: boolean; toggleJobRecommendations: () => void; skills: string[]; selectedRole: string; } +interface LinkedInSettings { + location: string; + dateSincePosted: string; + jobType: string; + experienceLevel: string; + skills: string[]; +} + const LinkedInIntegration: React.FC = ({ jobs, loadingJobs, @@ -29,41 +37,133 @@ const LinkedInIntegration: React.FC = ({ skills, selectedRole, }) => { + const [settings, setSettings] = useState({ + location: 'Israel', + dateSincePosted: 'past week', + jobType: 'full time', + experienceLevel: 'entry level', + skills: skills.slice(0, 3), // Limit to first 3 skills + }); + + const handleSettingChange = (key: keyof LinkedInSettings, value: string) => { + setSettings((prev) => ({ ...prev, [key]: value })); + }; + + const handleSkillChange = (index: number, value: string) => { + const updatedSkills = [...settings.skills]; + updatedSkills[index] = value; + setSettings((prev) => ({ ...prev, skills: updatedSkills })); + }; + + const handleFetchJobs = () => { + fetchJobs(settings); + }; + return ( - + Job Recommendations - {showJobRecommendations ? : } + {showJobRecommendations ? : } {showJobRecommendations && ( <> + + + + + Selected Role: {selectedRole || 'None'} + + + + handleSettingChange('location', e.target.value)} + fullWidth + /> + + + + Date Since Posted + + + + + + Job Type + + + + + + Experience Level + + + + + + + + Skills Sent: + + {settings.skills.map((skill, index) => ( + handleSkillChange(index, e.target.value)} + fullWidth + sx={{ mt: 1 }} + /> + ))} + +
- {jobs.length > 0 && ( + {jobs.length > 0 ? ( {jobs.map((job, index) => ( @@ -112,6 +212,10 @@ const LinkedInIntegration: React.FC = ({ ))} + ) : ( + + No job recommendations found. Try adjusting your search settings. + )} )} diff --git a/nextstep-frontend/src/pages/MainDashboard.tsx b/nextstep-frontend/src/pages/MainDashboard.tsx index d25e7dc..a2c22bc 100644 --- a/nextstep-frontend/src/pages/MainDashboard.tsx +++ b/nextstep-frontend/src/pages/MainDashboard.tsx @@ -163,14 +163,17 @@ const MainDashboard: React.FC = () => { }; // Fetch Linkedin Jobs - const fetchJobs = async () => { + const fetchJobs = async (settings: { location: string; dateSincePosted: string; jobType: string; experienceLevel: string; skills?: string[] }) => { setLoadingJobs(true); try { const response = await api.get('/linkedin-jobs/jobs', { params: { - skills: skills.join(','), + skills: (settings.skills || skills.slice(0, 3)).join(','), // Use updated skills or default to first three role: selectedRole, - location: 'Israel', // Optional: Add location if needed + location: settings.location, + dateSincePosted: settings.dateSincePosted, + jobType: settings.jobType, + experienceLevel: settings.experienceLevel, }, }); setJobs(response.data); From 8a4459f2234d44de2dee51e29fda95a7d7dcec47 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 18:35:36 +0300 Subject: [PATCH 03/10] set job recomendations close at first --- nextstep-frontend/src/pages/MainDashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextstep-frontend/src/pages/MainDashboard.tsx b/nextstep-frontend/src/pages/MainDashboard.tsx index a2c22bc..40cd1d9 100644 --- a/nextstep-frontend/src/pages/MainDashboard.tsx +++ b/nextstep-frontend/src/pages/MainDashboard.tsx @@ -71,7 +71,7 @@ const MainDashboard: React.FC = () => { const [loadingJobs, setLoadingJobs] = useState(false); // New state for loading jobs // Job Recommendations toggle - const [showJobRecommendations, setShowJobRecommendations] = useState(true); // New state for toggle + const [showJobRecommendations, setShowJobRecommendations] = useState(false); // New state for toggle const toggleJobRecommendations = () => { setShowJobRecommendations(!showJobRecommendations); From 2875d63af019ef71af155a405bf9c9d8815044a2 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 18:37:39 +0300 Subject: [PATCH 04/10] made experience level default 'all' --- nextstep-frontend/src/components/LinkedInIntegration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index a5e3e86..a870721 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -41,7 +41,7 @@ const LinkedInIntegration: React.FC = ({ location: 'Israel', dateSincePosted: 'past week', jobType: 'full time', - experienceLevel: 'entry level', + experienceLevel: 'all', skills: skills.slice(0, 3), // Limit to first 3 skills }); From 05c11a72a3df3cd5106b29d502069c6dc5ba7f84 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 18:43:21 +0300 Subject: [PATCH 05/10] edited to past month default --- nextstep-frontend/src/components/LinkedInIntegration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index a870721..7c99afd 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -39,7 +39,7 @@ const LinkedInIntegration: React.FC = ({ }) => { const [settings, setSettings] = useState({ location: 'Israel', - dateSincePosted: 'past week', + dateSincePosted: 'past month', jobType: 'full time', experienceLevel: 'all', skills: skills.slice(0, 3), // Limit to first 3 skills From b58d293906b35c9737e7ee85371738a57c12a232 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 19:00:16 +0300 Subject: [PATCH 06/10] added dialog for 'view job' --- .../src/components/LinkedInIntegration.tsx | 98 +++++++++++++++---- 1 file changed, 77 insertions(+), 21 deletions(-) diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index 7c99afd..1d61077 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -1,13 +1,15 @@ import React, { useState } from 'react'; -import { Box, Typography, Button, Grid, CircularProgress, IconButton, TextField, MenuItem, Select, FormControl, InputLabel } from '@mui/material'; -import { ExpandLess, ExpandMore, LinkedIn, Settings } from '@mui/icons-material'; +import { Box, Typography, Button, Grid, CircularProgress, IconButton, TextField, MenuItem, Select, FormControl, InputLabel, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; +import { ExpandLess, LinkedIn, Settings } from '@mui/icons-material'; interface Job { position: string; company: string; location: string; - url: string; + jobUrl: string; companyLogo?: string; + date?: string; + salary?: string; } interface LinkedInIntegrationProps { @@ -45,6 +47,9 @@ const LinkedInIntegration: React.FC = ({ skills: skills.slice(0, 3), // Limit to first 3 skills }); + const [selectedJob, setSelectedJob] = useState(null); + const [jobDetails, setJobDetails] = useState(null); + const handleSettingChange = (key: keyof LinkedInSettings, value: string) => { setSettings((prev) => ({ ...prev, [key]: value })); }; @@ -59,6 +64,16 @@ const LinkedInIntegration: React.FC = ({ fetchJobs(settings); }; + const handleViewJob = (job: Job) => { + setSelectedJob(job); + setJobDetails(job); + }; + + const handleCloseDialog = () => { + setSelectedJob(null); + setJobDetails(null); + }; + return ( @@ -132,21 +147,21 @@ const LinkedInIntegration: React.FC = ({ - - Skills Sent: - - {settings.skills.map((skill, index) => ( - handleSkillChange(index, e.target.value)} - fullWidth - sx={{ mt: 1 }} - /> - ))} - -
+ + Skills Sent: + + {settings.skills.map((skill, index) => ( + handleSkillChange(index, e.target.value)} + fullWidth + sx={{ mt: 1 }} + /> + ))} + +
+ {selectedJob?.jobUrl && ( + + )} + + )} From f8508faecec95ab5786c4b76fdd307c595e225e8 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 19:09:22 +0300 Subject: [PATCH 07/10] changed skills filter in client --- .../src/components/LinkedInIntegration.tsx | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index 1d61077..b9a66af 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Box, Typography, Button, Grid, CircularProgress, IconButton, TextField, MenuItem, Select, FormControl, InputLabel, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; +import { Box, Typography, Button, Grid, CircularProgress, IconButton, TextField, MenuItem, Select, FormControl, InputLabel, Dialog, DialogTitle, DialogContent, DialogActions, Chip, Stack } from '@mui/material'; import { ExpandLess, LinkedIn, Settings } from '@mui/icons-material'; interface Job { @@ -54,12 +54,6 @@ const LinkedInIntegration: React.FC = ({ setSettings((prev) => ({ ...prev, [key]: value })); }; - const handleSkillChange = (index: number, value: string) => { - const updatedSkills = [...settings.skills]; - updatedSkills[index] = value; - setSettings((prev) => ({ ...prev, skills: updatedSkills })); - }; - const handleFetchJobs = () => { fetchJobs(settings); }; @@ -147,19 +141,44 @@ const LinkedInIntegration: React.FC = ({
- - Skills Sent: + + Skills Filter: - {settings.skills.map((skill, index) => ( - handleSkillChange(index, e.target.value)} - fullWidth - sx={{ mt: 1 }} - /> - ))} + + {settings.skills.map((skill, index) => ( + { + const updatedSkills = [...settings.skills]; + updatedSkills.splice(index, 1); + setSettings((prev) => ({ ...prev, skills: updatedSkills })); + }} + sx={{ backgroundColor: '#e3f2fd', color: '#0d47a1' }} + /> + ))} + + { + const input = e.target as HTMLInputElement; + if (e.key === 'Enter' && input.value.trim()) { + const newSkill = input.value.trim(); + if (!settings.skills.includes(newSkill) && settings.skills.length < 3) { + setSettings((prev) => ({ + ...prev, + skills: [...prev.skills, newSkill], + })); + } + input.value = ''; + } + }} + placeholder={settings.skills.length >= 3 ? "Reached max of 3 skills" : "Type a skill and press Enter"} + disabled={settings.skills.length >= 3} // Disable input if 3 skills are already added + sx={{ mt: 1 }} + />
From ec31979282d2cea2b64a429703bbfe68d9721c33 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 19:16:49 +0300 Subject: [PATCH 08/10] reverted swagger --- nextstep-backend/src/openapi/swagger.yaml | 98 ----------------------- 1 file changed, 98 deletions(-) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index 5b1aa7b..f724bcd 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -19,8 +19,6 @@ tags: description: Operations related to chat rooms - name: Resume description: Operations related to resume ATS scoring - - name: LinkedIn - description: Operations related to LinkedIn jobs paths: /post: @@ -1116,102 +1114,6 @@ paths: '400': description: Bad Request - /linkedin/jobs: - get: - tags: - - LinkedIn - 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 - - name: role - in: query - required: true - schema: - type: string - description: Desired role - responses: - '200': - description: List of jobs retrieved successfully - content: - application/json: - schema: - type: array - items: - type: object - properties: - title: - type: string - description: Job title - company: - type: string - description: Company name - location: - type: string - description: Job location - url: - type: string - description: Job posting URL - '400': - description: Bad request - Missing skills or role - '500': - description: Internal server error - - /google-jobs/jobs: - get: - tags: - - Google Jobs - summary: Retrieve jobs from Google Jobs based on skills and role - parameters: - - name: skills - in: query - required: true - schema: - type: string - description: Comma-separated list of skills - - name: role - in: query - required: true - schema: - type: string - description: Desired role - - name: location - in: query - required: false - schema: - type: string - description: Job location (default: remote) - responses: - '200': - description: List of jobs retrieved successfully - content: - application/json: - schema: - type: array - items: - type: object - properties: - title: - type: string - description: Job title - company: - type: string - description: Company name - location: - type: string - description: Job location - url: - type: string - description: Job posting URL - '400': - description: Bad request - Missing skills or role - '500': - description: Internal server error - components: schemas: Post: From 64fdcd2f8d9b6e7f08f16557d620dff1e97153df Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 19:19:13 +0300 Subject: [PATCH 09/10] added fixed linkedin swagger text --- nextstep-backend/src/openapi/swagger.yaml | 131 ++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index f724bcd..4ba9fb1 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -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: @@ -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: From 4384cb2263fff9f26725226417ac4507f3455a82 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sat, 24 May 2025 19:28:24 +0300 Subject: [PATCH 10/10] fixed missing parameter in Job obj --- nextstep-frontend/src/components/LinkedInIntegration.tsx | 2 +- nextstep-frontend/src/pages/MainDashboard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index b9a66af..a5e0929 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -6,7 +6,7 @@ interface Job { position: string; company: string; location: string; - jobUrl: string; + jobUrl?: string; companyLogo?: string; date?: string; salary?: string; diff --git a/nextstep-frontend/src/pages/MainDashboard.tsx b/nextstep-frontend/src/pages/MainDashboard.tsx index 40cd1d9..354e3cb 100644 --- a/nextstep-frontend/src/pages/MainDashboard.tsx +++ b/nextstep-frontend/src/pages/MainDashboard.tsx @@ -67,7 +67,7 @@ const MainDashboard: React.FC = () => { const shouldShowToggle = skills.length > SKILL_DISPLAY_LIMIT; // LinkedIn jobs state - const [jobs, setJobs] = useState<{ position: string; company: string; location: string; url: string, companyLogo?: string }[]>([]); + const [jobs, setJobs] = useState<{ position: string; company: string; location: string; url: string, companyLogo?: string, jobUrl?: string }[]>([]); const [loadingJobs, setLoadingJobs] = useState(false); // New state for loading jobs // Job Recommendations toggle