iris_backend / src /pages /ApplicantATS.jsx
sameer2026's picture
fix: ats resume builder UI and backend extraction handling
9d6cc86
import React, { useRef, useState } from 'react';
import ApplicantLayout from '../components/ApplicantLayout';
import ResumeBuilder from '../components/ResumeBuilder';
import { motion } from 'framer-motion';
import {
UploadIcon,
SpinnerIcon
} from '../components/Icons';
export default function ApplicantATS({ onNavigate }) {
const fileInputRef = useRef(null);
const [resumeFile, setResumeFile] = useState(null);
const [jobDescription, setJobDescription] = useState('');
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysisResult, setAnalysisResult] = useState(null);
const [showBuilder, setShowBuilder] = useState(false);
// -------------------------
// Handle Resume Upload
// -------------------------
const handleFileChange = (e) => {
const file = e.target.files[0];
if (
file &&
[
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
].includes(file.type)
) {
setResumeFile(file);
} else {
alert('Please upload a PDF or DOCX file only.');
}
};
const clearFile = () => {
setResumeFile(null);
fileInputRef.current.value = '';
};
// -------------------------
// Simulated ATS Analysis
// -------------------------
const handleAnalyze = async () => {
if (!resumeFile || !jobDescription.trim()) {
alert("Please upload a resume and enter a job description.");
return;
}
setIsAnalyzing(true);
setAnalysisResult(null);
const formData = new FormData();
formData.append("resume", resumeFile);
formData.append("job_description", jobDescription);
try {
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
const response = await fetch(`${API_URL}/analyze-ats`, {
method: "POST",
body: formData,
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.detail || "Analysis failed. Please try again.");
}
const result = await response.json();
if (result.status === "success" && result.data) {
setAnalysisResult(result.data);
} else {
throw new Error("Invalid response format.");
}
} catch (error) {
console.error("ATS Check Error:", error);
alert(`Analysis Error: ${error.message}`);
} finally {
setIsAnalyzing(false);
}
};
const isDisabled = !resumeFile || !jobDescription.trim() || isAnalyzing;
if (showBuilder) {
return (
<ApplicantLayout activePage="applicant-ats" onNavigate={onNavigate}>
<ResumeBuilder analysisResult={analysisResult} onBack={() => setShowBuilder(false)} />
</ApplicantLayout>
);
}
return (
<ApplicantLayout activePage="applicant-ats" onNavigate={onNavigate}>
<div>
<header style={{ marginBottom: '2rem' }}>
<h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>ATS Checker</h1>
<p style={{ color: '#d1d5db', marginTop: '0.5rem' }}>Upload your resume and job description to check ATS compatibility</p>
</header>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '2rem', alignItems: 'flex-start' }}>
{/* Left Side - Loading Screen or Results */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{isAnalyzing ? (
// Loading Screen
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
style={{
backgroundColor: 'rgba(239, 68, 68, 0.05)',
border: '1px solid rgba(97, 239, 68, 0.2)',
borderRadius: '1rem',
padding: '4rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '400px',
}}
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
style={{ width: '80px', height: '80px', marginBottom: '2rem' }}
>
<svg viewBox="0 0 24 24" fill="none" stroke="#EF4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
</motion.div>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'white', marginBottom: '0.5rem' }}>Analyzing Resume...</h2>
<p style={{ color: '#d1d5db', textAlign: 'center' }}>Please wait while we analyze your resume against the job description</p>
</motion.div>
) : analysisResult ? (
// Results Section
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
style={{
backgroundColor: 'rgba(251, 191, 36, 0.05)',
border: '1px solid rgba(251, 191, 36, 0.2)',
borderRadius: '1rem',
padding: '1.5rem',
}}
>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1.5rem' }}>Analysis Results</h2>
{/* Score Card */}
<div style={{ backgroundColor: 'rgba(251, 191, 36, 0.1)', border: '1px solid rgba(251, 191, 36, 0.3)', borderRadius: '1rem', padding: '2rem', textAlign: 'center', marginBottom: '2rem' }}>
<div style={{ fontSize: '4rem', fontWeight: 'bold', color: '#FBBF24', marginBottom: '0.5rem' }}>
{analysisResult.score}/100
</div>
<p style={{ fontSize: '1.25rem', color: '#d1d5db' }}>ATS Match Score</p>
<p style={{ color: '#d1d5db', marginTop: '0.5rem', marginBottom: '1.5rem' }}>{analysisResult.summary}</p>
<button
onClick={() => setShowBuilder(true)}
style={{
backgroundColor: '#FBBF24',
color: '#111827',
padding: '0.75rem 1.5rem',
borderRadius: '0.5rem',
fontWeight: 'bold',
border: 'none',
cursor: 'pointer',
fontSize: '1rem',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}
>
Create Optimized Resume
</button>
</div>
{/* Keyword Matches */}
<div style={{ marginBottom: '2rem' }}>
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Keyword Analysis</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{analysisResult.matches.map((match, index) => (
<div
key={index}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem',
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: '0.5rem',
border: '1px solid rgba(251, 191, 36, 0.2)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<span style={{ fontWeight: 'bold' }}>{match.keyword}</span>
<span style={{
padding: '0.25rem 0.75rem',
borderRadius: '9999px',
fontSize: '0.75rem',
backgroundColor: match.found ? 'rgba(16, 185, 129, 0.2)' : 'rgba(239, 68, 68, 0.2)',
color: match.found ? '#34D399' : '#F87171',
}}>
{match.found ? 'Found' : 'Missing'}
</span>
</div>
<span style={{ color: '#d1d5db' }}>Importance: {match.importance}</span>
</div>
))}
</div>
</div>
{/* Recommendations */}
<div>
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Recommendations to Make Your Resume ATS-Friendly</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{analysisResult.recommendations.map((rec, index) => (
<div
key={index}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: '1rem',
padding: '1rem',
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: '0.5rem',
border: '1px solid rgba(251, 191, 36, 0.2)',
}}
>
<span style={{ color: '#FBBF24', fontSize: '1.25rem', lineHeight: '1' }}></span>
<p style={{ color: '#d1d5db', margin: 0, flex: 1 }}>{rec}</p>
</div>
))}
</div>
</div>
</motion.div>
) : (
// Empty State
<div style={{
backgroundColor: 'rgba(251, 191, 36, 0.05)',
border: '1px solid rgba(251, 191, 36, 0.2)',
borderRadius: '1rem',
padding: '4rem',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '400px',
}}>
<p style={{ color: '#d1d5db', fontSize: '1.125rem' }}>Upload your resume and job description, then click "Analyze Resume" to see results here.</p>
</div>
)}
</div>
{/* Right Side - Upload Resume (Top) and Job Description (Bottom) */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{/* Upload Resume Section */}
<div style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(236, 183, 26, 0.86)', borderRadius: '1rem', padding: '1.5rem' }}>
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1.5rem' }}>Upload Resume</h3>
<div style={{ border: '2px dashed rgba(236, 183, 26, 0.46)', borderRadius: '1rem', padding: '2rem', textAlign: 'center' }}>
<input type="file" id="resume-upload" accept=".pdf,.doc,.docx" onChange={handleFileChange} style={{ display: 'none' }} />
<label htmlFor="resume-upload" style={{ cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<UploadIcon />
<p style={{ color: '#d1d5db', marginTop: '1rem' }}>{resumeFile ? resumeFile.name : 'Click to upload or drag and drop'}</p>
<p style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.5)', marginTop: '0.5rem' }}>PDF, DOC, or DOCX (MAX. 5MB)</p>
</label>
</div>
</div>
{/* Job Description Section */}
<div style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(236, 183, 26, 0.86)', borderRadius: '1rem', padding: '1.5rem' }}>
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Job Description</h3>
<textarea
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
placeholder="Paste the job description here..."
rows="15"
style={{
width: '100%',
padding: '1rem',
borderRadius: '0.5rem',
border: '1px solid rgba(236, 183, 26, 0.46)',
backgroundColor: 'rgba(255,255,255,0.1)',
color: 'white',
resize: 'vertical',
fontFamily: 'inherit',
}}
className="hide-scrollbar"
/>
</div>
{/* Analyze Button */}
<motion.button
onClick={handleAnalyze}
disabled={isAnalyzing || !resumeFile || !jobDescription.trim()}
whileHover={{ scale: (isAnalyzing || !resumeFile || !jobDescription.trim()) ? 1 : 1.03 }}
whileTap={{ scale: (isAnalyzing || !resumeFile || !jobDescription.trim()) ? 1 : 0.98 }}
style={{
backgroundColor: (!resumeFile || !jobDescription.trim()) ? 'rgba(251, 191, 36, 0.3)' : '#FBBF24',
color: 'white',
padding: '1rem 2rem',
borderRadius: '0.5rem',
fontWeight: 'bold',
cursor: (isAnalyzing || !resumeFile || !jobDescription.trim()) ? 'not-allowed' : 'pointer',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.125rem',
width: '100%',
}}
>
{isAnalyzing && <SpinnerIcon />}
{isAnalyzing ? 'Analyzing...' : 'Analyze Resume'}
</motion.button>
</div>
</div>
</div>
</ApplicantLayout>
);
}