Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2887f05
added initial app design
Lina0Elman Apr 22, 2025
cbf0f34
added github connection with skills update
Lina0Elman Apr 22, 2025
9fa24e4
added small change
Lina0Elman Apr 22, 2025
f591da5
fix
Lina0Elman Apr 22, 2025
ca01f01
added small design change
Lina0Elman Apr 22, 2025
7e17c64
changed to read only from github
Lina0Elman Apr 24, 2025
50d82b9
added choose between github oauth or without
Lina0Elman Apr 26, 2025
acb7f56
little fixes
Lina0Elman Apr 26, 2025
6c0b8a8
fix overflow
Lina0Elman Apr 26, 2025
5878753
fix
Lina0Elman Apr 26, 2025
96e11bb
Merge tag 'NXD-8-alpha.4' into NXD-9-Add-main-app-design
Lina0Elman Apr 26, 2025
7bdc9f7
added design changes
Lina0Elman May 3, 2025
eeb55dc
Merge branch 'master' into NXD-9-Add-main-app-design
Lina0Elman May 3, 2025
7cf082e
reverted unnecessary changes
Lina0Elman May 3, 2025
80a9a49
added small fix
Lina0Elman May 3, 2025
54ee7f7
added ui changes
Lina0Elman May 6, 2025
0110f1e
removed unnecessary
Lina0Elman May 6, 2025
5de1e1d
changed footer & top bar colors
Lina0Elman May 6, 2025
9978456
added initial try to use resume in main dashboard
Lina0Elman May 8, 2025
0c49af0
added parsing cv and auto fill
Lina0Elman May 8, 2025
ab3c29c
changed button of upload cv
Lina0Elman May 8, 2025
9ed7fb4
fixed placement of button
Lina0Elman May 8, 2025
3f235d6
Add ClassName To MainDashboard
taljacob2 May 18, 2025
dd004cb
Rename Dashboard As Feed
taljacob2 May 18, 2025
d06b45d
Add Feed To TopBar
taljacob2 May 18, 2025
62fea55
Reorder TopBar Icons
taljacob2 May 18, 2025
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
2 changes: 2 additions & 0 deletions nextstep-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import validateUser from "./middleware/validateUser";
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';

const specs = swaggerJsdoc(options);

Expand Down Expand Up @@ -74,5 +75,6 @@ app.use('/user', usersRoutes);
app.use('/resource', resource_routes);
app.use('/room', roomsRoutes);
app.use('/resume', resume_routes);
app.use('/github', githubRoutes);

export { app, corsOptions };
93 changes: 93 additions & 0 deletions nextstep-backend/src/controllers/github_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import axios from 'axios';
import { Request, Response } from 'express';

const clientId = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;

export const handleGitHubOAuth = async (req: Request, res: Response) => {
const { code } = req.body;

if (!code) {
return res.status(400).json({ error: 'Authorization code is required' });
}

try {
const tokenResponse = await axios.post('https://github.com/login/oauth/access_token', {
headers: { 'Content-Type': 'application/json' },
client_id: clientId,
client_secret: clientSecret,
code: code,
}) as any;

const tokenData = await tokenResponse.data;
const params = new URLSearchParams(tokenData);
const accessToken = params.get('access_token');

if (!accessToken) {
return res.status(400).json({ error: 'Failed to retrieve access token' });
}

const userResponse = await axios.get('https://api.github.com/user', {
headers: { Authorization: `Bearer ${accessToken}` },
}) as {status: number, data: { login?: string }, json: () => Promise<any>};

if (userResponse.status !== 200) {
const errorText = await userResponse.data;
return res.status(400).json({ error: errorText });
}

const userData = await userResponse.data;
res.json({ username: userData.login });
} catch (error) {
console.error('Error during GitHub OAuth:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

export const fetchGitHubRepos = async (req: Request, res: Response) => {
const { username } = req.params;
const { accessToken } = req.query; // Optional access token for authenticated requests

try {
const apiUrl = `https://api.github.com/users/${username}/repos`;

const headers = accessToken
? { Authorization: `Bearer ${accessToken}` }
: undefined;

const response = await axios.get(apiUrl, { headers }) as { data: any, status: number };

if (response.status !== 200) {
return res.status(400).json({ error: `Error fetching repos: ${response.status}` });
}

const repos = await response.data;
res.json(repos);
} catch (error) {
console.error('Error fetching repos:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

export const fetchRepoLanguages = async (req: Request, res: Response) => {
const { repoUrl } = req.query;

if (!repoUrl) {
return res.status(400).json({ error: 'Repository URL is required' });
}

try {
// Convert the repoUrl to the GitHub API URL if necessary
const apiUrl = (repoUrl as string).replace('https://github.com/', 'https://api.github.com/repos/');
const response = await axios.get(`${apiUrl}/languages`);

if (response.status !== 200) {
return res.status(400).json({ error: `Error fetching languages: ${response.statusText}` });
}

res.json(response.data);
} catch (error) {
console.error('Error fetching languages:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
18 changes: 16 additions & 2 deletions nextstep-backend/src/controllers/resume_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { config } from '../config/config';
import fs from 'fs';
import path from 'path';
import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume } from '../services/resume_service';
import { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume, parseResumeFields } from '../services/resume_service';
import multer from 'multer';
import { CustomRequest } from "types/customRequest";
import { handleError } from "../utils/handle_error";
Expand Down Expand Up @@ -93,4 +93,18 @@ const generateResume = async (req: Request, res: Response) => {
}
};

export default { getResumeScore, getStreamResumeScore, getTemplates, generateResume };

const parseResume = async (req: Request, res: Response) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No resume file uploaded' });
}
const parsed = await parseResumeFields(req.file.buffer, req.file.originalname);
return res.status(200).json(parsed);
} catch (err: any) {
console.error('Error parsing resume:', err);
return handleError(err, res);
}
};

export default { parseResume, getResumeScore, getStreamResumeScore, getTemplates, generateResume };
10 changes: 10 additions & 0 deletions nextstep-backend/src/routes/github_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import express from 'express';
import { handleGitHubOAuth, fetchGitHubRepos, fetchRepoLanguages } from '../controllers/github_controller';

const router = express.Router();

router.post('/oauth', handleGitHubOAuth);
router.get('/repos/:username', fetchGitHubRepos);
router.get('/languages', fetchRepoLanguages);

export default router;
5 changes: 5 additions & 0 deletions nextstep-backend/src/routes/resume_routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import express, { Request, Response } from 'express';
import Resume from '../controllers/resume_controller';
import { CustomRequest } from "types/customRequest";
import multer from 'multer';

const upload = multer();

const router = express.Router();

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

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

router.post('/parseResume', upload.single('resume'), Resume.parseResume);

export default router;
57 changes: 56 additions & 1 deletion nextstep-backend/src/services/resume_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import pdfParse from 'pdf-parse';
import AdmZip from 'adm-zip';
import { DOMParser, XMLSerializer } from 'xmldom';

export interface ParsedResume {
aboutMe: string;
skills: string[];
roleMatch: string;
experience:string[];
education?: string[];
}

const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named Bob the Resume builder.
You will review resumes with or without job descriptions.
You are an expert in resume evaluation and provide constructive feedback with dynamic evaluation.
Expand Down Expand Up @@ -386,4 +394,51 @@ Rules:
}
};

export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume };

/**
* Extracts raw text from the uploaded resume buffer,
* prompts the AI to return { aboutMe, skills[], roleMatch, experience[] } as JSON.
*/
const parseResumeFields = async (
fileBuffer: Buffer,
originalName: string
): Promise<ParsedResume> => {
// 1) Extract text
const ext = path.extname(originalName).toLowerCase();
let text: string;
if (ext === '.pdf') {
const data = await pdfParse(fileBuffer);
text = data.text;
} else {
// mammoth supports buffer input
const { value } = await mammoth.extractRawText({ buffer: fileBuffer });
text = value;
}

// 2) Build the extraction prompt
const prompt = `
Extract from this resume the following fields as JSON:
• "aboutMe": a 1–2 sentence self-summary.
• "skills": an array of technical skills.
• "roleMatch": one-sentence best-fit role suggestion.
• "experience": an array of 3–5 bullet points of key achievements.

Resume text:
---
${text}
---
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", ...}
`;

// 3) Call your Chat AI
const aiResponse = await chatWithAI(
SYSTEM_TEMPLATE, // you can reuse your existing SYSTEM_TEMPLATE or define a new one
[prompt]
);

// 4) Parse & return
const parsed = JSON.parse(aiResponse.trim().replace("```json", "").replace("```", "")) as ParsedResume;
return parsed;
};

export { scoreResume, streamScoreResume, getResumeTemplates, generateImprovedResume, parseResumeFields };
6 changes: 4 additions & 2 deletions nextstep-frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Login from './pages/Login';
import Register from './pages/Register';
import Profile from './pages/Profile';
import './App.css'
import Dashboard from './pages/Dashboard';
import Feed from './pages/Feed';
import Footer from './components/Footer';
import RequireAuth from './hoc/RequireAuth';
import NewPost from './pages/NewPost';
Expand All @@ -13,6 +13,7 @@ import Chat from './pages/Chat';
import Resume from './pages/Resume';
import TopBar from './components/TopBar';
import Layout from './components/Layout';
import MainDashboard from './pages/MainDashboard';

const App: React.FC = () => {
return (
Expand All @@ -22,12 +23,13 @@ const App: React.FC = () => {
<Route path="/" element={<Layout className="login"><Login /></Layout>} />
<Route path="/login" element={<Layout className="login"><Login /></Layout>} />
<Route path="/register" element={<Layout className="register"><Register /></Layout>} />
<Route path="/dashboard" element={<RequireAuth><TopBar /><Layout className="dashboard"><Dashboard /></Layout></RequireAuth>} />
<Route path="/feed" element={<RequireAuth><TopBar /><Layout className="feed"><Feed /></Layout></RequireAuth>} />
<Route path="/profile" element={<RequireAuth><TopBar /><Layout className="profile"><Profile /></Layout></RequireAuth>} />
<Route path="/new-post" element={<RequireAuth><TopBar /><Layout className="new-post"><NewPost /></Layout></RequireAuth>} />
<Route path="/post/:postId" element={<RequireAuth><TopBar /><Layout className="post-details"><PostDetails /></Layout></RequireAuth>} />
<Route path="/chat" element={<RequireAuth><TopBar /><Layout className="chat"><Chat /></Layout></RequireAuth>} />
<Route path="/resume" element={<RequireAuth><TopBar /><Layout className="resume"><Resume /></Layout></RequireAuth>} />
<Route path="/main-dashboard" element={<RequireAuth><TopBar /><Layout className="main-dashboard"><MainDashboard /></Layout></RequireAuth>} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</Router>
Expand Down
2 changes: 1 addition & 1 deletion nextstep-frontend/src/components/Footer.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.footer {
background-color: var(--color-5);
background-color: #233752;
color: var(--color-2);
padding: 10px 20px;
text-align: center;
Expand Down
23 changes: 14 additions & 9 deletions nextstep-frontend/src/components/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material';
import { Home, Person, Message, Logout, DocumentScannerTwoTone } from '@mui/icons-material';
import { Home, Person, Message, Logout, DocumentScannerTwoTone, Feed } from '@mui/icons-material';
import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts";
import api from "../serverApi.ts";

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

return (
<AppBar position="relative" sx={{ width: '100vw', left: 0 }} className='top-bar'>
<AppBar position="relative" sx={{ width: '100vw', left: 0, backgroundColor: '#233752' }} className='top-bar'>
<Toolbar>
<Tooltip title="Home">
<IconButton color="inherit" onClick={() => navigate('/dashboard')} sx={{ mx: 1 }}>
<IconButton color="inherit" onClick={() => navigate('/main-dashboard')} sx={{ mx: 1 }}>
<Home fontSize='large'/>
</IconButton>
</Tooltip>
<Tooltip title="Profile">
<IconButton color="inherit" onClick={() => navigate('/profile')} sx={{ mx: 1 }}>
<Person fontSize='large'/>
<Tooltip title="Resume">
<IconButton color="inherit" onClick={() => navigate('/resume')} sx={{ mx: 1 }}>
<DocumentScannerTwoTone fontSize='large'/>
</IconButton>
</Tooltip>
<Tooltip title="Feed">
<IconButton color="inherit" onClick={() => navigate('/feed')} sx={{ mx: 1 }}>
<Feed fontSize='large'/>
</IconButton>
</Tooltip>
<Tooltip title="Chat">
<IconButton color="inherit" onClick={() => navigate('/chat')} sx={{ mx: 1 }}>
<Message fontSize='large'/>
</IconButton>
</Tooltip>
<Tooltip title="Resume">
<IconButton color="inherit" onClick={() => navigate('/resume')} sx={{ mx: 1 }}>
<DocumentScannerTwoTone fontSize='large'/>
<Tooltip title="Profile">
<IconButton color="inherit" onClick={() => navigate('/profile')} sx={{ mx: 1 }}>
<Person fontSize='large'/>
</IconButton>
</Tooltip>
<Box sx={{ flexGrow: 1 }} />
Expand Down
Loading
Loading