-
Notifications
You must be signed in to change notification settings - Fork 139
feat: add resume analysis history tracking with database model and UI dashboard #1606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
KaranUnique
merged 2 commits into
Canopus-Labs:main
from
TanCodeX:feat/resume-analysis-history
Aug 9, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
240 changes: 240 additions & 0 deletions
240
frontend/src/pages/ResumeBuilder/ResumeAnalysisHistory.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| 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 */} | ||
| <button | ||
| onClick={() => toggleExpand(item._id)} | ||
| aria-expanded={expandedId === item._id} | ||
| aria-controls={`expandable-content-${item._id}`} | ||
| aria-label={`Toggle analysis details for ${item.targetRole || "General"}`} | ||
| className="w-full text-left 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> | ||
| <div 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} />} | ||
| </div> | ||
| </div> | ||
| </button> | ||
|
|
||
| {/* Expanded Details */} | ||
| {expandedId === item._id && ( | ||
| <div id={`expandable-content-${item._id}`} 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> | ||
|
|
||
| {/* Missing Keywords */} | ||
| {item.missingKeywords?.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"> | ||
| <Target size={16} className="text-indigo-500" /> Missing Keywords | ||
| </h4> | ||
| <div className="flex flex-wrap gap-2"> | ||
| {item.missingKeywords.map((keyword, i) => ( | ||
| <span key={i} className="px-3 py-1 text-xs font-bold rounded-lg bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-500/20"> | ||
| {keyword} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| </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; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.