Skip to content

Commit 2020585

Browse files
committed
Merge branch 'master' into NXD-8-edit-docx
Signed-off-by: Tal Jacob <taljacob2@gmail.com>
2 parents 1fe44e4 + 2a72786 commit 2020585

15 files changed

Lines changed: 670 additions & 26 deletions

File tree

nextstep-backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import validateUser from "./middleware/validateUser";
1515
import loadOpenApiFile from "./openapi/openapi_loader";
1616
import resource_routes from './routes/resources_routes';
1717
import resume_routes from './routes/resume_routes';
18+
import githubRoutes from './routes/github_routes';
1819

1920
const specs = swaggerJsdoc(options);
2021

@@ -74,5 +75,6 @@ app.use('/user', usersRoutes);
7475
app.use('/resource', resource_routes);
7576
app.use('/room', roomsRoutes);
7677
app.use('/resume', resume_routes);
78+
app.use('/github', githubRoutes);
7779

7880
export { app, corsOptions };
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import axios from 'axios';
2+
import { Request, Response } from 'express';
3+
4+
const clientId = process.env.GITHUB_CLIENT_ID;
5+
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
6+
7+
export const handleGitHubOAuth = async (req: Request, res: Response) => {
8+
const { code } = req.body;
9+
10+
if (!code) {
11+
return res.status(400).json({ error: 'Authorization code is required' });
12+
}
13+
14+
try {
15+
const tokenResponse = await axios.post('https://github.com/login/oauth/access_token', {
16+
headers: { 'Content-Type': 'application/json' },
17+
client_id: clientId,
18+
client_secret: clientSecret,
19+
code: code,
20+
}) as any;
21+
22+
const tokenData = await tokenResponse.data;
23+
const params = new URLSearchParams(tokenData);
24+
const accessToken = params.get('access_token');
25+
26+
if (!accessToken) {
27+
return res.status(400).json({ error: 'Failed to retrieve access token' });
28+
}
29+
30+
const userResponse = await axios.get('https://api.github.com/user', {
31+
headers: { Authorization: `Bearer ${accessToken}` },
32+
}) as {status: number, data: { login?: string }, json: () => Promise<any>};
33+
34+
if (userResponse.status !== 200) {
35+
const errorText = await userResponse.data;
36+
return res.status(400).json({ error: errorText });
37+
}
38+
39+
const userData = await userResponse.data;
40+
res.json({ username: userData.login });
41+
} catch (error) {
42+
console.error('Error during GitHub OAuth:', error);
43+
res.status(500).json({ error: 'Internal server error' });
44+
}
45+
};
46+
47+
export const fetchGitHubRepos = async (req: Request, res: Response) => {
48+
const { username } = req.params;
49+
const { accessToken } = req.query; // Optional access token for authenticated requests
50+
51+
try {
52+
const apiUrl = `https://api.github.com/users/${username}/repos`;
53+
54+
const headers = accessToken
55+
? { Authorization: `Bearer ${accessToken}` }
56+
: undefined;
57+
58+
const response = await axios.get(apiUrl, { headers }) as { data: any, status: number };
59+
60+
if (response.status !== 200) {
61+
return res.status(400).json({ error: `Error fetching repos: ${response.status}` });
62+
}
63+
64+
const repos = await response.data;
65+
res.json(repos);
66+
} catch (error) {
67+
console.error('Error fetching repos:', error);
68+
res.status(500).json({ error: 'Internal server error' });
69+
}
70+
};
71+
72+
export const fetchRepoLanguages = async (req: Request, res: Response) => {
73+
const { repoUrl } = req.query;
74+
75+
if (!repoUrl) {
76+
return res.status(400).json({ error: 'Repository URL is required' });
77+
}
78+
79+
try {
80+
// Convert the repoUrl to the GitHub API URL if necessary
81+
const apiUrl = (repoUrl as string).replace('https://github.com/', 'https://api.github.com/repos/');
82+
const response = await axios.get(`${apiUrl}/languages`);
83+
84+
if (response.status !== 200) {
85+
return res.status(400).json({ error: `Error fetching languages: ${response.statusText}` });
86+
}
87+
88+
res.json(response.data);
89+
} catch (error) {
90+
console.error('Error fetching languages:', error);
91+
res.status(500).json({ error: 'Internal server error' });
92+
}
93+
};

nextstep-backend/src/controllers/resume_controller.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
22
import { config } from '../config/config';
33
import fs from 'fs';
44
import path from 'path';
5-
import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume } from '../services/resume_service';
5+
import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume, parseResumeFields } from '../services/resume_service';
66
import multer from 'multer';
77
import { CustomRequest } from "types/customRequest";
88
import { handleError } from "../utils/handle_error";
@@ -95,4 +95,18 @@ const generateResume = async (req: Request, res: Response) => {
9595
}
9696
};
9797

98-
export default { getResumeScore, getStreamResumeScore, getTemplates, generateResume };
98+
99+
const parseResume = async (req: Request, res: Response) => {
100+
try {
101+
if (!req.file) {
102+
return res.status(400).json({ error: 'No resume file uploaded' });
103+
}
104+
const parsed = await parseResumeFields(req.file.buffer, req.file.originalname);
105+
return res.status(200).json(parsed);
106+
} catch (err: any) {
107+
console.error('Error parsing resume:', err);
108+
return handleError(err, res);
109+
}
110+
};
111+
112+
export default { parseResume, getResumeScore, getStreamResumeScore, getTemplates, generateResume };
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import express from 'express';
2+
import { handleGitHubOAuth, fetchGitHubRepos, fetchRepoLanguages } from '../controllers/github_controller';
3+
4+
const router = express.Router();
5+
6+
router.post('/oauth', handleGitHubOAuth);
7+
router.get('/repos/:username', fetchGitHubRepos);
8+
router.get('/languages', fetchRepoLanguages);
9+
10+
export default router;

nextstep-backend/src/routes/resume_routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import express, { Request, Response } from 'express';
22
import Resume from '../controllers/resume_controller';
33
import { CustomRequest } from "types/customRequest";
4+
import multer from 'multer';
5+
6+
const upload = multer();
47

58
const router = express.Router();
69

@@ -12,4 +15,6 @@ router.get('/templates', Resume.getTemplates);
1215

1316
router.post('/generate', Resume.generateResume);
1417

18+
router.post('/parseResume', upload.single('resume'), Resume.parseResume);
19+
1520
export default router;

nextstep-backend/src/services/resume_service.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import pdfParse from 'pdf-parse';
88
import AdmZip from 'adm-zip';
99
import { DOMParser, XMLSerializer } from 'xmldom';
1010

11+
export interface ParsedResume {
12+
aboutMe: string;
13+
skills: string[];
14+
roleMatch: string;
15+
experience:string[];
16+
education?: string[];
17+
}
18+
1119
const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named Bob the Resume builder.
1220
You will review resumes with or without job descriptions.
1321
You are an expert in resume evaluation and provide constructive feedback with dynamic evaluation.
@@ -478,4 +486,51 @@ Rules:
478486
}
479487
};
480488

481-
export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume };
489+
490+
/**
491+
* Extracts raw text from the uploaded resume buffer,
492+
* prompts the AI to return { aboutMe, skills[], roleMatch, experience[] } as JSON.
493+
*/
494+
const parseResumeFields = async (
495+
fileBuffer: Buffer,
496+
originalName: string
497+
): Promise<ParsedResume> => {
498+
// 1) Extract text
499+
const ext = path.extname(originalName).toLowerCase();
500+
let text: string;
501+
if (ext === '.pdf') {
502+
const data = await pdfParse(fileBuffer);
503+
text = data.text;
504+
} else {
505+
// mammoth supports buffer input
506+
const { value } = await mammoth.extractRawText({ buffer: fileBuffer });
507+
text = value;
508+
}
509+
510+
// 2) Build the extraction prompt
511+
const prompt = `
512+
Extract from this resume the following fields as JSON:
513+
• "aboutMe": a 1–2 sentence self-summary.
514+
• "skills": an array of technical skills.
515+
• "roleMatch": one-sentence best-fit role suggestion.
516+
• "experience": an array of 3–5 bullet points of key achievements.
517+
518+
Resume text:
519+
---
520+
${text}
521+
---
522+
Respond with a single JSON object and nothing else. The json object should begin directly with parentheses and have the following structure: {"a": "value", "b": "value", ...}
523+
`;
524+
525+
// 3) Call your Chat AI
526+
const aiResponse = await chatWithAI(
527+
SYSTEM_TEMPLATE, // you can reuse your existing SYSTEM_TEMPLATE or define a new one
528+
[prompt]
529+
);
530+
531+
// 4) Parse & return
532+
const parsed = JSON.parse(aiResponse.trim().replace("```json", "").replace("```", "")) as ParsedResume;
533+
return parsed;
534+
};
535+
536+
export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume, parseResumeFields };

nextstep-frontend/src/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Login from './pages/Login';
44
import Register from './pages/Register';
55
import Profile from './pages/Profile';
66
import './App.css'
7-
import Dashboard from './pages/Dashboard';
7+
import Feed from './pages/Feed';
88
import Footer from './components/Footer';
99
import RequireAuth from './hoc/RequireAuth';
1010
import NewPost from './pages/NewPost';
@@ -13,6 +13,7 @@ import Chat from './pages/Chat';
1313
import Resume from './pages/Resume';
1414
import TopBar from './components/TopBar';
1515
import Layout from './components/Layout';
16+
import MainDashboard from './pages/MainDashboard';
1617

1718
const App: React.FC = () => {
1819
return (
@@ -22,12 +23,13 @@ const App: React.FC = () => {
2223
<Route path="/" element={<Layout className="login"><Login /></Layout>} />
2324
<Route path="/login" element={<Layout className="login"><Login /></Layout>} />
2425
<Route path="/register" element={<Layout className="register"><Register /></Layout>} />
25-
<Route path="/dashboard" element={<RequireAuth><TopBar /><Layout className="dashboard"><Dashboard /></Layout></RequireAuth>} />
26+
<Route path="/feed" element={<RequireAuth><TopBar /><Layout className="feed"><Feed /></Layout></RequireAuth>} />
2627
<Route path="/profile" element={<RequireAuth><TopBar /><Layout className="profile"><Profile /></Layout></RequireAuth>} />
2728
<Route path="/new-post" element={<RequireAuth><TopBar /><Layout className="new-post"><NewPost /></Layout></RequireAuth>} />
2829
<Route path="/post/:postId" element={<RequireAuth><TopBar /><Layout className="post-details"><PostDetails /></Layout></RequireAuth>} />
2930
<Route path="/chat" element={<RequireAuth><TopBar /><Layout className="chat"><Chat /></Layout></RequireAuth>} />
3031
<Route path="/resume" element={<RequireAuth><TopBar /><Layout className="resume"><Resume /></Layout></RequireAuth>} />
32+
<Route path="/main-dashboard" element={<RequireAuth><TopBar /><Layout className="main-dashboard"><MainDashboard /></Layout></RequireAuth>} />
3133
<Route path="*" element={<Navigate to="/" />} />
3234
</Routes>
3335
</Router>

nextstep-frontend/src/components/Footer.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.footer {
2-
background-color: var(--color-5);
2+
background-color: #233752;
33
color: var(--color-2);
44
padding: 10px 20px;
55
text-align: center;

nextstep-frontend/src/components/TopBar.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useRef } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material';
4-
import { Home, Person, Message, Logout, DocumentScannerTwoTone } from '@mui/icons-material';
4+
import { Home, Person, Message, Logout, DocumentScannerTwoTone, Feed } from '@mui/icons-material';
55
import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts";
66
import api from "../serverApi.ts";
77

@@ -20,26 +20,31 @@ const TopBar: React.FC = () => {
2020
};
2121

2222
return (
23-
<AppBar position="relative" sx={{ width: '100vw', left: 0 }} className='top-bar'>
23+
<AppBar position="relative" sx={{ width: '100vw', left: 0, backgroundColor: '#233752' }} className='top-bar'>
2424
<Toolbar>
2525
<Tooltip title="Home">
26-
<IconButton color="inherit" onClick={() => navigate('/dashboard')} sx={{ mx: 1 }}>
26+
<IconButton color="inherit" onClick={() => navigate('/main-dashboard')} sx={{ mx: 1 }}>
2727
<Home fontSize='large'/>
2828
</IconButton>
2929
</Tooltip>
30-
<Tooltip title="Profile">
31-
<IconButton color="inherit" onClick={() => navigate('/profile')} sx={{ mx: 1 }}>
32-
<Person fontSize='large'/>
30+
<Tooltip title="Resume">
31+
<IconButton color="inherit" onClick={() => navigate('/resume')} sx={{ mx: 1 }}>
32+
<DocumentScannerTwoTone fontSize='large'/>
33+
</IconButton>
34+
</Tooltip>
35+
<Tooltip title="Feed">
36+
<IconButton color="inherit" onClick={() => navigate('/feed')} sx={{ mx: 1 }}>
37+
<Feed fontSize='large'/>
3338
</IconButton>
3439
</Tooltip>
3540
<Tooltip title="Chat">
3641
<IconButton color="inherit" onClick={() => navigate('/chat')} sx={{ mx: 1 }}>
3742
<Message fontSize='large'/>
3843
</IconButton>
3944
</Tooltip>
40-
<Tooltip title="Resume">
41-
<IconButton color="inherit" onClick={() => navigate('/resume')} sx={{ mx: 1 }}>
42-
<DocumentScannerTwoTone fontSize='large'/>
45+
<Tooltip title="Profile">
46+
<IconButton color="inherit" onClick={() => navigate('/profile')} sx={{ mx: 1 }}>
47+
<Person fontSize='large'/>
4348
</IconButton>
4449
</Tooltip>
4550
<Box sx={{ flexGrow: 1 }} />

0 commit comments

Comments
 (0)