Spaces:
Paused
Paused
Download frontend/src/components/sections/CVUpload.tsx from SkillSync-II/skillsync-cli: direct link, hf CLI and curl.
- Browser
- Download file 4.97 kB
-
https://huggingface.co/spaces/SkillSync-II/skillsync-cli/resolve/main/frontend/src/components/sections/CVUpload.tsx
- Command line
-
hf download hf://spaces/SkillSync-II/skillsync-cli/frontend/src/components/sections/CVUpload.tsx
-
curl -L -o CVUpload.tsx https://huggingface.co/spaces/SkillSync-II/skillsync-cli/resolve/main/frontend/src/components/sections/CVUpload.tsx
4.97 kB
| 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; | |