sameer2026 commited on
Commit
9d6cc86
·
1 Parent(s): c88824f

fix: ats resume builder UI and backend extraction handling

Browse files

- Added theme color picker to ResumeBuilder
- Formatted certifications to properly display issuer and name
- Fixed Chrome autofill background bug on login pages
- Removed headers/footers in PDF export via print CSS
- Made ats_service skill extraction robust to non-list JSON values
- Improved ATS Checker error reporting for unreadable image-based PDFs

backend/src/ingestion/pdf_reader.py CHANGED
@@ -35,4 +35,4 @@ def parse_pdf(path: str) -> str:
35
  except Exception as e:
36
  print(f"❌ pypdf also failed for {path}: {e}")
37
 
38
- raise ValueError(f"Unable to extract text from PDF: {path}")
 
35
  except Exception as e:
36
  print(f"❌ pypdf also failed for {path}: {e}")
37
 
38
+ raise ValueError("We couldn't extract any readable text from this PDF. If you generated this using 'Microsoft Print to PDF', please try using Chrome/Edge's native 'Save as PDF' option instead, as Microsoft Print to PDF often converts text into unreadable images.")
backend/src/services/ats_service.py CHANGED
@@ -58,17 +58,24 @@ def calculate_ats_score(resume_data: dict, job_data: dict) -> dict:
58
  # --- 1. Skill Matching (Weight: 60%) ---
59
  # Merge all job skills into a set for easier lookup
60
  job_skills = set()
61
- if job_data.get("technical_skills"):
62
- job_skills.update([s.lower() for s in job_data["technical_skills"]])
63
- if job_data.get("skills"):
64
- job_skills.update([s.lower() for s in job_data["skills"]])
 
 
 
 
 
 
 
 
 
65
 
66
  # Merge all resume skills
67
  resume_skills = set()
68
- if resume_data.get("technical_skills"):
69
- resume_skills.update([s.lower() for s in resume_data["technical_skills"]])
70
- if resume_data.get("skills"):
71
- resume_skills.update([s.lower() for s in resume_data["skills"]])
72
 
73
  # Calculate overlaps
74
  found_skills = job_skills.intersection(resume_skills)
 
58
  # --- 1. Skill Matching (Weight: 60%) ---
59
  # Merge all job skills into a set for easier lookup
60
  job_skills = set()
61
+
62
+ def safe_add_skills(target_set, data_dict, key):
63
+ if not data_dict: return
64
+ items = data_dict.get(key)
65
+ if items:
66
+ if isinstance(items, list):
67
+ for s in items:
68
+ if isinstance(s, str): target_set.add(s.lower())
69
+ elif isinstance(items, str):
70
+ target_set.add(items.lower())
71
+
72
+ safe_add_skills(job_skills, job_data, "technical_skills")
73
+ safe_add_skills(job_skills, job_data, "skills")
74
 
75
  # Merge all resume skills
76
  resume_skills = set()
77
+ safe_add_skills(resume_skills, resume_data, "technical_skills")
78
+ safe_add_skills(resume_skills, resume_data, "skills")
 
 
79
 
80
  # Calculate overlaps
81
  found_skills = job_skills.intersection(resume_skills)
src/components/ResumeBuilder.jsx CHANGED
@@ -26,9 +26,9 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
26
  id: Date.now() + idx,
27
  company: exp.company || '',
28
  role: exp.role || '',
29
- startDate: exp.start_date || '',
30
  endDate: exp.end_date || '',
31
- description: Array.isArray(exp.responsibilities) ? exp.responsibilities.map(r => `• ${r}`).join('\n') : (exp.responsibilities || '')
32
  }))
33
  : [{ id: 1, company: '', role: '', startDate: '', endDate: '', description: '' }]
34
  );
@@ -38,18 +38,51 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
38
  ? resumeData.education.map((edu, idx) => ({
39
  id: Date.now() + idx,
40
  institution: edu.institution || '',
41
- degree: edu.degree || '',
42
- startDate: edu.start_date || '',
43
- endDate: edu.end_date || ''
 
44
  }))
45
  : [{ id: 1, institution: '', degree: '', startDate: '', endDate: '' }]
46
  );
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  const initialSkills = [];
49
  if (resumeData.technical_skills) initialSkills.push(...resumeData.technical_skills);
50
  if (resumeData.skills) initialSkills.push(...resumeData.skills);
51
 
52
  const [skills, setSkills] = useState(initialSkills.join(', '));
 
53
 
54
  // Drag and Drop Handlers
55
  const handleDragStart = (e, keyword) => {
@@ -76,6 +109,15 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
76
  }
77
  };
78
 
 
 
 
 
 
 
 
 
 
79
  const handleDragOver = (e) => {
80
  e.preventDefault(); // Necessary to allow dropping
81
  };
@@ -110,6 +152,15 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
110
  setEducation(prev => prev.filter(edu => edu.id !== id));
111
  };
112
 
 
 
 
 
 
 
 
 
 
113
  const handlePrint = () => {
114
  window.print();
115
  };
@@ -141,6 +192,18 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
141
  paddingBottom: '0.5rem'
142
  };
143
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  return (
145
  <div style={{ display: 'grid', gridTemplateColumns: 'minmax(300px, 1fr) 1.5fr', gap: '2rem', height: 'calc(100vh - 150px)' }}>
146
 
@@ -155,12 +218,26 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
155
  >
156
  <ArrowLeftIcon /> Back to Analysis
157
  </button>
158
- <button
159
- onClick={handlePrint}
160
- 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' }}
161
- >
162
- <DownloadIcon /> Export PDF
163
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  </div>
165
 
166
  {/* Fix Your Gaps Panel */}
@@ -289,6 +366,62 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
289
  ))}
290
  </div>
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  {/* Skills */}
293
  <div>
294
  <h3 style={sectionHeaderStyle}>Skills</h3>
@@ -316,7 +449,7 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
316
  display: 'flex',
317
  justifyContent: 'center'
318
  }}
319
- className="print-container no-print hide-scrollbar" // Wrapper won't print, only content below
320
  >
321
  {/* Actual Paper Element */}
322
  <div
@@ -324,82 +457,125 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
324
  className="resume-print-area"
325
  style={{
326
  backgroundColor: 'white',
327
- color: '#333333',
328
  width: '100%',
329
- maxWidth: '800px',
330
- minHeight: '1056px', // ~8.5x11 aspect ratio
331
  padding: '40px 50px',
332
  boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5)',
333
- fontFamily: "'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif",
334
- lineHeight: '1.5',
335
  boxSizing: 'border-box'
336
  }}
337
  >
338
  {/* Header */}
339
  <div style={{ textAlign: 'center', marginBottom: '20px' }}>
340
- <h1 style={{ fontSize: '36px', fontWeight: '800', margin: '0 0 5px 0', color: '#1e3a8a', letterSpacing: '-0.5px' }}>
341
  {personalInfo.fullName || 'Your Name'}
342
  </h1>
343
- <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '10px', fontSize: '14px', color: '#555555' }}>
344
- {personalInfo.location && <span>{personalInfo.location}</span>}
345
- {personalInfo.location && (personalInfo.phone || personalInfo.email) && <span style={{ color: '#ccc' }}>•</span>}
346
- {personalInfo.phone && <span>{personalInfo.phone}</span>}
347
- {personalInfo.phone && personalInfo.email && <span style={{ color: '#ccc' }}>•</span>}
348
- {personalInfo.email && <span style={{ color: '#1e3a8a' }}>{personalInfo.email}</span>}
349
- {personalInfo.linkedin && <><span style={{ color: '#ccc' }}>•</span><span style={{ color: '#1e3a8a' }}>{personalInfo.linkedin}</span></>}
350
- {personalInfo.portfolio && <><span style={{ color: '#ccc' }}>•</span><span style={{ color: '#1e3a8a' }}>{personalInfo.portfolio}</span></>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  </div>
352
  </div>
353
 
354
- {/* Summary Preview */}
355
  {summary && (
356
- <div style={{ marginBottom: '20px' }}>
357
- <p style={{ fontSize: '15px', color: '#444444', margin: 0, textAlign: 'justify' }}>
 
 
 
358
  {summary}
359
  </p>
360
  </div>
361
  )}
362
 
363
- {/* Skills Preview */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  {skills && (
365
- <div style={{ marginBottom: '20px' }}>
366
- <h2 style={{ fontSize: '16px', fontWeight: '700', color: '#1e3a8a', textTransform: 'uppercase', borderBottom: '2px solid #1e3a8a', paddingBottom: '4px', marginBottom: '12px', letterSpacing: '0.5px' }}>
367
  Technical Skills
368
  </h2>
369
- <p style={{ fontSize: '15px', color: '#333333', margin: 0, lineHeight: '1.6' }}>
370
  {skills}
371
  </p>
372
  </div>
373
  )}
374
 
375
- {/* Experience Preview */}
376
  {experience.some(exp => exp.company || exp.role) && (
377
- <div style={{ marginBottom: '20px' }}>
378
- <h2 style={{ fontSize: '16px', fontWeight: '700', color: '#1e3a8a', textTransform: 'uppercase', borderBottom: '2px solid #1e3a8a', paddingBottom: '4px', marginBottom: '12px', letterSpacing: '0.5px' }}>
379
- Professional Experience
380
  </h2>
381
  {experience.map(exp => (
382
  (exp.company || exp.role) ? (
383
- <div key={exp.id} style={{ marginBottom: '16px' }}>
384
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '2px' }}>
385
- <div style={{ fontWeight: '700', fontSize: '16px', color: '#222222' }}>{exp.role || 'Role'}</div>
386
- <div style={{ fontSize: '14px', color: '#666666', fontStyle: 'italic', whiteSpace: 'nowrap' }}>
387
- {exp.startDate} {exp.startDate && exp.endDate && '–'} {exp.endDate}
388
- </div>
 
389
  </div>
390
- <div style={{ fontSize: '15px', color: '#1e3a8a', fontWeight: '600', marginBottom: '8px' }}>{exp.company}</div>
391
  {exp.description && (
392
- <div style={{ fontSize: '14px', color: '#444444' }}>
393
- {exp.description.split('\n').map((line, i) => (
394
- <div style={{ display: 'flex', marginBottom: '4px' }} key={i}>
395
- {line.trim().startsWith('•') || line.trim().startsWith('-') ? (
396
- <span style={{ marginRight: '8px', color: '#1e3a8a' }}>•</span>
397
- ) : line.trim() ? (
398
- <span style={{ marginRight: '8px', color: '#1e3a8a' }}></span>
399
- ) : null}
400
- <span style={{ flex: 1 }}>{line.replace(/^[•-]\s*/, '')}</span>
401
- </div>
402
- ))}
403
  </div>
404
  )}
405
  </div>
@@ -408,22 +584,55 @@ export default function ResumeBuilder({ analysisResult, onBack }) {
408
  </div>
409
  )}
410
 
411
- {/* Education Preview */}
412
- {education.some(edu => edu.institution || edu.degree) && (
413
- <div style={{ marginBottom: '20px' }}>
414
- <h2 style={{ fontSize: '16px', fontWeight: '700', color: '#1e3a8a', textTransform: 'uppercase', borderBottom: '2px solid #1e3a8a', paddingBottom: '4px', marginBottom: '12px', letterSpacing: '0.5px' }}>
415
- Education
416
  </h2>
417
- {education.map(edu => (
418
- (edu.institution || edu.degree) ? (
419
- <div key={edu.id} style={{ marginBottom: '12px' }}>
420
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '2px' }}>
421
- <div style={{ fontWeight: '700', fontSize: '15px', color: '#222222' }}>{edu.institution || 'Institution'}</div>
422
- <div style={{ fontSize: '14px', color: '#666666', fontStyle: 'italic', whiteSpace: 'nowrap' }}>
423
- {edu.startDate} {edu.startDate && edu.endDate && '–'} {edu.endDate}
 
 
 
 
 
 
 
 
 
 
 
424
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  </div>
426
- <div style={{ fontSize: '15px', color: '#444444' }}>{edu.degree}</div>
427
  </div>
428
  ) : null
429
  ))}
 
26
  id: Date.now() + idx,
27
  company: exp.company || '',
28
  role: exp.role || '',
29
+ startDate: exp.years || exp.start_date || '',
30
  endDate: exp.end_date || '',
31
+ description: Array.isArray(exp.responsibilities) ? exp.responsibilities.map(r => `• ${r}`).join('\n') : (exp.responsibilities || exp.description || '')
32
  }))
33
  : [{ id: 1, company: '', role: '', startDate: '', endDate: '', description: '' }]
34
  );
 
38
  ? resumeData.education.map((edu, idx) => ({
39
  id: Date.now() + idx,
40
  institution: edu.institution || '',
41
+ degree: edu.course || edu.degree || '',
42
+ startDate: edu.year || edu.start_date || '',
43
+ endDate: edu.end_date || '',
44
+ location: edu.location || ''
45
  }))
46
  : [{ id: 1, institution: '', degree: '', startDate: '', endDate: '' }]
47
  );
48
 
49
+ const [projects, setProjects] = useState(
50
+ resumeData.projects?.length > 0
51
+ ? resumeData.projects.map((proj, idx) => ({
52
+ id: Date.now() + idx,
53
+ name: proj.title || proj.name || '',
54
+ description: proj.description || '',
55
+ link: proj.link || ''
56
+ }))
57
+ : [{ id: 1, name: '', description: '', link: '' }]
58
+ );
59
+
60
+ const [certifications, setCertifications] = useState(
61
+ resumeData.certifications?.length > 0
62
+ ? resumeData.certifications.map((cert, idx) => {
63
+ if (typeof cert === 'string') {
64
+ const parts = cert.split(/ - | – | — |: /);
65
+ if (parts.length > 1) {
66
+ return { id: Date.now() + idx, issuer: parts[0].trim(), name: parts.slice(1).join(' - ').trim(), date: '' };
67
+ }
68
+ return { id: Date.now() + idx, name: cert, issuer: '', date: '' };
69
+ }
70
+ return {
71
+ id: Date.now() + idx,
72
+ name: cert.name || cert.title || '',
73
+ issuer: cert.issuer || '',
74
+ date: cert.date || ''
75
+ };
76
+ })
77
+ : [{ id: 1, name: '', issuer: '', date: '' }]
78
+ );
79
+
80
  const initialSkills = [];
81
  if (resumeData.technical_skills) initialSkills.push(...resumeData.technical_skills);
82
  if (resumeData.skills) initialSkills.push(...resumeData.skills);
83
 
84
  const [skills, setSkills] = useState(initialSkills.join(', '));
85
+ const [themeColor, setThemeColor] = useState('#000000');
86
 
87
  // Drag and Drop Handlers
88
  const handleDragStart = (e, keyword) => {
 
109
  }
110
  };
111
 
112
+ const handleProjectDrop = (e, id, currentValue) => {
113
+ e.preventDefault();
114
+ const draggedText = e.dataTransfer.getData("text/plain");
115
+ if (draggedText) {
116
+ const separator = currentValue ? ' ' : '';
117
+ updateProject(id, 'description', `${currentValue}${separator}${draggedText}`);
118
+ }
119
+ };
120
+
121
  const handleDragOver = (e) => {
122
  e.preventDefault(); // Necessary to allow dropping
123
  };
 
152
  setEducation(prev => prev.filter(edu => edu.id !== id));
153
  };
154
 
155
+ const addProject = () => setProjects(prev => [...prev, { id: Date.now(), name: '', description: '', link: '' }]);
156
+ const updateProject = (id, field, value) => setProjects(prev => prev.map(proj => proj.id === id ? { ...proj, [field]: value } : proj));
157
+ const removeProject = (id) => setProjects(prev => prev.filter(proj => proj.id !== id));
158
+
159
+ const addCertification = () => setCertifications(prev => [...prev, { id: Date.now(), name: '', issuer: '', date: '' }]);
160
+ const updateCertification = (id, field, value) => setCertifications(prev => prev.map(cert => cert.id === id ? { ...cert, [field]: value } : cert));
161
+ const removeCertification = (id) => setCertifications(prev => prev.filter(cert => cert.id !== id));
162
+
163
+
164
  const handlePrint = () => {
165
  window.print();
166
  };
 
192
  paddingBottom: '0.5rem'
193
  };
194
 
195
+ const resumeSectionHeaderStyle = {
196
+ fontSize: '14px',
197
+ fontVariant: 'small-caps',
198
+ fontWeight: 'bold',
199
+ textTransform: 'uppercase',
200
+ borderBottom: `1px solid ${themeColor}`,
201
+ color: themeColor,
202
+ paddingBottom: '2px',
203
+ marginBottom: '8px',
204
+ letterSpacing: '1px'
205
+ };
206
+
207
  return (
208
  <div style={{ display: 'grid', gridTemplateColumns: 'minmax(300px, 1fr) 1.5fr', gap: '2rem', height: 'calc(100vh - 150px)' }}>
209
 
 
218
  >
219
  <ArrowLeftIcon /> Back to Analysis
220
  </button>
221
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
222
+ <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)' }}>
223
+ <label style={{ fontSize: '0.75rem', color: '#d1d5db' }}>Theme Color</label>
224
+ <div style={{ position: 'relative', width: '24px', height: '24px', borderRadius: '4px', backgroundColor: themeColor, border: '1px solid rgba(255,255,255,0.2)', overflow: 'hidden' }}>
225
+ <input
226
+ type="color"
227
+ value={themeColor}
228
+ onChange={(e) => setThemeColor(e.target.value)}
229
+ style={{ position: 'absolute', top: '-10px', left: '-10px', width: '50px', height: '50px', cursor: 'pointer', opacity: 0 }}
230
+ title="Change Resume Theme Color"
231
+ />
232
+ </div>
233
+ </div>
234
+ <button
235
+ onClick={handlePrint}
236
+ 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' }}
237
+ >
238
+ <DownloadIcon /> Export PDF
239
+ </button>
240
+ </div>
241
  </div>
242
 
243
  {/* Fix Your Gaps Panel */}
 
366
  ))}
367
  </div>
368
 
369
+ {/* Projects */}
370
+ <div style={{ marginBottom: '2rem' }}>
371
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
372
+ <h3 style={{ ...sectionHeaderStyle, borderBottom: 'none', marginBottom: 0 }}>Projects</h3>
373
+ <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' }}>
374
+ <PlusIcon size={14} /> Add Project
375
+ </button>
376
+ </div>
377
+ {projects.map((proj, index) => (
378
+ <div key={proj.id} style={{ backgroundColor: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '0.5rem', marginBottom: '1rem', position: 'relative' }}>
379
+ {index > 0 && (
380
+ <button onClick={() => removeProject(proj.id)} style={{ position: 'absolute', top: '1rem', right: '1rem', background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer' }} title="Remove Project">
381
+ <TrashIcon size={16} />
382
+ </button>
383
+ )}
384
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
385
+ <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>
386
+ <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>
387
+ </div>
388
+ <label style={labelStyle}>Description (Bullet points recommended)</label>
389
+ <textarea
390
+ style={{ ...inputStyle, minHeight: '60px', resize: 'vertical', marginBottom: 0 }}
391
+ value={proj.description}
392
+ onChange={e => updateProject(proj.id, 'description', e.target.value)}
393
+ onDrop={(e) => handleProjectDrop(e, proj.id, proj.description)}
394
+ onDragOver={handleDragOver}
395
+ placeholder="• Developed using React..."
396
+ />
397
+ </div>
398
+ ))}
399
+ </div>
400
+
401
+ {/* Certifications */}
402
+ <div style={{ marginBottom: '2rem' }}>
403
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
404
+ <h3 style={{ ...sectionHeaderStyle, borderBottom: 'none', marginBottom: 0 }}>Certifications</h3>
405
+ <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' }}>
406
+ <PlusIcon size={14} /> Add Certification
407
+ </button>
408
+ </div>
409
+ {certifications.map((cert, index) => (
410
+ <div key={cert.id} style={{ backgroundColor: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '0.5rem', marginBottom: '1rem', position: 'relative' }}>
411
+ {index > 0 && (
412
+ <button onClick={() => removeCertification(cert.id)} style={{ position: 'absolute', top: '1rem', right: '1rem', background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer' }} title="Remove Certification">
413
+ <TrashIcon size={16} />
414
+ </button>
415
+ )}
416
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem' }}>
417
+ <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>
418
+ <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>
419
+ <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>
420
+ </div>
421
+ </div>
422
+ ))}
423
+ </div>
424
+
425
  {/* Skills */}
426
  <div>
427
  <h3 style={sectionHeaderStyle}>Skills</h3>
 
449
  display: 'flex',
450
  justifyContent: 'center'
451
  }}
452
+ className="hide-scrollbar" // Wrapper shouldn't have no-print so children can print
453
  >
454
  {/* Actual Paper Element */}
455
  <div
 
457
  className="resume-print-area"
458
  style={{
459
  backgroundColor: 'white',
460
+ color: 'black',
461
  width: '100%',
462
+ maxWidth: '850px',
463
+ minHeight: '1100px', // ~8.5x11 aspect ratio
464
  padding: '40px 50px',
465
  boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5)',
466
+ fontFamily: "'Times New Roman', Times, serif",
467
+ lineHeight: '1.4',
468
  boxSizing: 'border-box'
469
  }}
470
  >
471
  {/* Header */}
472
  <div style={{ textAlign: 'center', marginBottom: '20px' }}>
473
+ <h1 style={{ fontSize: '36px', fontWeight: 'normal', margin: '0 0 10px 0', letterSpacing: '1px', color: themeColor }}>
474
  {personalInfo.fullName || 'Your Name'}
475
  </h1>
476
+ <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '16px', fontSize: '13px' }}>
477
+ {personalInfo.phone && (
478
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
479
+ <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>
480
+ {personalInfo.phone}
481
+ </div>
482
+ )}
483
+ {personalInfo.email && (
484
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
485
+ <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>
486
+ {personalInfo.email}
487
+ </div>
488
+ )}
489
+ {personalInfo.linkedin && (
490
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
491
+ <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>
492
+ {personalInfo.linkedin}
493
+ </div>
494
+ )}
495
+ {personalInfo.portfolio && (
496
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
497
+ <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>
498
+ {personalInfo.portfolio}
499
+ </div>
500
+ )}
501
  </div>
502
  </div>
503
 
504
+ {/* Summary */}
505
  {summary && (
506
+ <div style={{ marginBottom: '16px' }}>
507
+ <h2 style={resumeSectionHeaderStyle}>
508
+ Summary
509
+ </h2>
510
+ <p style={{ fontSize: '13px', margin: 0, textAlign: 'justify' }}>
511
  {summary}
512
  </p>
513
  </div>
514
  )}
515
 
516
+ {/* Education */}
517
+ {education.some(edu => edu.institution || edu.degree) && (
518
+ <div style={{ marginBottom: '16px' }}>
519
+ <h2 style={resumeSectionHeaderStyle}>
520
+ Education
521
+ </h2>
522
+ {education.map(edu => (
523
+ (edu.institution || edu.degree) ? (
524
+ <div key={edu.id} style={{ marginBottom: '8px' }}>
525
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}>
526
+ <span style={{ fontWeight: 'bold' }}>{edu.institution}</span>
527
+ <span>{edu.location || ''}</span>
528
+ </div>
529
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', fontStyle: 'italic' }}>
530
+ <span>{edu.degree}</span>
531
+ <span>{edu.startDate} {edu.startDate && edu.endDate && '–'} {edu.endDate}</span>
532
+ </div>
533
+ </div>
534
+ ) : null
535
+ ))}
536
+ </div>
537
+ )}
538
+
539
+ {/* Technical Skills */}
540
  {skills && (
541
+ <div style={{ marginBottom: '16px' }}>
542
+ <h2 style={resumeSectionHeaderStyle}>
543
  Technical Skills
544
  </h2>
545
+ <p style={{ fontSize: '13px', margin: 0 }}>
546
  {skills}
547
  </p>
548
  </div>
549
  )}
550
 
551
+ {/* Professional Experience */}
552
  {experience.some(exp => exp.company || exp.role) && (
553
+ <div style={{ marginBottom: '16px' }}>
554
+ <h2 style={resumeSectionHeaderStyle}>
555
+ Experience
556
  </h2>
557
  {experience.map(exp => (
558
  (exp.company || exp.role) ? (
559
+ <div key={exp.id} style={{ marginBottom: '12px' }}>
560
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}>
561
+ <span style={{ fontWeight: 'bold' }}>{exp.role}</span>
562
+ <span style={{ fontWeight: 'bold' }}>{exp.startDate} {exp.startDate && exp.endDate && ''} {exp.endDate}</span>
563
+ </div>
564
+ <div style={{ fontSize: '13px', fontStyle: 'italic', marginBottom: '4px' }}>
565
+ {exp.company}
566
  </div>
 
567
  {exp.description && (
568
+ <div style={{ fontSize: '13px' }}>
569
+ {exp.description.split('\n').map((line, i) => {
570
+ const trimmedLine = line.trim();
571
+ if (!trimmedLine) return null;
572
+ return (
573
+ <div style={{ display: 'flex', marginBottom: '2px', paddingLeft: '16px' }} key={i}>
574
+ <span style={{ marginRight: '8px' }}></span>
575
+ <span>{trimmedLine.replace(/^[•-]\s*/, '')}</span>
576
+ </div>
577
+ );
578
+ })}
579
  </div>
580
  )}
581
  </div>
 
584
  </div>
585
  )}
586
 
587
+ {/* Projects */}
588
+ {projects.some(proj => proj.name || proj.description) && (
589
+ <div style={{ marginBottom: '16px' }}>
590
+ <h2 style={resumeSectionHeaderStyle}>
591
+ Projects
592
  </h2>
593
+ {projects.map(proj => (
594
+ (proj.name || proj.description) ? (
595
+ <div key={proj.id} style={{ marginBottom: '10px' }}>
596
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', marginBottom: '4px' }}>
597
+ <span><strong style={{ fontWeight: 'bold' }}>{proj.name}</strong>{proj.link ? ` | ${proj.link}` : ''}</span>
598
+ </div>
599
+ {proj.description && (
600
+ <div style={{ fontSize: '13px' }}>
601
+ {proj.description.split('\n').map((line, i) => {
602
+ const trimmedLine = line.trim();
603
+ if (!trimmedLine) return null;
604
+ return (
605
+ <div style={{ display: 'flex', marginBottom: '2px', paddingLeft: '16px' }} key={i}>
606
+ <span style={{ marginRight: '8px' }}>–</span>
607
+ <span>{trimmedLine.replace(/^[•-]\s*/, '')}</span>
608
+ </div>
609
+ );
610
+ })}
611
  </div>
612
+ )}
613
+ </div>
614
+ ) : null
615
+ ))}
616
+ </div>
617
+ )}
618
+
619
+ {/* Certifications */}
620
+ {certifications.some(cert => cert.name || cert.issuer) && (
621
+ <div style={{ marginBottom: '16px' }}>
622
+ <h2 style={resumeSectionHeaderStyle}>
623
+ Certifications
624
+ </h2>
625
+ {certifications.map(cert => (
626
+ (cert.name || cert.issuer) ? (
627
+ <div key={cert.id} style={{ marginBottom: '4px', fontSize: '13px' }}>
628
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
629
+ <span>
630
+ {cert.issuer && <strong style={{ fontWeight: 'bold' }}>{cert.issuer}</strong>}
631
+ {cert.issuer && cert.name && ' — '}
632
+ {cert.name && <span>{cert.name}</span>}
633
+ </span>
634
+ <span>{cert.date}</span>
635
  </div>
 
636
  </div>
637
  ) : null
638
  ))}
src/index.css CHANGED
@@ -1,10 +1,13 @@
1
- body, html, #root {
 
 
2
  margin: 0;
3
  padding: 0;
4
  height: 100%;
5
  width: 100%;
6
  box-sizing: border-box;
7
  }
 
8
  body {
9
  margin: 0;
10
  }
@@ -16,7 +19,7 @@ body {
16
  .dark-select {
17
  background-color: #111827;
18
  color: rgb(51, 8, 8);
19
- border: 1px solid rgba(255,255,255,0.2);
20
  padding: 0.75rem;
21
  border-radius: 0.5rem;
22
  color-scheme: dark;
@@ -35,8 +38,10 @@ body {
35
  }
36
 
37
  .hide-scrollbar {
38
- -ms-overflow-style: none; /* IE and Edge */
39
- scrollbar-width: none; /* Firefox */
 
 
40
  }
41
 
42
  /* ===============================
@@ -46,33 +51,41 @@ body {
46
  body * {
47
  visibility: hidden;
48
  }
49
-
50
- .print-container, .print-container * {
 
51
  visibility: visible;
52
  }
53
-
54
- .print-container {
55
- position: absolute;
56
- left: 0;
57
- top: 0;
58
- width: 100vw;
59
- margin: 0;
60
- padding: 0;
61
- background: white !important;
62
  }
63
 
64
- .no-print {
65
- display: none !important;
 
 
 
 
66
  }
67
-
68
  .resume-print-area {
 
 
69
  width: 100% !important;
70
  max-width: none !important;
71
- padding: 0 !important;
72
  box-shadow: none !important;
 
73
  }
74
-
 
 
 
 
75
  @page {
76
- margin: 1cm;
77
  }
78
  }
 
1
+ body,
2
+ html,
3
+ #root {
4
  margin: 0;
5
  padding: 0;
6
  height: 100%;
7
  width: 100%;
8
  box-sizing: border-box;
9
  }
10
+
11
  body {
12
  margin: 0;
13
  }
 
19
  .dark-select {
20
  background-color: #111827;
21
  color: rgb(51, 8, 8);
22
+ border: 1px solid rgba(255, 255, 255, 0.2);
23
  padding: 0.75rem;
24
  border-radius: 0.5rem;
25
  color-scheme: dark;
 
38
  }
39
 
40
  .hide-scrollbar {
41
+ -ms-overflow-style: none;
42
+ /* IE and Edge */
43
+ scrollbar-width: none;
44
+ /* Firefox */
45
  }
46
 
47
  /* ===============================
 
51
  body * {
52
  visibility: hidden;
53
  }
54
+
55
+ .resume-print-area,
56
+ .resume-print-area * {
57
  visibility: visible;
58
  }
59
+
60
+ /* Force ancestors to be static so absolute positioning aligns to the page */
61
+ body, html, #root, .no-print-bg {
62
+ position: static !important;
63
+ margin: 0 !important;
64
+ padding: 0 !important;
 
 
 
65
  }
66
 
67
+ .hide-scrollbar {
68
+ position: absolute !important;
69
+ top: 0 !important;
70
+ left: 0 !important;
71
+ overflow: visible !important;
72
+ display: block !important;
73
  }
74
+
75
  .resume-print-area {
76
+ position: relative !important;
77
+ margin: 0 auto !important;
78
  width: 100% !important;
79
  max-width: none !important;
 
80
  box-shadow: none !important;
81
+ background-color: white !important;
82
  }
83
+
84
+ .no-print {
85
+ display: none !important;
86
+ }
87
+
88
  @page {
89
+ margin: 0;
90
  }
91
  }
src/pages/AdminLogin.jsx CHANGED
@@ -69,7 +69,7 @@ export default function AdminLogin({ onNavigate }) {
69
  alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
70
  backgroundColor: '#020617', color: 'white', fontFamily: "'Montserrat', sans-serif", padding: '1rem',
71
  }}>
72
- <style>{`@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); input:-webkit-autofill { -webkit-text-fill-color: #fff !important; -webkit-box-shadow: 0 0 0px 1000px rgba(239, 68, 68, 0.1) inset !important; }`}</style>
73
 
74
  {/* --- 🔴 FIXED BACK BUTTON --- */}
75
  <button
 
69
  alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
70
  backgroundColor: '#020617', color: 'white', fontFamily: "'Montserrat', sans-serif", padding: '1rem',
71
  }}>
72
+ <style>{`@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { transition: background-color 5000s ease-in-out 0s; -webkit-text-fill-color: #fff !important; }`}</style>
73
 
74
  {/* --- 🔴 FIXED BACK BUTTON --- */}
75
  <button
src/pages/AppliLogin.jsx CHANGED
@@ -98,7 +98,7 @@ export default function AppliLogin({ onNavigate }) {
98
 
99
  return (
100
  <div style={{ position: 'relative', minHeight: '100vh', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1rem', overflow: 'hidden', backgroundColor: '#020617', color: 'white', fontFamily: "'Montserrat', sans-serif" }}>
101
- <style>{`@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); input:-webkit-autofill { -webkit-text-fill-color: #fff !important; -webkit-box-shadow: 0 0 0px 1000px rgba(251, 191, 36, 0.1) inset !important; }`}</style>
102
 
103
  {/* --- 🔴 FIXED BACK BUTTON (Z-INDEX 999) --- */}
104
  <button
 
98
 
99
  return (
100
  <div style={{ position: 'relative', minHeight: '100vh', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1rem', overflow: 'hidden', backgroundColor: '#020617', color: 'white', fontFamily: "'Montserrat', sans-serif" }}>
101
+ <style>{`@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { transition: background-color 5000s ease-in-out 0s; -webkit-text-fill-color: #fff !important; }`}</style>
102
 
103
  {/* --- 🔴 FIXED BACK BUTTON (Z-INDEX 999) --- */}
104
  <button
src/pages/ApplicantATS.jsx CHANGED
@@ -65,7 +65,8 @@ export default function ApplicantATS({ onNavigate }) {
65
  });
66
 
67
  if (!response.ok) {
68
- throw new Error("Analysis failed. Please try again.");
 
69
  }
70
 
71
  const result = await response.json();
@@ -77,7 +78,7 @@ export default function ApplicantATS({ onNavigate }) {
77
  }
78
  } catch (error) {
79
  console.error("ATS Check Error:", error);
80
- alert("An error occurred during analysis. Check console for details.");
81
  } finally {
82
  setIsAnalyzing(false);
83
  }
 
65
  });
66
 
67
  if (!response.ok) {
68
+ const errData = await response.json().catch(() => null);
69
+ throw new Error(errData?.detail || "Analysis failed. Please try again.");
70
  }
71
 
72
  const result = await response.json();
 
78
  }
79
  } catch (error) {
80
  console.error("ATS Check Error:", error);
81
+ alert(`Analysis Error: ${error.message}`);
82
  } finally {
83
  setIsAnalyzing(false);
84
  }