File size: 4,972 Bytes
88da18c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | import React from 'react';
import { motion } from 'framer-motion';
import { Upload, FileText, AlertCircle } from 'lucide-react';
import { Card, Button } from '@/components/common';
import { useJobStore } from '@/store/appStore';
import { jobsAPI } from '@/utils/api';
import { readFileAsArrayBuffer } from '@/utils/helpers';
interface CVUploadProps {
onSuccess?: (filename: string) => void;
onError?: (error: string) => void;
}
const CVUpload: React.FC<CVUploadProps> = ({ onSuccess, onError }) => {
const [isDragging, setIsDragging] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const { cvTemplate, setCVTemplate } = useJobStore();
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleFileSelect = async (files: FileList | null) => {
if (!files || files.length === 0) return;
const file = files[0];
setError(null);
// Validate file type
if (!file.name.endsWith('.docx')) {
setError('Only .docx files are supported');
onError?.('Only .docx files are supported');
return;
}
// Validate file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
setError('File size must be less than 10MB');
onError?.('File size must be less than 10MB');
return;
}
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const response = await jobsAPI.uploadCV(file);
if (response.success) {
setCVTemplate({
filename: file.name,
uploadedAt: new Date().toISOString(),
size: file.size,
content: arrayBuffer,
});
onSuccess?.(file.name);
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to upload CV';
setError(message);
onError?.(message);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
handleFileSelect(e.dataTransfer.files);
};
return (
<div className="w-full">
{cvTemplate ? (
<Card glow innerGlow>
<div className="flex items-start gap-4">
<div className="p-3 bg-emerald-500/20 rounded-lg">
<FileText className="w-6 h-6 text-emerald-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-white mb-1">
{cvTemplate.filename}
</h3>
<p className="text-sm text-slate-400 mb-3">
Uploaded on{' '}
{new Date(cvTemplate.uploadedAt).toLocaleDateString()}
</p>
<Button
size="sm"
variant="outline"
onClick={() => {
setCVTemplate(null);
setError(null);
}}
>
Change CV
</Button>
</div>
</div>
</Card>
) : (
<motion.div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
animate={isDragging ? { scale: 1.02 } : { scale: 1 }}
className={`border-2 border-dashed rounded-xl p-8 text-center transition-all cursor-pointer ${
isDragging
? 'border-emerald-500 bg-emerald-500/10'
: 'border-slate-600 bg-slate-800/30 hover:border-emerald-400'
}`}
onClick={() => fileInputRef.current?.click()}
>
<motion.div
animate={isDragging ? { scale: 1.1, rotate: 12 } : { scale: 1, rotate: 0 }}
className="flex justify-center mb-4"
>
<div className="p-3 bg-emerald-500/20 rounded-lg">
<Upload className="w-8 h-8 text-emerald-400" />
</div>
</motion.div>
<h3 className="text-lg font-semibold text-white mb-2">
Drop your CV or click to browse
</h3>
<p className="text-slate-400 text-sm mb-4">
Supported format: .docx (Word Document)
</p>
<p className="text-xs text-slate-500">Max file size: 10MB</p>
<input
ref={fileInputRef}
type="file"
accept=".docx"
onChange={(e) => handleFileSelect(e.target.files)}
className="hidden"
/>
</motion.div>
)}
{error && (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-red-500/10 border border-red-500/30 rounded-lg flex gap-3"
>
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-200">{error}</p>
</motion.div>
)}
</div>
);
};
export default CVUpload;
|