Spaces:
Sleeping
Sleeping
| import React, { useState, useRef } from 'react'; | |
| import { motion } from 'framer-motion'; | |
| import { DownloadIcon, ArrowLeftIcon, PlusIcon, TrashIcon } from './Icons'; | |
| export default function ResumeBuilder({ analysisResult, onBack }) { | |
| const printRef = useRef(null); | |
| const missingKeywords = analysisResult?.matches?.filter(m => !m.found) || []; | |
| const resumeData = analysisResult?.resume_data || {}; | |
| // Form State - Auto-fill from resume_data if available | |
| const [personalInfo, setPersonalInfo] = useState({ | |
| fullName: resumeData.name || '', | |
| email: resumeData.email || '', | |
| phone: resumeData.phone || '', | |
| location: resumeData.location || '', | |
| linkedin: resumeData.linkedin || '', | |
| portfolio: resumeData.portfolio || '' | |
| }); | |
| const [summary, setSummary] = useState(resumeData.summary || ''); | |
| const [experience, setExperience] = useState( | |
| resumeData.work_experience?.length > 0 | |
| ? resumeData.work_experience.map((exp, idx) => ({ | |
| id: Date.now() + idx, | |
| company: exp.company || '', | |
| role: exp.role || '', | |
| startDate: exp.years || exp.start_date || '', | |
| endDate: exp.end_date || '', | |
| description: Array.isArray(exp.responsibilities) ? exp.responsibilities.map(r => `β’ ${r}`).join('\n') : (exp.responsibilities || exp.description || '') | |
| })) | |
| : [{ id: 1, company: '', role: '', startDate: '', endDate: '', description: '' }] | |
| ); | |
| const [education, setEducation] = useState( | |
| resumeData.education?.length > 0 | |
| ? resumeData.education.map((edu, idx) => ({ | |
| id: Date.now() + idx, | |
| institution: edu.institution || '', | |
| degree: edu.course || edu.degree || '', | |
| startDate: edu.year || edu.start_date || '', | |
| endDate: edu.end_date || '', | |
| location: edu.location || '' | |
| })) | |
| : [{ id: 1, institution: '', degree: '', startDate: '', endDate: '' }] | |
| ); | |
| const [projects, setProjects] = useState( | |
| resumeData.projects?.length > 0 | |
| ? resumeData.projects.map((proj, idx) => ({ | |
| id: Date.now() + idx, | |
| name: proj.title || proj.name || '', | |
| description: proj.description || '', | |
| link: proj.link || '' | |
| })) | |
| : [{ id: 1, name: '', description: '', link: '' }] | |
| ); | |
| const [certifications, setCertifications] = useState( | |
| resumeData.certifications?.length > 0 | |
| ? resumeData.certifications.map((cert, idx) => { | |
| if (typeof cert === 'string') { | |
| const parts = cert.split(/ - | β | β |: /); | |
| if (parts.length > 1) { | |
| return { id: Date.now() + idx, issuer: parts[0].trim(), name: parts.slice(1).join(' - ').trim(), date: '' }; | |
| } | |
| return { id: Date.now() + idx, name: cert, issuer: '', date: '' }; | |
| } | |
| return { | |
| id: Date.now() + idx, | |
| name: cert.name || cert.title || '', | |
| issuer: cert.issuer || '', | |
| date: cert.date || '' | |
| }; | |
| }) | |
| : [{ id: 1, name: '', issuer: '', date: '' }] | |
| ); | |
| const initialSkills = []; | |
| if (resumeData.technical_skills) initialSkills.push(...resumeData.technical_skills); | |
| if (resumeData.skills) initialSkills.push(...resumeData.skills); | |
| const [skills, setSkills] = useState(initialSkills.join(', ')); | |
| const [themeColor, setThemeColor] = useState('#000000'); | |
| // Drag and Drop Handlers | |
| const handleDragStart = (e, keyword) => { | |
| e.dataTransfer.setData("text/plain", keyword); | |
| }; | |
| const handleDrop = (e, setter, currentValue) => { | |
| e.preventDefault(); | |
| const draggedText = e.dataTransfer.getData("text/plain"); | |
| if (draggedText) { | |
| // Append the dropped text | |
| // Determine if we need a comma or space based on context (hacky but functional for this scope) | |
| const separator = currentValue ? (setter === setSkills ? ', ' : ' ') : ''; | |
| setter(`${currentValue}${separator}${draggedText}`); | |
| } | |
| }; | |
| const handleExperienceDrop = (e, id, currentValue) => { | |
| e.preventDefault(); | |
| const draggedText = e.dataTransfer.getData("text/plain"); | |
| if (draggedText) { | |
| const separator = currentValue ? ' ' : ''; | |
| updateExperience(id, 'description', `${currentValue}${separator}${draggedText}`); | |
| } | |
| }; | |
| const handleProjectDrop = (e, id, currentValue) => { | |
| e.preventDefault(); | |
| const draggedText = e.dataTransfer.getData("text/plain"); | |
| if (draggedText) { | |
| const separator = currentValue ? ' ' : ''; | |
| updateProject(id, 'description', `${currentValue}${separator}${draggedText}`); | |
| } | |
| }; | |
| const handleDragOver = (e) => { | |
| e.preventDefault(); // Necessary to allow dropping | |
| }; | |
| // Handlers | |
| const handlePersonalInfoChange = (e) => { | |
| const { name, value } = e.target; | |
| setPersonalInfo(prev => ({ ...prev, [name]: value })); | |
| }; | |
| const addExperience = () => { | |
| setExperience(prev => [...prev, { id: Date.now(), company: '', role: '', startDate: '', endDate: '', description: '' }]); | |
| }; | |
| const updateExperience = (id, field, value) => { | |
| setExperience(prev => prev.map(exp => exp.id === id ? { ...exp, [field]: value } : exp)); | |
| }; | |
| const removeExperience = (id) => { | |
| setExperience(prev => prev.filter(exp => exp.id !== id)); | |
| }; | |
| const addEducation = () => { | |
| setEducation(prev => [...prev, { id: Date.now(), institution: '', degree: '', startDate: '', endDate: '' }]); | |
| }; | |
| const updateEducation = (id, field, value) => { | |
| setEducation(prev => prev.map(edu => edu.id === id ? { ...edu, [field]: value } : edu)); | |
| }; | |
| const removeEducation = (id) => { | |
| setEducation(prev => prev.filter(edu => edu.id !== id)); | |
| }; | |
| const addProject = () => setProjects(prev => [...prev, { id: Date.now(), name: '', description: '', link: '' }]); | |
| const updateProject = (id, field, value) => setProjects(prev => prev.map(proj => proj.id === id ? { ...proj, [field]: value } : proj)); | |
| const removeProject = (id) => setProjects(prev => prev.filter(proj => proj.id !== id)); | |
| const addCertification = () => setCertifications(prev => [...prev, { id: Date.now(), name: '', issuer: '', date: '' }]); | |
| const updateCertification = (id, field, value) => setCertifications(prev => prev.map(cert => cert.id === id ? { ...cert, [field]: value } : cert)); | |
| const removeCertification = (id) => setCertifications(prev => prev.filter(cert => cert.id !== id)); | |
| const handlePrint = () => { | |
| window.print(); | |
| }; | |
| const inputStyle = { | |
| width: '100%', | |
| padding: '0.75rem', | |
| borderRadius: '0.5rem', | |
| border: '1px solid rgba(236, 183, 26, 0.46)', | |
| backgroundColor: 'rgba(255,255,255,0.05)', | |
| color: 'white', | |
| marginBottom: '1rem', | |
| fontFamily: 'inherit', | |
| }; | |
| const labelStyle = { | |
| display: 'block', | |
| marginBottom: '0.5rem', | |
| color: '#d1d5db', | |
| fontSize: '0.875rem' | |
| }; | |
| const sectionHeaderStyle = { | |
| fontSize: '1.25rem', | |
| fontWeight: 'bold', | |
| color: '#FBBF24', | |
| marginBottom: '1rem', | |
| borderBottom: '1px solid rgba(236, 183, 26, 0.3)', | |
| paddingBottom: '0.5rem' | |
| }; | |
| const resumeSectionHeaderStyle = { | |
| fontSize: '14px', | |
| fontVariant: 'small-caps', | |
| fontWeight: 'bold', | |
| textTransform: 'uppercase', | |
| borderBottom: `1px solid ${themeColor}`, | |
| color: themeColor, | |
| paddingBottom: '2px', | |
| marginBottom: '8px', | |
| letterSpacing: '1px' | |
| }; | |
| return ( | |
| <div style={{ display: 'grid', gridTemplateColumns: 'minmax(300px, 1fr) 1.5fr', gap: '2rem', height: 'calc(100vh - 150px)' }}> | |
| {/* Left Panel: Form & Gaps */} | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', overflowY: 'auto', paddingRight: '1rem' }} className="no-print hide-scrollbar"> | |
| {/* Header Actions */} | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <button | |
| onClick={onBack} | |
| style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'none', border: 'none', color: '#d1d5db', cursor: 'pointer', padding: '0.5rem 0' }} | |
| > | |
| <ArrowLeftIcon /> Back to Analysis | |
| </button> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', backgroundColor: 'rgba(255,255,255,0.05)', padding: '0.25rem 0.5rem', borderRadius: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}> | |
| <label style={{ fontSize: '0.75rem', color: '#d1d5db' }}>Theme Color</label> | |
| <div style={{ position: 'relative', width: '24px', height: '24px', borderRadius: '4px', backgroundColor: themeColor, border: '1px solid rgba(255,255,255,0.2)', overflow: 'hidden' }}> | |
| <input | |
| type="color" | |
| value={themeColor} | |
| onChange={(e) => setThemeColor(e.target.value)} | |
| style={{ position: 'absolute', top: '-10px', left: '-10px', width: '50px', height: '50px', cursor: 'pointer', opacity: 0 }} | |
| title="Change Resume Theme Color" | |
| /> | |
| </div> | |
| </div> | |
| <button | |
| onClick={handlePrint} | |
| style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', backgroundColor: '#FBBF24', color: 'black', border: 'none', padding: '0.5rem 1rem', borderRadius: '0.5rem', fontWeight: 'bold', cursor: 'pointer' }} | |
| > | |
| <DownloadIcon /> Export PDF | |
| </button> | |
| </div> | |
| </div> | |
| {/* Fix Your Gaps Panel */} | |
| {missingKeywords.length > 0 && ( | |
| <div style={{ | |
| backgroundColor: 'rgba(239, 68, 68, 0.1)', | |
| border: '1px solid rgba(239, 68, 68, 0.3)', | |
| borderRadius: '1rem', | |
| padding: '1.5rem', | |
| position: 'sticky', | |
| top: 0, | |
| zIndex: 10, | |
| backdropFilter: 'blur(8px)', // Optional: makes it look better when scrolling over text | |
| boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.5)' | |
| }}> | |
| <h3 style={{ fontSize: '1.125rem', fontWeight: 'bold', color: '#EF4444', marginBottom: '1rem' }}>Fix Your ATS Gaps</h3> | |
| <p style={{ color: '#d1d5db', fontSize: '0.875rem', marginBottom: '1rem' }}>Drag and drop these keywords into your text fields below.</p> | |
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}> | |
| {missingKeywords.map((match, idx) => ( | |
| <span | |
| key={idx} | |
| draggable | |
| onDragStart={(e) => handleDragStart(e, match.keyword)} | |
| style={{ | |
| backgroundColor: 'rgba(239, 68, 68, 0.2)', | |
| color: '#FCA5A5', | |
| padding: '0.25rem 0.5rem', | |
| borderRadius: '0.25rem', | |
| fontSize: '0.75rem', | |
| border: '1px solid rgba(239, 68, 68, 0.4)', | |
| cursor: 'grab' | |
| }} | |
| > | |
| {match.keyword} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Builder Form */} | |
| <div style={{ backgroundColor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '1rem', padding: '1.5rem' }}> | |
| {/* Personal Info */} | |
| <div style={{ marginBottom: '2rem' }}> | |
| <h3 style={sectionHeaderStyle}>Personal Information</h3> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div><label style={labelStyle}>Full Name</label><input style={inputStyle} name="fullName" value={personalInfo.fullName} onChange={handlePersonalInfoChange} placeholder="John Doe" /></div> | |
| <div><label style={labelStyle}>Email</label><input style={inputStyle} name="email" value={personalInfo.email} onChange={handlePersonalInfoChange} placeholder="john@example.com" /></div> | |
| <div><label style={labelStyle}>Phone</label><input style={inputStyle} name="phone" value={personalInfo.phone} onChange={handlePersonalInfoChange} placeholder="(555) 123-4567" /></div> | |
| <div><label style={labelStyle}>Location</label><input style={inputStyle} name="location" value={personalInfo.location} onChange={handlePersonalInfoChange} placeholder="City, State" /></div> | |
| <div><label style={labelStyle}>LinkedIn (Optional)</label><input style={inputStyle} name="linkedin" value={personalInfo.linkedin} onChange={handlePersonalInfoChange} placeholder="linkedin.com/in/johndoe" /></div> | |
| <div><label style={labelStyle}>Portfolio (Optional)</label><input style={inputStyle} name="portfolio" value={personalInfo.portfolio} onChange={handlePersonalInfoChange} placeholder="johndoe.com" /></div> | |
| </div> | |
| </div> | |
| {/* Summary */} | |
| <div style={{ marginBottom: '2rem' }}> | |
| <h3 style={sectionHeaderStyle}>Professional Summary</h3> | |
| <textarea | |
| style={{ ...inputStyle, minHeight: '100px', resize: 'vertical' }} | |
| value={summary} | |
| onChange={e => setSummary(e.target.value)} | |
| onDrop={(e) => handleDrop(e, setSummary, summary)} | |
| onDragOver={handleDragOver} | |
| placeholder="Brief overview of your professional background and goals..." | |
| /> | |
| </div> | |
| {/* Experience */} | |
| <div style={{ marginBottom: '2rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> | |
| <h3 style={{ ...sectionHeaderStyle, borderBottom: 'none', marginBottom: 0 }}>Experience</h3> | |
| <button onClick={addExperience} style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', background: 'none', border: '1px solid #FBBF24', color: '#FBBF24', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', cursor: 'pointer', fontSize: '0.75rem' }}> | |
| <PlusIcon size={14} /> Add Role | |
| </button> | |
| </div> | |
| {experience.map((exp, index) => ( | |
| <div key={exp.id} style={{ backgroundColor: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '0.5rem', marginBottom: '1rem', position: 'relative' }}> | |
| {index > 0 && ( | |
| <button onClick={() => removeExperience(exp.id)} style={{ position: 'absolute', top: '1rem', right: '1rem', background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer' }} title="Remove Role"> | |
| <TrashIcon size={16} /> | |
| </button> | |
| )} | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div><label style={labelStyle}>Company</label><input style={inputStyle} value={exp.company} onChange={e => updateExperience(exp.id, 'company', e.target.value)} placeholder="Acme Corp" /></div> | |
| <div><label style={labelStyle}>Job Title</label><input style={inputStyle} value={exp.role} onChange={e => updateExperience(exp.id, 'role', e.target.value)} placeholder="Software Engineer" /></div> | |
| <div><label style={labelStyle}>Start Date</label><input style={inputStyle} value={exp.startDate} onChange={e => updateExperience(exp.id, 'startDate', e.target.value)} placeholder="MMM YYYY" /></div> | |
| <div><label style={labelStyle}>End Date</label><input style={inputStyle} value={exp.endDate} onChange={e => updateExperience(exp.id, 'endDate', e.target.value)} placeholder="MMM YYYY or Present" /></div> | |
| </div> | |
| <label style={labelStyle}>Description (Bullet points recommended)</label> | |
| <textarea | |
| style={{ ...inputStyle, minHeight: '80px', resize: 'vertical', marginBottom: 0 }} | |
| value={exp.description} | |
| onChange={e => updateExperience(exp.id, 'description', e.target.value)} | |
| onDrop={(e) => handleExperienceDrop(e, exp.id, exp.description)} | |
| onDragOver={handleDragOver} | |
| placeholder="β’ Achieved X by doing Y..." | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Education */} | |
| <div style={{ marginBottom: '2rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> | |
| <h3 style={{ ...sectionHeaderStyle, borderBottom: 'none', marginBottom: 0 }}>Education</h3> | |
| <button onClick={addEducation} style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', background: 'none', border: '1px solid #FBBF24', color: '#FBBF24', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', cursor: 'pointer', fontSize: '0.75rem' }}> | |
| <PlusIcon size={14} /> Add Degree | |
| </button> | |
| </div> | |
| {education.map((edu, index) => ( | |
| <div key={edu.id} style={{ backgroundColor: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '0.5rem', marginBottom: '1rem', position: 'relative' }}> | |
| {index > 0 && ( | |
| <button onClick={() => removeEducation(edu.id)} style={{ position: 'absolute', top: '1rem', right: '1rem', background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer' }} title="Remove Degree"> | |
| <TrashIcon size={16} /> | |
| </button> | |
| )} | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div><label style={labelStyle}>Institution</label><input style={{ ...inputStyle, marginBottom: 0 }} value={edu.institution} onChange={e => updateEducation(edu.id, 'institution', e.target.value)} placeholder="University of State" /></div> | |
| <div><label style={labelStyle}>Degree / Major</label><input style={{ ...inputStyle, marginBottom: 0 }} value={edu.degree} onChange={e => updateEducation(edu.id, 'degree', e.target.value)} placeholder="B.S. Computer Science" /></div> | |
| <div><label style={labelStyle}>Start Date</label><input style={{ ...inputStyle, marginBottom: 0 }} value={edu.startDate} onChange={e => updateEducation(edu.id, 'startDate', e.target.value)} placeholder="YYYY" /></div> | |
| <div><label style={labelStyle}>End Date</label><input style={{ ...inputStyle, marginBottom: 0 }} value={edu.endDate} onChange={e => updateEducation(edu.id, 'endDate', e.target.value)} placeholder="YYYY" /></div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Projects */} | |
| <div style={{ marginBottom: '2rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> | |
| <h3 style={{ ...sectionHeaderStyle, borderBottom: 'none', marginBottom: 0 }}>Projects</h3> | |
| <button onClick={addProject} style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', background: 'none', border: '1px solid #FBBF24', color: '#FBBF24', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', cursor: 'pointer', fontSize: '0.75rem' }}> | |
| <PlusIcon size={14} /> Add Project | |
| </button> | |
| </div> | |
| {projects.map((proj, index) => ( | |
| <div key={proj.id} style={{ backgroundColor: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '0.5rem', marginBottom: '1rem', position: 'relative' }}> | |
| {index > 0 && ( | |
| <button onClick={() => removeProject(proj.id)} style={{ position: 'absolute', top: '1rem', right: '1rem', background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer' }} title="Remove Project"> | |
| <TrashIcon size={16} /> | |
| </button> | |
| )} | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem' }}> | |
| <div><label style={labelStyle}>Project Name</label><input style={{ ...inputStyle, marginBottom: 0 }} value={proj.name} onChange={e => updateProject(proj.id, 'name', e.target.value)} placeholder="E-commerce App" /></div> | |
| <div><label style={labelStyle}>Link (Optional)</label><input style={{ ...inputStyle, marginBottom: 0 }} value={proj.link} onChange={e => updateProject(proj.id, 'link', e.target.value)} placeholder="github.com/..." /></div> | |
| </div> | |
| <label style={labelStyle}>Description (Bullet points recommended)</label> | |
| <textarea | |
| style={{ ...inputStyle, minHeight: '60px', resize: 'vertical', marginBottom: 0 }} | |
| value={proj.description} | |
| onChange={e => updateProject(proj.id, 'description', e.target.value)} | |
| onDrop={(e) => handleProjectDrop(e, proj.id, proj.description)} | |
| onDragOver={handleDragOver} | |
| placeholder="β’ Developed using React..." | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Certifications */} | |
| <div style={{ marginBottom: '2rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> | |
| <h3 style={{ ...sectionHeaderStyle, borderBottom: 'none', marginBottom: 0 }}>Certifications</h3> | |
| <button onClick={addCertification} style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', background: 'none', border: '1px solid #FBBF24', color: '#FBBF24', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', cursor: 'pointer', fontSize: '0.75rem' }}> | |
| <PlusIcon size={14} /> Add Certification | |
| </button> | |
| </div> | |
| {certifications.map((cert, index) => ( | |
| <div key={cert.id} style={{ backgroundColor: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '0.5rem', marginBottom: '1rem', position: 'relative' }}> | |
| {index > 0 && ( | |
| <button onClick={() => removeCertification(cert.id)} style={{ position: 'absolute', top: '1rem', right: '1rem', background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer' }} title="Remove Certification"> | |
| <TrashIcon size={16} /> | |
| </button> | |
| )} | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem' }}> | |
| <div><label style={labelStyle}>Name</label><input style={{ ...inputStyle, marginBottom: 0 }} value={cert.name} onChange={e => updateCertification(cert.id, 'name', e.target.value)} placeholder="AWS Cloud Practitioner" /></div> | |
| <div><label style={labelStyle}>Issuer</label><input style={{ ...inputStyle, marginBottom: 0 }} value={cert.issuer} onChange={e => updateCertification(cert.id, 'issuer', e.target.value)} placeholder="Amazon Web Services" /></div> | |
| <div><label style={labelStyle}>Date / Year</label><input style={{ ...inputStyle, marginBottom: 0 }} value={cert.date} onChange={e => updateCertification(cert.id, 'date', e.target.value)} placeholder="YYYY" /></div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Skills */} | |
| <div> | |
| <h3 style={sectionHeaderStyle}>Skills</h3> | |
| <label style={labelStyle}>Comma separated list of skills</label> | |
| <textarea | |
| style={{ ...inputStyle, minHeight: '80px', resize: 'vertical' }} | |
| value={skills} | |
| onChange={e => setSkills(e.target.value)} | |
| onDrop={(e) => handleDrop(e, setSkills, skills)} | |
| onDragOver={handleDragOver} | |
| placeholder="React, Python, SQL, Project Management..." | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Right Panel: Live Preview (Printable Area) */} | |
| <div | |
| style={{ | |
| backgroundColor: '#52525B', // Darker backdrop for contrast | |
| padding: '2rem', | |
| overflowY: 'auto', | |
| borderRadius: '1rem', | |
| display: 'flex', | |
| justifyContent: 'center' | |
| }} | |
| className="hide-scrollbar" // Wrapper shouldn't have no-print so children can print | |
| > | |
| {/* Actual Paper Element */} | |
| <div | |
| ref={printRef} | |
| className="resume-print-area" | |
| style={{ | |
| backgroundColor: 'white', | |
| color: 'black', | |
| width: '100%', | |
| maxWidth: '850px', | |
| minHeight: '1100px', // ~8.5x11 aspect ratio | |
| padding: '40px 50px', | |
| boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5)', | |
| fontFamily: "'Times New Roman', Times, serif", | |
| lineHeight: '1.4', | |
| boxSizing: 'border-box' | |
| }} | |
| > | |
| {/* Header */} | |
| <div style={{ textAlign: 'center', marginBottom: '20px' }}> | |
| <h1 style={{ fontSize: '36px', fontWeight: 'normal', margin: '0 0 10px 0', letterSpacing: '1px', color: themeColor }}> | |
| {personalInfo.fullName || 'Your Name'} | |
| </h1> | |
| <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '16px', fontSize: '13px' }}> | |
| {personalInfo.phone && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M6.62 10.79a15.053 15.053 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.11-.27c1.12.37 2.33.57 3.58.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A19 19 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1.11l-2.2 2.2z"></path></svg> | |
| {personalInfo.phone} | |
| </div> | |
| )} | |
| {personalInfo.email && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"></path></svg> | |
| {personalInfo.email} | |
| </div> | |
| )} | |
| {personalInfo.linkedin && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path></svg> | |
| {personalInfo.linkedin} | |
| </div> | |
| )} | |
| {personalInfo.portfolio && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path></svg> | |
| {personalInfo.portfolio} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Summary */} | |
| {summary && ( | |
| <div style={{ marginBottom: '16px' }}> | |
| <h2 style={resumeSectionHeaderStyle}> | |
| Summary | |
| </h2> | |
| <p style={{ fontSize: '13px', margin: 0, textAlign: 'justify' }}> | |
| {summary} | |
| </p> | |
| </div> | |
| )} | |
| {/* Education */} | |
| {education.some(edu => edu.institution || edu.degree) && ( | |
| <div style={{ marginBottom: '16px' }}> | |
| <h2 style={resumeSectionHeaderStyle}> | |
| Education | |
| </h2> | |
| {education.map(edu => ( | |
| (edu.institution || edu.degree) ? ( | |
| <div key={edu.id} style={{ marginBottom: '8px' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}> | |
| <span style={{ fontWeight: 'bold' }}>{edu.institution}</span> | |
| <span>{edu.location || ''}</span> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', fontStyle: 'italic' }}> | |
| <span>{edu.degree}</span> | |
| <span>{edu.startDate} {edu.startDate && edu.endDate && 'β'} {edu.endDate}</span> | |
| </div> | |
| </div> | |
| ) : null | |
| ))} | |
| </div> | |
| )} | |
| {/* Technical Skills */} | |
| {skills && ( | |
| <div style={{ marginBottom: '16px' }}> | |
| <h2 style={resumeSectionHeaderStyle}> | |
| Technical Skills | |
| </h2> | |
| <p style={{ fontSize: '13px', margin: 0 }}> | |
| {skills} | |
| </p> | |
| </div> | |
| )} | |
| {/* Professional Experience */} | |
| {experience.some(exp => exp.company || exp.role) && ( | |
| <div style={{ marginBottom: '16px' }}> | |
| <h2 style={resumeSectionHeaderStyle}> | |
| Experience | |
| </h2> | |
| {experience.map(exp => ( | |
| (exp.company || exp.role) ? ( | |
| <div key={exp.id} style={{ marginBottom: '12px' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}> | |
| <span style={{ fontWeight: 'bold' }}>{exp.role}</span> | |
| <span style={{ fontWeight: 'bold' }}>{exp.startDate} {exp.startDate && exp.endDate && 'β'} {exp.endDate}</span> | |
| </div> | |
| <div style={{ fontSize: '13px', fontStyle: 'italic', marginBottom: '4px' }}> | |
| {exp.company} | |
| </div> | |
| {exp.description && ( | |
| <div style={{ fontSize: '13px' }}> | |
| {exp.description.split('\n').map((line, i) => { | |
| const trimmedLine = line.trim(); | |
| if (!trimmedLine) return null; | |
| return ( | |
| <div style={{ display: 'flex', marginBottom: '2px', paddingLeft: '16px' }} key={i}> | |
| <span style={{ marginRight: '8px' }}>β</span> | |
| <span>{trimmedLine.replace(/^[β’-]\s*/, '')}</span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ) : null | |
| ))} | |
| </div> | |
| )} | |
| {/* Projects */} | |
| {projects.some(proj => proj.name || proj.description) && ( | |
| <div style={{ marginBottom: '16px' }}> | |
| <h2 style={resumeSectionHeaderStyle}> | |
| Projects | |
| </h2> | |
| {projects.map(proj => ( | |
| (proj.name || proj.description) ? ( | |
| <div key={proj.id} style={{ marginBottom: '10px' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', marginBottom: '4px' }}> | |
| <span><strong style={{ fontWeight: 'bold' }}>{proj.name}</strong>{proj.link ? ` | ${proj.link}` : ''}</span> | |
| </div> | |
| {proj.description && ( | |
| <div style={{ fontSize: '13px' }}> | |
| {proj.description.split('\n').map((line, i) => { | |
| const trimmedLine = line.trim(); | |
| if (!trimmedLine) return null; | |
| return ( | |
| <div style={{ display: 'flex', marginBottom: '2px', paddingLeft: '16px' }} key={i}> | |
| <span style={{ marginRight: '8px' }}>β</span> | |
| <span>{trimmedLine.replace(/^[β’-]\s*/, '')}</span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ) : null | |
| ))} | |
| </div> | |
| )} | |
| {/* Certifications */} | |
| {certifications.some(cert => cert.name || cert.issuer) && ( | |
| <div style={{ marginBottom: '16px' }}> | |
| <h2 style={resumeSectionHeaderStyle}> | |
| Certifications | |
| </h2> | |
| {certifications.map(cert => ( | |
| (cert.name || cert.issuer) ? ( | |
| <div key={cert.id} style={{ marginBottom: '4px', fontSize: '13px' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between' }}> | |
| <span> | |
| {cert.issuer && <strong style={{ fontWeight: 'bold' }}>{cert.issuer}</strong>} | |
| {cert.issuer && cert.name && ' β '} | |
| {cert.name && <span>{cert.name}</span>} | |
| </span> | |
| <span>{cert.date}</span> | |
| </div> | |
| </div> | |
| ) : null | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |