import React, { useState, useCallback } from 'react';
import { Plus, Trash2, GripVertical, Sparkles, FileText, Download, ChevronDown, ChevronRight, X, Check, AlertCircle, Briefcase, GraduationCap, Award, Languages, Mic, Users } from 'lucide-react';
// Draggable context for bullet points
const DragContext = React.createContext();
const useDrag = () => React.useContext(DragContext);
export default function ResumeBuilder() {
// Resume data state
const [resume, setResume] = useState({
contact: {
name: 'EJ SMILEY',
phone: '(310) 555-1212',
email: 'ej.smiley.20xx@anderson.ucla.edu',
linkedin: 'linkedin.com/in/ejsmiley'
},
education: [
{
id: 'edu1',
institution: 'UCLA ANDERSON SCHOOL OF MANAGEMENT',
location: 'Los Angeles, CA',
degree: 'M.B.A., Fully Employed Program, Specialization Easton Technology Leadership',
details: 'GMAT: 720',
date: '06/20xx',
bullets: [
{ id: 'edu1b1', text: 'Awards: ACG Case Competition 1st place winner; Dean\'s Honor List' },
{ id: 'edu1b2', text: 'Leadership: President, AnderTech' }
]
},
{
id: 'edu2',
institution: 'UNIVERSITY OF MICHIGAN',
location: 'Ann Arbor, MI',
degree: 'B.S., Mechanical Engineering',
details: 'GPA: 3.9',
date: '12/2012',
bullets: [
{ id: 'edu2b1', text: 'Honors: Dean\'s International Computing & Engineering excellence award; Dean\'s Honor List' }
]
}
],
experience: [
{
id: 'exp1',
company: 'BIOSTERN',
subtitle: 'Medical device manufacturer and global market leader in measurement of human performance',
location: 'San Jose, CA',
titles: [
{ id: 'title1', title: 'Product Manager', dates: '11/2016 – Present' },
{ id: 'title2', title: 'Product Marketing Manager', dates: '10/2014 – 10/2016' }
],
description: 'Responsibilities ranged from market research and marketing to product category management and market strategy.',
skillBuckets: [
{
id: 'bucket1',
name: 'Product Innovation',
bullets: [
{ id: 'b1', text: 'Accelerated product launch timetable by 35% after forensic analysis led to design changes optimized for manufacturability' },
{ id: 'b2', text: 'Spearheaded consumer insight-based strategic initiative to identify and prioritize key consumer pain points, generating 200+ product concepts, resulting in 30+ products commercialized in less than 2 years' },
{ id: 'b3', text: 'Defined go-to market strategy via competitive analysis, trend spotting and primary research to execute diversification into new category of fitness wearables, generating profitable revenues of $1.5M in Year 1' }
]
},
{
id: 'bucket2',
name: 'Cross-functional Team Management',
bullets: [
{ id: 'b4', text: 'Led multi-departmental task force across Marketing, Sales, Operations and Engineering to pioneer new user interface design via customer needs assessment, technology analysis and rapid prototyping that landed multi-year, multi-million dollar contract' },
{ id: 'b5', text: 'Accelerated time-to-market by 40% for new products through integration of Engineering and Operations personnel and processes into launch planning and project management' }
]
},
{
id: 'bucket3',
name: 'Customer Insights',
bullets: [
{ id: 'b6', text: 'Pioneered methodology for ranking consumer pain points, generating 4 new customer segments for potential diversification' },
{ id: 'b7', text: 'Developed white-space analysis of adjacent business opportunities resulting in customization of biomechanical sensors and landing largest OEM account in history of company' },
{ id: 'b8', text: 'Conducted anthropological-style consumer research to identify perceptions of technology-aided performance enhancers and impact on intention to purchase, resulting in re-positioning of key benefits and 110% growth in product revenues' }
]
},
{
id: 'bucket4',
name: 'Integrated Marketing Programs',
bullets: [
{ id: 'b9', text: 'Supervised marketing team to develop global product positioning and marketing materials, saving $2M+ in localization costs and doubling worldwide brand awareness' },
{ id: 'b10', text: 'Utilized data analytics to tap into databases of targeted users and customers to cross-promote and offer product enhancements, resulting in 400% revenue growth through affiliate marketing activities' }
]
}
]
},
{
id: 'exp2',
company: 'INNOPROTO',
subtitle: 'New product think tank serving healthcare and technology industries',
location: 'Long Beach, CA',
titles: [
{ id: 'title3', title: 'Strategy Analyst', dates: '7/2012 – 10/2014' }
],
description: 'Responsible for trend spotting; emerging technology research; market analyses and authoring white papers.',
skillBuckets: [
{
id: 'bucket5',
name: 'Strategic Analysis',
bullets: [
{ id: 'b11', text: 'Devised product diversification strategy via customer segmentation, competitive benchmarking and field research for leading medical device manufacturer, bringing in $500K new revenues in 1st year' },
{ id: 'b12', text: 'Developed market entry plan for traumatic brain injury (TBI) monitoring devices, including identifying key channel partnerships, forecast to deliver $500K in first year with projected growth 8.7% YoY for initial five years' },
{ id: 'b13', text: 'Authored thought leadership paper for biotech research company that led to two new contracts with US DoD (Dept. of Defense)' }
]
}
]
}
],
additional: {
languages: 'Bilingual English-French',
volunteer: 'Board of Directors of LA GOAL; Regional Vice President of SITI (Students in Tech International)',
awards: 'National winner of humorous speech contest, Toastmasters International, 2017 World Competition',
conferences: 'Invited panel speaker at annual International Marketing Innovations Summit, 2016, Montreal, Canada'
}
});
// Bullet point bank
const [bulletBank, setBulletBank] = useState([
{ id: 'bank1', text: 'Increased revenue by 50% through strategic partnerships', category: 'Revenue Growth' },
{ id: 'bank2', text: 'Led cross-functional team of 12 to deliver project 2 weeks ahead of schedule', category: 'Leadership' },
{ id: 'bank3', text: 'Implemented agile methodologies resulting in 30% improvement in sprint velocity', category: 'Process Improvement' },
{ id: 'bank4', text: 'Conducted A/B testing on marketing campaigns, improving conversion rates by 25%', category: 'Marketing' },
{ id: 'bank5', text: 'Developed machine learning model that reduced customer churn by 15%', category: 'Technical' }
]);
// Job description analysis
const [jobDescription, setJobDescription] = useState('');
const [keywordAnalysis, setKeywordAnalysis] = useState(null);
const [suggestedKeywords, setSuggestedKeywords] = useState([]);
// UI state
const [activeTab, setActiveTab] = useState('editor');
const [expandedSections, setExpandedSections] = useState({
contact: true,
education: true,
experience: true,
additional: true
});
const [draggedItem, setDraggedItem] = useState(null);
const [dragOverTarget, setDragOverTarget] = useState(null);
const [newBullet, setNewBullet] = useState({ text: '', category: '' });
// Generate unique ID
const generateId = () => `id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Extract keywords from text
const extractKeywords = (text) => {
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought', 'used', 'we', 'you', 'they', 'he', 'she', 'it', 'i', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'its', 'our', 'their', 'this', 'that', 'these', 'those', 'who', 'whom', 'which', 'what', 'whose', 'where', 'when', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'also', 'now', 'here', 'there', 'then', 'once', 'if', 'unless', 'until', 'while', 'about', 'above', 'after', 'again', 'against', 'any', 'because', 'before', 'below', 'between', 'during', 'into', 'through', 'under', 'up', 'down', 'out', 'off', 'over', 'under', 'further', 'etc', 'including', 'across', 'within', 'without', 'along', 'among', 'around', 'behind', 'beyond']);
const words = text.toLowerCase()
.replace(/[^\w\s-]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 2 && !stopWords.has(word));
// Also extract multi-word phrases
const phrases = [];
const cleanText = text.toLowerCase().replace(/[^\w\s-]/g, ' ');
const twoWordPattern = /\b(\w+\s+\w+)\b/g;
let match;
while ((match = twoWordPattern.exec(cleanText)) !== null) {
const phrase = match[1];
const words = phrase.split(/\s+/);
if (!words.some(w => stopWords.has(w)) && words.every(w => w.length > 2)) {
phrases.push(phrase);
}
}
return { words: [...new Set(words)], phrases: [...new Set(phrases)] };
};
// Get all resume text for keyword matching
const getResumeText = () => {
let text = '';
text += Object.values(resume.contact).join(' ');
resume.education.forEach(edu => {
text += ` ${edu.institution} ${edu.degree} ${edu.details}`;
edu.bullets.forEach(b => text += ` ${b.text}`);
});
resume.experience.forEach(exp => {
text += ` ${exp.company} ${exp.subtitle} ${exp.description}`;
exp.titles.forEach(t => text += ` ${t.title}`);
exp.skillBuckets.forEach(bucket => {
text += ` ${bucket.name}`;
bucket.bullets.forEach(b => text += ` ${b.text}`);
});
});
text += ` ${Object.values(resume.additional).join(' ')}`;
return text;
};
// Analyze job description
const analyzeJobDescription = () => {
if (!jobDescription.trim()) return;
const jdKeywords = extractKeywords(jobDescription);
const resumeText = getResumeText().toLowerCase();
const matchedWords = jdKeywords.words.filter(word => resumeText.includes(word));
const missingWords = jdKeywords.words.filter(word => !resumeText.includes(word));
const matchedPhrases = jdKeywords.phrases.filter(phrase => resumeText.includes(phrase));
const missingPhrases = jdKeywords.phrases.filter(phrase => !resumeText.includes(phrase));
// Score important keywords higher (those that appear multiple times in JD)
const wordFreq = {};
jobDescription.toLowerCase().split(/\s+/).forEach(word => {
wordFreq[word] = (wordFreq[word] || 0) + 1;
});
const importantMissing = missingWords
.filter(word => wordFreq[word] > 1 || word.length > 6)
.slice(0, 15);
setKeywordAnalysis({
total: jdKeywords.words.length,
matched: matchedWords.length,
missing: missingWords.length,
matchedWords,
missingWords: importantMissing,
matchedPhrases,
missingPhrases: missingPhrases.slice(0, 10),
score: Math.round((matchedWords.length / jdKeywords.words.length) * 100)
});
setSuggestedKeywords([...importantMissing, ...missingPhrases.slice(0, 5)]);
};
// Drag and drop handlers
const handleDragStart = (e, item, source) => {
setDraggedItem({ item, source });
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (e, target) => {
e.preventDefault();
setDragOverTarget(target);
};
const handleDragLeave = () => {
setDragOverTarget(null);
};
const handleDrop = (e, target) => {
e.preventDefault();
setDragOverTarget(null);
if (!draggedItem) return;
const { item, source } = draggedItem;
// Handle dropping keyword suggestion into a bullet
if (source === 'keyword' && target.type === 'bucket') {
const { expId, bucketId } = target;
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.map(bucket => {
if (bucket.id !== bucketId) return bucket;
return {
...bucket,
bullets: [...bucket.bullets, { id: generateId(), text: `[Add accomplishment using: ${item}]` }]
};
})
};
})
}));
setSuggestedKeywords(prev => prev.filter(k => k !== item));
}
// Handle dropping from bullet bank
if (source === 'bank' && target.type === 'bucket') {
const { expId, bucketId } = target;
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.map(bucket => {
if (bucket.id !== bucketId) return bucket;
return {
...bucket,
bullets: [...bucket.bullets, { id: generateId(), text: item.text }]
};
})
};
})
}));
}
// Handle reordering bullets within buckets
if (source.type === 'bucket' && target.type === 'bucket') {
// Implement reordering logic here if needed
}
setDraggedItem(null);
};
// Update functions
const updateContact = (field, value) => {
setResume(prev => ({
...prev,
contact: { ...prev.contact, [field]: value }
}));
};
const updateEducation = (eduId, field, value) => {
setResume(prev => ({
...prev,
education: prev.education.map(edu =>
edu.id === eduId ? { ...edu, [field]: value } : edu
)
}));
};
const updateEducationBullet = (eduId, bulletId, value) => {
setResume(prev => ({
...prev,
education: prev.education.map(edu => {
if (edu.id !== eduId) return edu;
return {
...edu,
bullets: edu.bullets.map(b =>
b.id === bulletId ? { ...b, text: value } : b
)
};
})
}));
};
const addEducationBullet = (eduId) => {
setResume(prev => ({
...prev,
education: prev.education.map(edu => {
if (edu.id !== eduId) return edu;
return {
...edu,
bullets: [...edu.bullets, { id: generateId(), text: '' }]
};
})
}));
};
const deleteEducationBullet = (eduId, bulletId) => {
setResume(prev => ({
...prev,
education: prev.education.map(edu => {
if (edu.id !== eduId) return edu;
return {
...edu,
bullets: edu.bullets.filter(b => b.id !== bulletId)
};
})
}));
};
const updateExperience = (expId, field, value) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp =>
exp.id === expId ? { ...exp, [field]: value } : exp
)
}));
};
const updateExperienceTitle = (expId, titleId, field, value) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
titles: exp.titles.map(t =>
t.id === titleId ? { ...t, [field]: value } : t
)
};
})
}));
};
const addExperienceTitle = (expId) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
titles: [...exp.titles, { id: generateId(), title: '', dates: '' }]
};
})
}));
};
const deleteExperienceTitle = (expId, titleId) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
titles: exp.titles.filter(t => t.id !== titleId)
};
})
}));
};
const updateBucket = (expId, bucketId, field, value) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.map(bucket =>
bucket.id === bucketId ? { ...bucket, [field]: value } : bucket
)
};
})
}));
};
const addBucket = (expId) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: [...exp.skillBuckets, { id: generateId(), name: 'New Skill Category', bullets: [] }]
};
})
}));
};
const deleteBucket = (expId, bucketId) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.filter(b => b.id !== bucketId)
};
})
}));
};
const updateBullet = (expId, bucketId, bulletId, value) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.map(bucket => {
if (bucket.id !== bucketId) return bucket;
return {
...bucket,
bullets: bucket.bullets.map(b =>
b.id === bulletId ? { ...b, text: value } : b
)
};
})
};
})
}));
};
const addBullet = (expId, bucketId) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.map(bucket => {
if (bucket.id !== bucketId) return bucket;
return {
...bucket,
bullets: [...bucket.bullets, { id: generateId(), text: '' }]
};
})
};
})
}));
};
const deleteBullet = (expId, bucketId, bulletId) => {
setResume(prev => ({
...prev,
experience: prev.experience.map(exp => {
if (exp.id !== expId) return exp;
return {
...exp,
skillBuckets: exp.skillBuckets.map(bucket => {
if (bucket.id !== bucketId) return bucket;
return {
...bucket,
bullets: bucket.bullets.filter(b => b.id !== bulletId)
};
})
};
})
}));
};
const updateAdditional = (field, value) => {
setResume(prev => ({
...prev,
additional: { ...prev.additional, [field]: value }
}));
};
const addExperience = () => {
setResume(prev => ({
...prev,
experience: [...prev.experience, {
id: generateId(),
company: 'NEW COMPANY',
subtitle: 'Company description',
location: 'City, ST',
titles: [{ id: generateId(), title: 'Job Title', dates: 'Start – End' }],
description: 'Brief description of responsibilities.',
skillBuckets: [{ id: generateId(), name: 'Key Skills', bullets: [] }]
}]
}));
};
const deleteExperience = (expId) => {
setResume(prev => ({
...prev,
experience: prev.experience.filter(e => e.id !== expId)
}));
};
const addEducation = () => {
setResume(prev => ({
...prev,
education: [...prev.education, {
id: generateId(),
institution: 'INSTITUTION NAME',
location: 'City, ST',
degree: 'Degree Type, Major',
details: 'GPA or other details',
date: 'MM/YYYY',
bullets: []
}]
}));
};
const deleteEducation = (eduId) => {
setResume(prev => ({
...prev,
education: prev.education.filter(e => e.id !== eduId)
}));
};
// Bullet bank functions
const addToBulletBank = () => {
if (!newBullet.text.trim()) return;
setBulletBank(prev => [...prev, {
id: generateId(),
text: newBullet.text,
category: newBullet.category || 'Uncategorized'
}]);
setNewBullet({ text: '', category: '' });
};
const deleteFromBulletBank = (id) => {
setBulletBank(prev => prev.filter(b => b.id !== id));
};
const toggleSection = (section) => {
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
};
// Section Header Component
const SectionHeader = ({ title, section, icon: Icon }) => (
<button
onClick={() => toggleSection(section)}
className="w-full flex items-center gap-3 py-3 px-4 bg-gradient-to-r from-slate-800 to-slate-700 rounded-lg mb-3 hover:from-slate-700 hover:to-slate-600 transition-all group"
>
<Icon size={18} className="text-amber-400" />
<span className="font-semibold text-white tracking-wide flex-1 text-left">{title}</span>
{expandedSections[section] ?
<ChevronDown size={18} className="text-slate-400 group-hover:text-white transition-colors" /> :
<ChevronRight size={18} className="text-slate-400 group-hover:text-white transition-colors" />
}
</button>
);
// Input Component
const Input = ({ value, onChange, placeholder, className = '', ...props }) => (
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className={`w-full px-3 py-2 bg-slate-800/50 border border-slate-600/50 rounded-lg text-slate-200 placeholder-slate-500 focus:outline-none focus:border-amber-500/50 focus:ring-1 focus:ring-amber-500/25 transition-all ${className}`}
{...props}
/>
);
// Textarea Component
const Textarea = ({ value, onChange, placeholder, rows = 3, className = '' }) => (
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
rows={rows}
className={`w-full px-3 py-2 bg-slate-800/50 border border-slate-600/50 rounded-lg text-slate-200 placeholder-slate-500 focus:outline-none focus:border-amber-500/50 focus:ring-1 focus:ring-amber-500/25 transition-all resize-none ${className}`}
/>
);
// Draggable Bullet Component
const DraggableBullet = ({ bullet, expId, bucketId, onUpdate, onDelete }) => (
<div
draggable
onDragStart={(e) => handleDragStart(e, bullet, { type: 'bucket', expId, bucketId })}
className="group flex items-start gap-2 p-2 bg-slate-800/30 rounded-lg hover:bg-slate-700/50 transition-all cursor-move"
>
<GripVertical size={16} className="text-slate-500 mt-2 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
<Textarea
value={bullet.text}
onChange={(val) => onUpdate(val)}
rows={2}
className="flex-1 text-sm"
/>
<button
onClick={onDelete}
className="p-1 text-slate-500 hover:text-red-400 transition-colors flex-shrink-0 opacity-0 group-hover:opacity-100"
>
<Trash2 size={14} />
</button>
</div>
);
// Drop Zone Component
const DropZone = ({ target, children, className = '' }) => (
<div
onDragOver={(e) => handleDragOver(e, target)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, target)}
className={`transition-all ${
dragOverTarget && dragOverTarget.bucketId === target.bucketId
? 'ring-2 ring-amber-500/50 bg-amber-500/10'
: ''
} ${className}`}
>
{children}
</div>
);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
{/* Decorative elements */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-amber-500/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl" />
</div>
{/* Header */}
<header className="relative border-b border-slate-800/50 backdrop-blur-xl bg-slate-900/50">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center">
<FileText size={20} className="text-slate-900" />
</div>
<div>
<h1 className="text-xl font-bold tracking-tight">Resume Builder</h1>
<p className="text-xs text-slate-500">Stacked Titles • Skill Buckets Format</p>
</div>
</div>
<div className="flex items-center gap-2">
{['editor', 'preview', 'analyze', 'bank'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
activeTab === tab
? 'bg-amber-500 text-slate-900'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
</div>
</div>
</header>
<main className="relative max-w-7xl mx-auto px-6 py-8">
{/* Editor Tab */}
{activeTab === 'editor' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Editor Panel */}
<div className="space-y-4">
{/* Contact Section */}
<SectionHeader title="CONTACT INFORMATION" section="contact" icon={Users} />
{expandedSections.contact && (
<div className="space-y-3 pl-4 mb-6">
<Input
value={resume.contact.name}
onChange={(val) => updateContact('name', val)}
placeholder="Full Name"
className="text-lg font-semibold"
/>
<div className="grid grid-cols-2 gap-3">
<Input
value={resume.contact.phone}
onChange={(val) => updateContact('phone', val)}
placeholder="Phone"
/>
<Input
value={resume.contact.email}
onChange={(val) => updateContact('email', val)}
placeholder="Email"
/>
</div>
<Input
value={resume.contact.linkedin}
onChange={(val) => updateContact('linkedin', val)}
placeholder="LinkedIn URL"
/>
</div>
)}
{/* Education Section */}
<SectionHeader title="EDUCATION" section="education" icon={GraduationCap} />
{expandedSections.education && (
<div className="space-y-4 pl-4 mb-6">
{resume.education.map((edu) => (
<div key={edu.id} className="p-4 bg-slate-800/30 rounded-xl border border-slate-700/50 space-y-3">
<div className="flex items-start justify-between gap-2">
<Input
value={edu.institution}
onChange={(val) => updateEducation(edu.id, 'institution', val)}
placeholder="Institution"
className="font-semibold"
/>
<button
onClick={() => deleteEducation(edu.id)}
className="p-2 text-slate-500 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
value={edu.location}
onChange={(val) => updateEducation(edu.id, 'location', val)}
placeholder="Location"
/>
<Input
value={edu.date}
onChange={(val) => updateEducation(edu.id, 'date', val)}
placeholder="Date"
/>
</div>
<Input
value={edu.degree}
onChange={(val) => updateEducation(edu.id, 'degree', val)}
placeholder="Degree"
/>
<Input
value={edu.details}
onChange={(val) => updateEducation(edu.id, 'details', val)}
placeholder="GPA / Details"
/>
<div className="space-y-2">
{edu.bullets.map((bullet) => (
<div key={bullet.id} className="flex items-center gap-2">
<span className="text-amber-400">•</span>
<Input
value={bullet.text}
onChange={(val) => updateEducationBullet(edu.id, bullet.id, val)}
placeholder="Award, honor, or activity"
className="flex-1 text-sm"
/>
<button
onClick={() => deleteEducationBullet(edu.id, bullet.id)}
className="p-1 text-slate-500 hover:text-red-400"
>
<Trash2 size={14} />
</button>
</div>
))}
<button
onClick={() => addEducationBullet(edu.id)}
className="flex items-center gap-2 text-sm text-slate-400 hover:text-amber-400 transition-colors"
>
<Plus size={14} /> Add bullet
</button>
</div>
</div>
))}
<button
onClick={addEducation}
className="w-full py-3 border-2 border-dashed border-slate-700 rounded-xl text-slate-400 hover:border-amber-500/50 hover:text-amber-400 transition-all flex items-center justify-center gap-2"
>
<Plus size={18} /> Add Education
</button>
</div>
)}
{/* Experience Section */}
<SectionHeader title="EXPERIENCE" section="experience" icon={Briefcase} />
{expandedSections.experience && (
<div className="space-y-6 pl-4 mb-6">
{resume.experience.map((exp) => (
<div key={exp.id} className="p-4 bg-slate-800/30 rounded-xl border border-slate-700/50 space-y-4">
<div className="flex items-start justify-between gap-2">
<Input
value={exp.company}
onChange={(val) => updateExperience(exp.id, 'company', val)}
placeholder="Company Name"
className="font-bold text-lg"
/>
<button
onClick={() => deleteExperience(exp.id)}
className="p-2 text-slate-500 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
<Input
value={exp.subtitle}
onChange={(val) => updateExperience(exp.id, 'subtitle', val)}
placeholder="Company description"
className="text-sm italic"
/>
<Input
value={exp.location}
onChange={(val) => updateExperience(exp.id, 'location', val)}
placeholder="Location"
/>
{/* Stacked Titles */}
<div className="space-y-2 p-3 bg-slate-900/50 rounded-lg">
<p className="text-xs text-slate-500 uppercase tracking-wider mb-2">Stacked Titles</p>
{exp.titles.map((title) => (
<div key={title.id} className="flex items-center gap-2">
<Input
value={title.title}
onChange={(val) => updateExperienceTitle(exp.id, title.id, 'title', val)}
placeholder="Job Title"
className="flex-1 font-medium"
/>
<Input
value={title.dates}
onChange={(val) => updateExperienceTitle(exp.id, title.id, 'dates', val)}
placeholder="Dates"
className="w-40"
/>
{exp.titles.length > 1 && (
<button
onClick={() => deleteExperienceTitle(exp.id, title.id)}
className="p-1 text-slate-500 hover:text-red-400"
>
<Trash2 size={14} />
</button>
)}
</div>
))}
<button
onClick={() => addExperienceTitle(exp.id)}
className="flex items-center gap-2 text-sm text-slate-400 hover:text-amber-400 transition-colors"
>
<Plus size={14} /> Add Title
</button>
</div>
<Textarea
value={exp.description}
onChange={(val) => updateExperience(exp.id, 'description', val)}
placeholder="Brief description of responsibilities"
rows={2}
className="text-sm"
/>
{/* Skill Buckets */}
<div className="space-y-4">
<p className="text-xs text-slate-500 uppercase tracking-wider">Skill Buckets</p>
{exp.skillBuckets.map((bucket) => (
<DropZone
key={bucket.id}
target={{ type: 'bucket', expId: exp.id, bucketId: bucket.id }}
className="p-3 bg-slate-900/50 rounded-lg space-y-2"
>
<div className="flex items-center gap-2">
<Input
value={bucket.name}
onChange={(val) => updateBucket(exp.id, bucket.id, 'name', val)}
placeholder="Skill Category Name"
className="font-medium text-amber-400"
/>
<button
onClick={() => deleteBucket(exp.id, bucket.id)}
className="p-1 text-slate-500 hover:text-red-400"
>
<Trash2 size={14} />
</button>
</div>
<div className="space-y-2">
{bucket.bullets.map((bullet) => (
<DraggableBullet
key={bullet.id}
bullet={bullet}
expId={exp.id}
bucketId={bucket.id}
onUpdate={(val) => updateBullet(exp.id, bucket.id, bullet.id, val)}
onDelete={() => deleteBullet(exp.id, bucket.id, bullet.id)}
/>
))}
<button
onClick={() => addBullet(exp.id, bucket.id)}
className="flex items-center gap-2 text-sm text-slate-400 hover:text-amber-400 transition-colors"
>
<Plus size={14} /> Add Bullet
</button>
</div>
</DropZone>
))}
<button
onClick={() => addBucket(exp.id)}
className="w-full py-2 border border-dashed border-slate-700 rounded-lg text-slate-400 hover:border-amber-500/50 hover:text-amber-400 transition-all flex items-center justify-center gap-2 text-sm"
>
<Plus size={14} /> Add Skill Bucket
</button>
</div>
</div>
))}
<button
onClick={addExperience}
className="w-full py-3 border-2 border-dashed border-slate-700 rounded-xl text-slate-400 hover:border-amber-500/50 hover:text-amber-400 transition-all flex items-center justify-center gap-2"
>
<Plus size={18} /> Add Experience
</button>
</div>
)}
{/* Additional Section */}
<SectionHeader title="ADDITIONAL" section="additional" icon={Award} />
{expandedSections.additional && (
<div className="space-y-3 pl-4">
<div className="flex items-center gap-2">
<Languages size={16} className="text-slate-500" />
<Input
value={resume.additional.languages}
onChange={(val) => updateAdditional('languages', val)}
placeholder="Languages"
/>
</div>
<div className="flex items-center gap-2">
<Users size={16} className="text-slate-500" />
<Input
value={resume.additional.volunteer}
onChange={(val) => updateAdditional('volunteer', val)}
placeholder="Volunteer Leadership"
/>
</div>
<div className="flex items-center gap-2">
<Award size={16} className="text-slate-500" />
<Input
value={resume.additional.awards}
onChange={(val) => updateAdditional('awards', val)}
placeholder="Awards"
/>
</div>
<div className="flex items-center gap-2">
<Mic size={16} className="text-slate-500" />
<Input
value={resume.additional.conferences}
onChange={(val) => updateAdditional('conferences', val)}
placeholder="Conferences"
/>
</div>
</div>
)}
</div>
{/* Live Preview Panel */}
<div className="lg:sticky lg:top-8 lg:self-start">
<div className="bg-white rounded-xl shadow-2xl overflow-hidden">
<div className="bg-slate-100 px-4 py-2 flex items-center gap-2 border-b">
<div className="w-3 h-3 rounded-full bg-red-400" />
<div className="w-3 h-3 rounded-full bg-amber-400" />
<div className="w-3 h-3 rounded-full bg-green-400" />
<span className="text-xs text-slate-500 ml-2">Live Preview</span>
</div>
<div className="p-8 text-slate-900 text-sm max-h-[75vh] overflow-y-auto" style={{ fontFamily: 'Georgia, serif' }}>
{/* Contact */}
<div className="text-center mb-4">
<h1 className="text-2xl font-bold tracking-wide">{resume.contact.name}</h1>
<p className="text-xs text-slate-600 mt-1">
{resume.contact.phone} | {resume.contact.email} | {resume.contact.linkedin}
</p>
</div>
{/* Education */}
<div className="mb-4">
<h2 className="text-sm font-bold border-b border-slate-300 pb-1 mb-2 tracking-wider">EDUCATION</h2>
{resume.education.map((edu) => (
<div key={edu.id} className="mb-3">
<div className="flex justify-between items-baseline">
<span className="font-bold text-xs">{edu.institution}</span>
<span className="text-xs text-slate-600">{edu.location}</span>
</div>
<div className="flex justify-between items-baseline">
<span className="text-xs italic">{edu.degree}{edu.details && `; ${edu.details}`}</span>
<span className="text-xs">{edu.date}</span>
</div>
{edu.bullets.map((bullet) => (
<p key={bullet.id} className="text-xs ml-4">• {bullet.text}</p>
))}
</div>
))}
</div>
{/* Experience */}
<div className="mb-4">
<h2 className="text-sm font-bold border-b border-slate-300 pb-1 mb-2 tracking-wider">EXPERIENCE</h2>
{resume.experience.map((exp) => (
<div key={exp.id} className="mb-4">
<div className="flex justify-between items-baseline">
<span className="font-bold text-xs">{exp.company}</span>
<span className="text-xs text-slate-600">{exp.location}</span>
</div>
{exp.subtitle && (
<p className="text-xs italic text-slate-600">{exp.subtitle}</p>
)}
{exp.titles.map((title, idx) => (
<div key={title.id} className="flex justify-between items-baseline">
<span className="text-xs font-medium">{title.title}</span>
<span className="text-xs">{title.dates}</span>
</div>
))}
{exp.description && (
<p className="text-xs italic mt-1">{exp.description}</p>
)}
{exp.skillBuckets.map((bucket) => (
<div key={bucket.id} className="mt-2">
<p className="text-xs font-semibold underline">{bucket.name}</p>
{bucket.bullets.map((bullet) => (
<p key={bullet.id} className="text-xs ml-4">• {bullet.text}</p>
))}
</div>
))}
</div>
))}
</div>
{/* Additional */}
<div>
<h2 className="text-sm font-bold border-b border-slate-300 pb-1 mb-2 tracking-wider">ADDITIONAL</h2>
{resume.additional.languages && (
<p className="text-xs">• <strong>Languages:</strong> {resume.additional.languages}</p>
)}
{resume.additional.volunteer && (
<p className="text-xs">• <strong>Volunteer Leadership:</strong> {resume.additional.volunteer}</p>
)}
{resume.additional.awards && (
<p className="text-xs">• <strong>Awards:</strong> {resume.additional.awards}</p>
)}
{resume.additional.conferences && (
<p className="text-xs">• <strong>Conferences:</strong> {resume.additional.conferences}</p>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Preview Tab - Full Page */}
{activeTab === 'preview' && (
<div className="max-w-3xl mx-auto">
<div className="bg-white rounded-xl shadow-2xl overflow-hidden">
<div className="p-12 text-slate-900" style={{ fontFamily: 'Georgia, serif' }}>
{/* Contact */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold tracking-wide">{resume.contact.name}</h1>
<p className="text-sm text-slate-600 mt-2">
{resume.contact.phone} | {resume.contact.email} | {resume.contact.linkedin}
</p>
</div>
{/* Education */}
<div className="mb-6">
<h2 className="font-bold border-b-2 border-slate-400 pb-1 mb-3 tracking-wider">EDUCATION</h2>
{resume.education.map((edu) => (
<div key={edu.id} className="mb-4">
<div className="flex justify-between items-baseline">
<span className="font-bold">{edu.institution}</span>
<span className="text-slate-600">{edu.location}</span>
</div>
<div className="flex justify-between items-baseline">
<span className="italic">{edu.degree}{edu.details && `; ${edu.details}`}</span>
<span>{edu.date}</span>
</div>
{edu.bullets.map((bullet) => (
<p key={bullet.id} className="ml-6 text-sm">• {bullet.text}</p>
))}
</div>
))}
</div>
{/* Experience */}
<div className="mb-6">
<h2 className="font-bold border-b-2 border-slate-400 pb-1 mb-3 tracking-wider">EXPERIENCE</h2>
{resume.experience.map((exp) => (
<div key={exp.id} className="mb-6">
<div className="flex justify-between items-baseline">
<span className="font-bold">{exp.company}</span>
<span className="text-slate-600">{exp.location}</span>
</div>
{exp.subtitle && (
<p className="italic text-slate-600 text-sm">{exp.subtitle}</p>
)}
{exp.titles.map((title) => (
<div key={title.id} className="flex justify-between items-baseline">
<span className="font-medium">{title.title}</span>
<span className="text-sm">{title.dates}</span>
</div>
))}
{exp.description && (
<p className="italic text-sm mt-1">{exp.description}</p>
)}
{exp.skillBuckets.map((bucket) => (
<div key={bucket.id} className="mt-3">
<p className="font-semibold underline text-sm">{bucket.name}</p>
{bucket.bullets.map((bullet) => (
<p key={bullet.id} className="ml-6 text-sm">• {bullet.text}</p>
))}
</div>
))}
</div>
))}
</div>
{/* Additional */}
<div>
<h2 className="font-bold border-b-2 border-slate-400 pb-1 mb-3 tracking-wider">ADDITIONAL</h2>
{resume.additional.languages && (
<p className="text-sm">• <strong>Languages:</strong> {resume.additional.languages}</p>
)}
{resume.additional.volunteer && (
<p className="text-sm">• <strong>Volunteer Leadership:</strong> {resume.additional.volunteer}</p>
)}
{resume.additional.awards && (
<p className="text-sm">• <strong>Awards:</strong> {resume.additional.awards}</p>
)}
{resume.additional.conferences && (
<p className="text-sm">• <strong>Conferences:</strong> {resume.additional.conferences}</p>
)}
</div>
</div>
</div>
</div>
)}
{/* Analyze Tab */}
{activeTab === 'analyze' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Job Description Input */}
<div className="space-y-4">
<div className="p-6 bg-slate-800/30 rounded-xl border border-slate-700/50">
<div className="flex items-center gap-3 mb-4">
<Sparkles className="text-amber-400" size={24} />
<h2 className="text-xl font-bold">Job Description Analyzer</h2>
</div>
<p className="text-slate-400 text-sm mb-4">
Paste a job description below to analyze keyword matches and get suggestions for improving your resume.
</p>
<textarea
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
placeholder="Paste the job description here..."
rows={12}
className="w-full px-4 py-3 bg-slate-900/50 border border-slate-600/50 rounded-lg text-slate-200 placeholder-slate-500 focus:outline-none focus:border-amber-500/50 focus:ring-1 focus:ring-amber-500/25 transition-all resize-none"
/>
<button
onClick={analyzeJobDescription}
className="w-full mt-4 py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-slate-900 font-semibold rounded-lg hover:from-amber-400 hover:to-orange-400 transition-all flex items-center justify-center gap-2"
>
<Sparkles size={18} />
Analyze Keywords
</button>
</div>
</div>
{/* Analysis Results */}
<div className="space-y-4">
{keywordAnalysis && (
<>
{/* Score Card */}
<div className="p-6 bg-slate-800/30 rounded-xl border border-slate-700/50">
<h3 className="text-lg font-semibold mb-4">Match Score</h3>
<div className="flex items-center gap-6">
<div className="relative w-32 h-32">
<svg className="w-full h-full transform -rotate-90">
<circle
cx="64"
cy="64"
r="56"
fill="none"
stroke="currentColor"
strokeWidth="12"
className="text-slate-700"
/>
<circle
cx="64"
cy="64"
r="56"
fill="none"
stroke="currentColor"
strokeWidth="12"
strokeDasharray={`${keywordAnalysis.score * 3.52} 352`}
className={keywordAnalysis.score >= 70 ? 'text-green-500' : keywordAnalysis.score >= 40 ? 'text-amber-500' : 'text-red-500'}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-3xl font-bold">{keywordAnalysis.score}%</span>
</div>
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<Check className="text-green-500" size={18} />
<span className="text-slate-300">{keywordAnalysis.matched} keywords matched</span>
</div>
<div className="flex items-center gap-2">
<AlertCircle className="text-amber-500" size={18} />
<span className="text-slate-300">{keywordAnalysis.missing} keywords missing</span>
</div>
</div>
</div>
</div>
{/* Missing Keywords - Draggable */}
<div className="p-6 bg-slate-800/30 rounded-xl border border-slate-700/50">
<h3 className="text-lg font-semibold mb-2">Suggested Keywords</h3>
<p className="text-slate-400 text-sm mb-4">
Drag these into your skill buckets in the Editor tab
</p>
<div className="flex flex-wrap gap-2">
{suggestedKeywords.map((keyword, idx) => (
<div
key={idx}
draggable
onDragStart={(e) => handleDragStart(e, keyword, 'keyword')}
className="px-3 py-1.5 bg-amber-500/20 border border-amber-500/50 text-amber-300 rounded-full text-sm cursor-grab hover:bg-amber-500/30 transition-all flex items-center gap-2"
>
<GripVertical size={12} />
{keyword}
</div>
))}
</div>
</div>
{/* Matched Keywords */}
<div className="p-6 bg-slate-800/30 rounded-xl border border-slate-700/50">
<h3 className="text-lg font-semibold mb-2">Already Matched</h3>
<div className="flex flex-wrap gap-2">
{keywordAnalysis.matchedWords.slice(0, 20).map((keyword, idx) => (
<span
key={idx}
className="px-3 py-1 bg-green-500/20 border border-green-500/50 text-green-300 rounded-full text-sm"
>
{keyword}
</span>
))}
{keywordAnalysis.matchedWords.length > 20 && (
<span className="px-3 py-1 text-slate-400 text-sm">
+{keywordAnalysis.matchedWords.length - 20} more
</span>
)}
</div>
</div>
</>
)}
{!keywordAnalysis && (
<div className="p-12 bg-slate-800/30 rounded-xl border border-slate-700/50 text-center">
<FileText size={48} className="mx-auto text-slate-600 mb-4" />
<p className="text-slate-400">Paste a job description and click "Analyze Keywords" to see results</p>
</div>
)}
</div>
</div>
)}
{/* Bullet Bank Tab */}
{activeTab === 'bank' && (
<div className="max-w-4xl mx-auto space-y-6">
<div className="p-6 bg-slate-800/30 rounded-xl border border-slate-700/50">
<h2 className="text-xl font-bold mb-4">Bullet Point Bank</h2>
<p className="text-slate-400 text-sm mb-6">
Store your best bullet points here. Drag them into skill buckets when building your resume.
</p>
{/* Add New Bullet */}
<div className="flex gap-3 mb-6">
<input
type="text"
value={newBullet.text}
onChange={(e) => setNewBullet(prev => ({ ...prev, text: e.target.value }))}
placeholder="Enter a bullet point..."
className="flex-1 px-4 py-3 bg-slate-900/50 border border-slate-600/50 rounded-lg text-slate-200 placeholder-slate-500 focus:outline-none focus:border-amber-500/50"
/>
<input
type="text"
value={newBullet.category}
onChange={(e) => setNewBullet(prev => ({ ...prev, category: e.target.value }))}
placeholder="Category"
className="w-40 px-4 py-3 bg-slate-900/50 border border-slate-600/50 rounded-lg text-slate-200 placeholder-slate-500 focus:outline-none focus:border-amber-500/50"
/>
<button
onClick={addToBulletBank}
className="px-6 py-3 bg-amber-500 text-slate-900 font-semibold rounded-lg hover:bg-amber-400 transition-all"
>
<Plus size={20} />
</button>
</div>
{/* Bullet List */}
<div className="space-y-3">
{bulletBank.map((bullet) => (
<div
key={bullet.id}
draggable
onDragStart={(e) => handleDragStart(e, bullet, 'bank')}
className="flex items-start gap-3 p-4 bg-slate-900/50 rounded-lg hover:bg-slate-800/50 transition-all cursor-grab group"
>
<GripVertical size={18} className="text-slate-500 mt-1 flex-shrink-0" />
<div className="flex-1">
<p className="text-slate-200">{bullet.text}</p>
<span className="text-xs text-amber-400/70 mt-1 inline-block">{bullet.category}</span>
</div>
<button
onClick={() => deleteFromBulletBank(bullet.id)}
className="p-2 text-slate-500 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
{bulletBank.length === 0 && (
<div className="text-center py-12 text-slate-500">
<FileText size={48} className="mx-auto mb-4 opacity-50" />
<p>No bullets saved yet. Add some above!</p>
</div>
)}
</div>
</div>
)}
</main>
</div>
);
}