Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |