ResumeLens/web/src/pages/upload.tsx
Hayden Hargreaves 5500449334 feat: docker and nginx
Working on deployments
2026-03-31 12:40:57 -07:00

332 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import type { ResumeAnalysisResult } from '../types/resumeAnalysis';
import Modal from '../components/Modal';
import { API_ENDPOINTS } from '../config/api';
import '../assets/css/upload.css';
/**
* Upload page component for resume and job description input
* Trace: SDD_LLD_0020 - Provides responsive file-picker and text input area
* Trace: SDD_LLD_0021 - Accept textual job description input
* Trace: SDD_LLD_0022 - Display resume preview
* Trace: SDD_LLD_0027 - Manages global "loading" state with progress spinner
* Trace: SDD_HLD_0001 - Accept PDF resume input
* Trace: SDD_HLD_0004 - Read string of text from textbox
*/
export default function UploadPage() {
const [resumeFile, setResumeFile] = useState<File | null>(null);
const [resumePreview, setResumePreview] = useState<string | null>(null);
const [jobDescription, setJobDescription] = useState('');
const [isDragging, setIsDragging] = useState(false);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [modalState, setModalState] = useState<{
isOpen: boolean;
title: string;
message: string;
type: 'error' | 'warning' | 'info';
}>({
isOpen: false,
title: '',
message: '',
type: 'error'
});
const fileInputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
// Helper to show modal
const showModal = (title: string, message: string, type: 'error' | 'warning' | 'info' = 'error') => {
setModalState({ isOpen: true, title, message, type });
};
const closeModal = () => {
setModalState(prev => ({ ...prev, isOpen: false }));
};
// Trace: SDD_LLD_0001 - Accept only PDF uploads (validation)
// Trace: SDD_HLD_0001 - Accept only PDF formatting for input resumes
const handleFileSelect = (file: File) => {
if (file && (file.type === 'application/pdf' || file.type.startsWith('image/'))) {
setResumeFile(file);
// Trace: SDD_LLD_0022 - Display resume preview
// Create preview URL
const reader = new FileReader();
reader.onloadend = () => {
setResumePreview(reader.result as string);
};
if (file.type === 'application/pdf') {
// For PDF, we'll show a placeholder or use an iframe
reader.readAsDataURL(file);
} else {
reader.readAsDataURL(file);
}
} else {
// Trace: SDD_LLD_0002 - Reject non-PDF uploads
// Trace: SRD_InterfaceReq_0015 - Display errors in persistent manner
showModal(
'Invalid File Type',
'Please upload a PDF or image file. Other file formats are not supported.',
'warning'
);
}
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file) {
handleFileSelect(file);
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
};
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
};
const handleRemoveFile = () => {
setResumeFile(null);
setResumePreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
// Trace: SDD_LLD_0005 - Integrate with upload interface to send data to backend
// Trace: SDD_LLD_0027 - Show loading spinner during active API calls
const handleAnalyze = async () => {
if (!resumeFile) {
showModal(
'Resume Required',
'Please upload a resume before analyzing.',
'warning'
);
return;
}
if (!jobDescription.trim()) {
showModal(
'Job Description Required',
'Please enter a job description to compare your resume against.',
'warning'
);
return;
}
// Trace: SDD_LLD_0027 - Manages loading state and disables submission buttons
setIsAnalyzing(true);
try {
// Trace: SDD_LLD_0005 - Send multipart/form data to backend endpoint
const formData = new FormData();
formData.append('resume', resumeFile);
formData.append('job_description', jobDescription);
// Trace: SDD_HLD_0005 - Provide structured inputs to AI grader
const response = await fetch(API_ENDPOINTS.analyze, { method: 'POST', body: formData });
// Trace: SRD_UseCase_0006 - Display error message when rate limit reached
// Trace: SRD_QualAssurReq_0006 - Handle rate limit error appropriately
// Trace: SRD_InterfaceReq_0015 - Display errors in persistent manner
if (response.status === 429) {
const errorData = await response.json();
showModal(
'Rate Limit Exceeded',
errorData.error || 'You can make up to 10 requests per hour. Please try again later.',
'warning'
);
setIsAnalyzing(false);
return;
}
if (!response.ok) throw new Error(`Server error: ${response.status}`);
// Trace: SDD_LLD_0019 - Receive JSON output from backend
const analysisData: ResumeAnalysisResult = await response.json();
// Trace: SDD_LLD_0028 - Navigate to results page
navigate('/results', { state: analysisData });
} catch (error) {
// Trace: SDD_LLD_0030 - Display persistent error messages
// Trace: SRD_InterfaceReq_0015 - Display errors in persistent manner
console.error('Error analyzing resume:', error);
showModal(
'Analysis Failed',
'Failed to analyze resume. Please check your connection and try again.',
'error'
);
setIsAnalyzing(false);
}
};
return (
<div className="upload-page">
<div className="upload-container">
{/* Header */}
<div className="upload-header">
<Link to="/" className="back-link">
Back to Home
</Link>
<h1 className="upload-title">
Upload Your <span className="gradient-text">Resume</span>
</h1>
<p className="upload-subtitle">
Upload your resume and add a job description to get personalized feedback
</p>
</div>
<div className="upload-content">
{/* Left Column - Upload Area */}
<div className="upload-section">
<div className="section-header">
<h2 className="section-title">Resume Upload</h2>
{resumeFile && (
<button onClick={handleRemoveFile} className="remove-btn">
Remove File
</button>
)}
</div>
{!resumeFile ? (
<div
className={`upload-area ${isDragging ? 'dragging' : ''}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => fileInputRef.current?.click()}
>
<div className="upload-icon">📄</div>
<p className="upload-text">
<span className="upload-text-bold">Click to upload</span> or drag and drop
</p>
<p className="upload-text-small">
PDF (MAX. 10MB)
</p>
<input
ref={fileInputRef}
type="file"
accept=".pdf"
onChange={handleFileInputChange}
className="file-input"
/>
</div>
) : (
<div className="file-info">
<div className="file-info-header">
<div className="file-icon">📄</div>
<div className="file-details">
<p className="file-name">{resumeFile.name}</p>
<p className="file-size">
{(resumeFile.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</div>
</div>
)}
{/* Resume Preview */}
{/* Trace: SDD_LLD_0022 - Display resume preview */}
{resumePreview && (
<div className="preview-section">
<h3 className="preview-title">Resume Preview</h3>
<div className="preview-container">
{resumeFile?.type === 'application/pdf' ? (
<iframe
src={`${resumePreview}#toolbar=0&navpanes=0&scrollbar=0`}
className="preview-pdf"
title="Resume Preview"
/>
) : (
<img
src={resumePreview}
alt="Resume Preview"
className="preview-image"
/>
)}
</div>
</div>
)}
</div>
{/* Right Column - Job Description */}
{/* Trace: SDD_LLD_0021 - Accept textual job description input */}
{/* Trace: SDD_HLD_0004 - Read string of text from textbox */}
<div className="job-description-section">
<div className="section-header">
<h2 className="section-title">Job Description</h2>
</div>
<p className="section-description">
Paste the job description to get targeted feedback on how well your resume matches the requirements.
</p>
<textarea
className="job-description-input"
placeholder="Paste the job description here...&#10;&#10;Example:&#10;We are looking for a software engineer with 3+ years of experience in React and TypeScript..."
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
rows={15}
/>
<div className="char-count">
{jobDescription.length} characters
</div>
</div>
</div>
{/* Action Buttons */}
<div className="upload-actions">
<Link to="/" className="btn btn-secondary">
Cancel
</Link>
<button
onClick={handleAnalyze}
className="btn btn-primary btn-large"
disabled={!resumeFile || !jobDescription.trim() || isAnalyzing}
>
Analyze Resume
</button>
</div>
</div>
{/* Loading Overlay */}
{/* Trace: SDD_LLD_0027 - Display loading indicator during processing */}
{isAnalyzing && (
<div className="loading-overlay">
<div className="loading-modal">
<div className="loading-spinner" />
<h2 className="loading-title">Analyzing Resume</h2>
<p className="loading-message">
Our AI is reviewing your resume against the job description.
This usually takes 1530 seconds.
</p>
</div>
</div>
)}
{/* Error/Warning Modal */}
{/* Trace: SDD_LLD_0030 - Display persistent error messages */}
{/* Trace: SRD_InterfaceReq_0015 - Display errors in persistent manner (not toasts or popups) */}
<Modal
isOpen={modalState.isOpen}
onClose={closeModal}
title={modalState.title}
message={modalState.message}
type={modalState.type}
/>
</div>
);
}