197 lines
6.6 KiB
JavaScript
197 lines
6.6 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import api from '../auth/apiClient.js';
|
|
|
|
function ResumeRewrite() {
|
|
const [resumeFile, setResumeFile] = useState(null);
|
|
const [jobTitle, setJobTitle] = useState('');
|
|
const [jobDescription, setJobDescription] = useState('');
|
|
const [optimizedResume, setOptimizedResume] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [remainingOptimizations, setRemainingOptimizations] = useState(null);
|
|
const [resetDate, setResetDate] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const ALLOWED_TYPES = [
|
|
'application/pdf',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
|
|
];
|
|
const MAX_MB = 5;
|
|
|
|
const handleFileChange = (e) => {
|
|
const f = e.target.files?.[0] || null;
|
|
setOptimizedResume('');
|
|
setError('');
|
|
|
|
if (!f) {
|
|
setResumeFile(null);
|
|
return;
|
|
}
|
|
|
|
// Basic client-side validation
|
|
if (!ALLOWED_TYPES.includes(f.type)) {
|
|
setError('Please upload a PDF or DOCX file.');
|
|
setResumeFile(null);
|
|
return;
|
|
}
|
|
if (f.size > MAX_MB * 1024 * 1024) {
|
|
setError(`File is too large. Maximum ${MAX_MB}MB.`);
|
|
setResumeFile(null);
|
|
return;
|
|
}
|
|
|
|
setResumeFile(f);
|
|
};
|
|
|
|
const fetchRemainingOptimizations = async () => {
|
|
try {
|
|
const res = await api.get('/api/premium/resume/remaining', { withCredentials: true });
|
|
setRemainingOptimizations(res.data.remainingOptimizations);
|
|
setResetDate(res.data.resetDate ? new Date(res.data.resetDate).toLocaleDateString() : null);
|
|
} catch (err) {
|
|
console.error('Error fetching optimizations:', err);
|
|
setError('Could not fetch optimization limits.');
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchRemainingOptimizations();
|
|
}, []);
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setOptimizedResume('');
|
|
|
|
if (!resumeFile || !jobTitle.trim() || !jobDescription.trim()) {
|
|
setError('Please fill in all fields.');
|
|
return;
|
|
}
|
|
// If a previous error existed, reset the file input to prompt a fresh pick
|
|
if (error) {
|
|
// reset the native input if accessible
|
|
const el = e.target?.querySelector('input[type="file"]');
|
|
if (el) el.value = '';
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('resumeFile', resumeFile);
|
|
formData.append('jobTitle', jobTitle.trim());
|
|
formData.append('jobDescription', jobDescription.trim());
|
|
|
|
// Let axios/browser set multipart boundary automatically; just include credentials.
|
|
const res = await api.post('/api/premium/resume/optimize', formData, {
|
|
withCredentials: true,
|
|
});
|
|
|
|
setOptimizedResume(res.data.optimizedResume || '');
|
|
setError('');
|
|
fetchRemainingOptimizations();
|
|
} catch (err) {
|
|
console.error('Resume optimization error:', err);
|
|
const status = err?.response?.status;
|
|
const code = err?.response?.data?.error;
|
|
|
|
if (status === 413 || code === 'file_too_large') {
|
|
setError(`File is too large. Maximum ${MAX_MB}MB.`);
|
|
} else if (status === 415 || code === 'unsupported_type') {
|
|
setError('Unsupported file type. Please upload a PDF or DOCX.');
|
|
} else if (status === 429) {
|
|
setError('Too many requests. Please wait a moment and try again.');
|
|
} else if (status === 400) {
|
|
setError('Bad upload. Please re-select your file and try again.');
|
|
} else {
|
|
setError('Failed to optimize resume. Please try again.');
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto mt-8 p-6 bg-white rounded shadow">
|
|
<h2 className="text-2xl font-bold mb-4">Resume Optimizer</h2>
|
|
|
|
{remainingOptimizations !== null && (
|
|
<div className="mb-4 p-2 bg-blue-50 rounded border border-blue-200 text-blue-700">
|
|
<strong>{remainingOptimizations}</strong> Resume Optimizations Remaining This Week
|
|
{resetDate && <span className="ml-2">(Resets on {resetDate})</span>}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div>
|
|
<label className="block font-medium text-gray-700 mb-1">
|
|
Upload Resume (PDF or DOCX):
|
|
</label>
|
|
<input
|
|
type="file"
|
|
accept=".pdf,.docx"
|
|
onChange={handleFileChange}
|
|
className="file:mr-4 file:py-2 file:px-4 file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer text-gray-600"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium text-gray-700 mb-1">Job Title:</label>
|
|
<input
|
|
type="text"
|
|
value={jobTitle}
|
|
onChange={(e) => {
|
|
setJobTitle(e.target.value);
|
|
if (error) setError('');
|
|
}}
|
|
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
|
|
placeholder="e.g., Software Engineer"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium text-gray-700 mb-1">Job Description:</label>
|
|
<textarea
|
|
value={jobDescription}
|
|
onChange={(e) => {
|
|
setJobDescription(e.target.value);
|
|
if (error) setError('');
|
|
}}
|
|
rows={4}
|
|
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
|
|
placeholder="Paste the job listing or requirements here..."
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className="text-red-600 font-semibold">{error}</p>}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className={`inline-block font-semibold px-5 py-2 rounded transition-colors ${
|
|
loading ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'
|
|
} text-white`}
|
|
>
|
|
{loading ? 'Optimizing Resume...' : 'Optimize Resume'}
|
|
</button>
|
|
|
|
{loading && (
|
|
<div className="flex items-center justify-center mt-4">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
|
<span className="ml-3 text-blue-700 font-semibold">Optimizing your resume...</span>
|
|
</div>
|
|
)}
|
|
</form>
|
|
|
|
{optimizedResume && (
|
|
<div className="mt-8">
|
|
<h3 className="text-xl font-bold mb-2">Optimized Resume</h3>
|
|
<pre className="whitespace-pre-wrap bg-gray-50 p-4 rounded border">
|
|
{optimizedResume}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ResumeRewrite;
|