Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
38 changes: 37 additions & 1 deletion backend/controllers/resumeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw
console.error("Failed to parse Gemini JSON:", aiResponse);
return res.status(500).json({ message: "AI response parsing failed.", raw: aiResponse });
}
try {
await ResumeAnalysisHistory.create({
user: req.user._id,
targetRole,
resumeScore: jsonResult.resumeScore || 0,
roleMatch: jsonResult.roleMatch || 0,
missingSkills: jsonResult.missingSkills || [],
missingKeywords: jsonResult.missingKeywords || [],
actionVerbs: jsonResult.actionVerbs || [],
formattingIssues: jsonResult.formattingIssues || [],
missingProjects: jsonResult.missingProjects || [],
atsCompatibility: jsonResult.atsCompatibility || {},
suggestions: jsonResult.suggestions || [],
sections: jsonResult.sections || {}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (dbErr) {
console.error("Failed to save analysis history:", dbErr);
}

res.status(200).json(jsonResult);

Expand All @@ -165,6 +183,7 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw
}

const Resume = require("../models/Resume");
const ResumeAnalysisHistory = require("../models/ResumeAnalysisHistory");

/**
* Save or update a user's resume record.
Expand Down Expand Up @@ -261,4 +280,21 @@ async function deleteResume(req, res) {
}
}

module.exports = { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume };
/**
* Retrieve saved resume analysis history for the authenticated user.
* @route GET /api/resume/analysis-history
*/
const getResumeAnalysisHistory = async (req, res) => {
try {
const userId = req.user._id;
const history = await ResumeAnalysisHistory.find({ user: userId })
.sort({ createdAt: -1 })
.limit(50);
res.status(200).json({ success: true, history });
} catch (error) {
console.error("Get Analysis History Error:", error);
res.status(500).json({ success: false, message: "Server Error" });
}
};

module.exports = { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume, getResumeAnalysisHistory };
24 changes: 24 additions & 0 deletions backend/models/ResumeAnalysisHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const mongoose = require("mongoose");

const ResumeAnalysisHistorySchema = new mongoose.Schema(
{
user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true },
targetRole: { type: String, default: "General" },
resumeScore: { type: Number, required: true },
roleMatch: { type: Number, required: true },
missingSkills: { type: [String], default: [] },
missingKeywords: { type: [String], default: [] },
actionVerbs: { type: [String], default: [] },
formattingIssues: { type: [String], default: [] },
missingProjects: { type: [String], default: [] },
atsCompatibility: {
status: { type: String },
remarks: { type: String }
},
suggestions: { type: [String], default: [] },
sections: { type: mongoose.Schema.Types.Mixed }
},
{ timestamps: true }
);

module.exports = mongoose.model("ResumeAnalysisHistory", ResumeAnalysisHistorySchema);
7 changes: 6 additions & 1 deletion backend/routes/resumeRoutes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const express = require('express');
const router = express.Router();
const { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume } = require('../controllers/resumeController');
const { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume, getResumeAnalysisHistory } = require('../controllers/resumeController');
const { protect } = require('../middlewares/authMiddleware');
const { upload, uploadResume, validateResumeMagicBytes } = require('../middlewares/uploadMiddleware');
const { aiLimiter } = require('../middlewares/rateLimiter');
Expand Down Expand Up @@ -28,6 +28,11 @@ router.post('/save', validateSaveResume, saveResume);
// @access Private
router.get('/my-resumes', getMyResumes);

// @route GET /api/resume/analysis-history
// @desc Get all saved resume analyses for logged-in user
// @access Private
router.get('/analysis-history', getResumeAnalysisHistory);

// @route DELETE /api/resume/:id
// @desc Delete a saved resume by ID
// @access Private
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Navigate, Outlet } from "react-router-dom";
import ResumeTemplates from "./pages/ResumeBuilder/ResumeTemplates";
import ResumeEditor from "./pages/ResumeBuilder/ResumeEditor";
import ResumeAnalyzer from "./pages/ResumeBuilder/ResumeAnalyzer";
import ResumeAnalysisHistory from "./pages/ResumeBuilder/ResumeAnalysisHistory";
import InterviewExperiences from "./pages/InterviewExperiences/InterviewExperiences";
import TermsandConditions from "./pages/Terms/TermsandConditions";
import ProjectIdeas from "./pages/ProjectIdeas/ProjectIdeas";
Expand Down Expand Up @@ -334,6 +335,16 @@ const App = () => {
</ProtectedRoute>
}
/>
<Route
path="/resume-analyzer/history"
element={
<ProtectedRoute>
<PageTransition>
<ResumeAnalysisHistory />
</PageTransition>
</ProtectedRoute>
}
/>
<Route
path="/interview-experiences"
element={
Expand Down
221 changes: 221 additions & 0 deletions frontend/src/pages/ResumeBuilder/ResumeAnalysisHistory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import React, { useState, useEffect } from "react";
import { Clock, Briefcase, Zap, CheckCircle2, AlertTriangle, AlertCircle, X, ChevronDown, ChevronUp, FileText, Target } from "lucide-react";
import axiosInstance from "../../utils/axiosinstance";
import { API_PATHS } from "../../utils/apiPaths";
import { Link } from "react-router-dom";

const ResumeAnalysisHistory = () => {
const [history, setHistory] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedId, setExpandedId] = useState(null);

useEffect(() => {
fetchHistory();
}, []);

const fetchHistory = async () => {
try {
setLoading(true);
const response = await axiosInstance.get(API_PATHS.RESUME.GET_ANALYSIS_HISTORY);
setHistory(response.data.history || []);
} catch (err) {
console.error(err);
setError("Failed to load analysis history.");
} finally {
setLoading(false);
}
};

const toggleExpand = (id) => {
setExpandedId(expandedId === id ? null : id);
};

const renderScore = (score) => {
let colorClass = "text-emerald-500 bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-500/20";
if (score < 50) {
colorClass = "text-red-500 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-500/20";
} else if (score < 75) {
colorClass = "text-amber-500 bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-500/20";
}
return (
<div className={`px-4 py-2 rounded-xl border font-black text-xl ${colorClass}`}>
{score}%
</div>
);
};

if (loading) {
return (
<div className="min-h-screen bg-[var(--color-background)] dark:bg-gradient-to-b dark:from-[#0f172a] dark:to-[#0b1120] px-5 py-10 md:px-12 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
</div>
);
}

return (
<div className="min-h-screen bg-[var(--color-background)] dark:bg-gradient-to-b dark:from-[#0f172a] dark:to-[#0b1120] px-5 py-10 md:px-12 transition-colors duration-300">
<div className="max-w-5xl mx-auto space-y-10">

{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-500/20 shadow-sm flex items-center justify-center shrink-0">
<Clock size={28} />
</div>
<div>
<h1 className="text-3xl md:text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Analysis History
</h1>
<p className="text-sm md:text-base text-gray-500 dark:text-gray-400 mt-1 font-medium">
Track your progress and revisit past resume improvements.
</p>
</div>
</div>
<Link
to="/resume-analyzer"
className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-bold transition-all shadow-sm shadow-indigo-500/25"
>
<Zap size={18} /> New Analysis
</Link>
</div>

{error && (
<div className="p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 text-sm font-medium flex items-center gap-3">
<AlertCircle size={18} /> {error}
</div>
)}

{history.length === 0 && !error ? (
<div className="bg-white dark:bg-[#151c2f] border border-gray-200 dark:border-white/5 rounded-3xl p-12 text-center shadow-sm flex flex-col items-center">
<div className="w-20 h-20 rounded-full bg-gray-50 dark:bg-white/5 flex items-center justify-center text-gray-400 mb-4">
<FileText size={40} />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">No Analysis History Yet</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto mb-6">
You haven't analyzed any resumes yet. Upload a resume to get instant ATS parsing and AI feedback.
</p>
<Link
to="/resume-analyzer"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-gray-300 dark:border-gray-700 font-bold hover:bg-gray-50 dark:hover:bg-white/5 transition text-gray-700 dark:text-gray-200"
>
Get Started
</Link>
</div>
) : (
<div className="space-y-6">
{history.map((item) => (
<div key={item._id} className="bg-white dark:bg-[#151c2f] border border-gray-200 dark:border-white/5 rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">

{/* Summary Row */}
<div
onClick={() => toggleExpand(item._id)}
className="p-6 sm:p-8 cursor-pointer flex flex-col sm:flex-row sm:items-center justify-between gap-6"
>
<div className="flex items-center gap-6">
{renderScore(item.resumeScore)}
<div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<Briefcase size={18} className="text-indigo-500" />
{item.targetRole || "General"}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 font-medium flex items-center gap-2">
<Clock size={14} />
{new Date(item.createdAt).toLocaleString()}
</p>
</div>
</div>

<div className="flex items-center justify-between sm:justify-end gap-6 w-full sm:w-auto border-t sm:border-0 border-gray-100 dark:border-gray-800 pt-4 sm:pt-0">
<div className="flex flex-col items-start sm:items-end">
<span className="text-xs uppercase tracking-wider font-bold text-gray-400">ATS Status</span>
<span className={`font-bold ${
item.atsCompatibility?.status === 'Good' ? 'text-emerald-500' :
item.atsCompatibility?.status === 'Average' ? 'text-amber-500' : 'text-red-500'
}`}>
{item.atsCompatibility?.status || 'Unknown'}
</span>
</div>
<button className="w-10 h-10 rounded-full bg-gray-50 dark:bg-white/5 flex items-center justify-center text-gray-500 hover:text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 transition">
{expandedId === item._id ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>
</div>

{/* Expanded Details */}
{expandedId === item._id && (
<div className="p-6 sm:p-8 border-t border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-[#111827] space-y-8 animate-in slide-in-from-top-4 duration-300">

<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-6">
{/* Missing Skills */}
<div className="bg-white dark:bg-[#151c2f] p-6 rounded-2xl border border-gray-200 dark:border-white/5">
<h4 className="text-sm font-bold tracking-wide uppercase text-gray-700 dark:text-gray-300 mb-4 flex items-center gap-2">
<AlertTriangle size={16} className="text-amber-500" /> Missing Skills
</h4>
{item.missingSkills?.length > 0 ? (
<div className="flex flex-wrap gap-2">
{item.missingSkills.map((skill, i) => (
<span key={i} className="px-3 py-1 text-xs font-bold rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-100 dark:border-red-500/20">
{skill}
</span>
))}
</div>
) : (
<p className="text-sm text-gray-500">None detected</p>
)}
</div>

{/* Formatting Issues */}
{item.formattingIssues?.length > 0 && (
<div className="bg-white dark:bg-[#151c2f] p-6 rounded-2xl border border-gray-200 dark:border-white/5">
<h4 className="text-sm font-bold tracking-wide uppercase text-gray-700 dark:text-gray-300 mb-4 flex items-center gap-2">
<AlertCircle size={16} className="text-red-500" /> Formatting Issues
</h4>
<ul className="space-y-2">
{item.formattingIssues.map((issue, i) => (
<li key={i} className="text-sm text-gray-600 dark:text-gray-400 flex items-start gap-2">
<X size={14} className="text-red-400 mt-1 shrink-0" />
{issue}
</li>
))}
</ul>
</div>
)}
</div>

<div className="space-y-6">
{/* Suggestions */}
<div className="bg-white dark:bg-[#151c2f] p-6 rounded-2xl border border-gray-200 dark:border-white/5 h-full">
<h4 className="text-sm font-bold tracking-wide uppercase text-gray-700 dark:text-gray-300 mb-4 flex items-center gap-2">
<Zap size={16} className="text-emerald-500" /> Suggestions
</h4>
<div className="space-y-3">
{item.suggestions?.length > 0 ? item.suggestions.map((sug, i) => (
<div key={i} className="flex items-start gap-3">
<CheckCircle2 size={16} className="text-emerald-500 shrink-0 mt-0.5" />
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
{sug}
</p>
</div>
)) : (
<p className="text-sm text-gray-500">No suggestions available.</p>
)}
</div>
</div>
</div>
</div>

</div>
)}
</div>
))}
</div>
)}

</div>
</div>
);
};

export default ResumeAnalysisHistory;
33 changes: 21 additions & 12 deletions frontend/src/pages/ResumeBuilder/ResumeAnalyzer.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from "react";
import { Upload, FileText, Briefcase, Zap, CheckCircle2, AlertTriangle, AlertCircle, X, ChevronRight, RefreshCw, Target } from "lucide-react";
import { Upload, FileText, Briefcase, Zap, CheckCircle2, AlertTriangle, AlertCircle, X, ChevronRight, RefreshCw, Target, History } from "lucide-react";
import axiosInstance from "../../utils/axiosinstance";
import { API_PATHS } from "../../utils/apiPaths";
import { Link } from "react-router-dom";

const ResumeAnalyzer = () => {
const [file, setFile] = useState(null);
Expand Down Expand Up @@ -128,18 +129,26 @@ const ResumeAnalyzer = () => {
<div className="max-w-5xl mx-auto space-y-10">

{/* Header */}
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-500/20 shadow-sm flex items-center justify-center shrink-0">
<Zap size={28} />
</div>
<div>
<h1 className="text-3xl md:text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">
AI Resume Analyzer
</h1>
<p className="text-sm md:text-base text-gray-500 dark:text-gray-400 mt-1 font-medium">
Upload your resume for real-time ATS parsing, scoring, and role-matched AI suggestions.
</p>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-500/20 shadow-sm flex items-center justify-center shrink-0">
<Zap size={28} />
</div>
<div>
<h1 className="text-3xl md:text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">
AI Resume Analyzer
</h1>
<p className="text-sm md:text-base text-gray-500 dark:text-gray-400 mt-1 font-medium">
Upload your resume for real-time ATS parsing, scoring, and role-matched AI suggestions.
</p>
</div>
</div>
<Link
to="/resume-analyzer/history"
className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-gray-100 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-white/10 text-gray-800 dark:text-gray-200 font-bold transition-all border border-gray-200 dark:border-gray-700"
>
<History size={18} /> View History
</Link>
</div>

{/* Dynamic Display */}
Expand Down
Loading
Loading