29312444d0
- Two-column resume layout (sidebar + main) - Edit mode with forms for all sections - Preview mode with live resume preview - Version saving system - Print/Download functionality - Personal info, Experience, Education, Skills, Languages
581 lines
22 KiB
TypeScript
581 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import BackToMC from "@/components/mission-control/BackToMC";
|
||
|
||
interface Experience {
|
||
id: string;
|
||
title: string;
|
||
company: string;
|
||
period: string;
|
||
description: string;
|
||
}
|
||
|
||
interface Education {
|
||
id: string;
|
||
degree: string;
|
||
school: string;
|
||
year: string;
|
||
}
|
||
|
||
interface Skill {
|
||
id: string;
|
||
name: string;
|
||
}
|
||
|
||
export default function ResumeBuilderPage() {
|
||
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
|
||
const [resumeName, setResumeName] = useState("My Resume");
|
||
|
||
// Personal Info
|
||
const [personalInfo, setPersonalInfo] = useState({
|
||
name: "Your Name",
|
||
title: "Professional Title",
|
||
email: "email@example.com",
|
||
phone: "+34 XXX XXX XXX",
|
||
location: "City, Country",
|
||
website: "www.yoursite.com",
|
||
linkedin: "linkedin.com/in/yourprofile",
|
||
summary: "A brief professional summary..."
|
||
});
|
||
|
||
// Experience
|
||
const [experiences, setExperiences] = useState<Experience[]>([
|
||
{
|
||
id: "1",
|
||
title: "Job Title",
|
||
company: "Company Name",
|
||
period: "2020 - Present",
|
||
description: "Key responsibilities and achievements in this role."
|
||
}
|
||
]);
|
||
|
||
// Education
|
||
const [education, setEducation] = useState<Education[]>([
|
||
{
|
||
id: "1",
|
||
degree: "Degree Name",
|
||
school: "University Name",
|
||
year: "2016 - 2020"
|
||
}
|
||
]);
|
||
|
||
// Skills
|
||
const [skills, setSkills] = useState<Skill[]>([
|
||
{ id: "1", name: "Skill 1" },
|
||
{ id: "2", name: "Skill 2" },
|
||
{ id: "3", name: "Skill 3" }
|
||
]);
|
||
|
||
const [languages, setLanguages] = useState([
|
||
{ id: "1", name: "Language 1", level: "Fluent" },
|
||
{ id: "2", name: "Language 2", level: "Native" }
|
||
]);
|
||
|
||
// Versions
|
||
const [versions, setVersions] = useState<string[]>(["Default"]);
|
||
const [currentVersion, setCurrentVersion] = useState("Default");
|
||
|
||
// CRUD operations
|
||
const addExperience = () => {
|
||
setExperiences([...experiences, {
|
||
id: Date.now().toString(),
|
||
title: "New Position",
|
||
company: "Company",
|
||
period: "2023 - Present",
|
||
description: "Description..."
|
||
}]);
|
||
};
|
||
|
||
const removeExperience = (id: string) => {
|
||
setExperiences(experiences.filter(e => e.id !== id));
|
||
};
|
||
|
||
const updateExperience = (id: string, field: keyof Experience, value: string) => {
|
||
setExperiences(experiences.map(e => e.id === id ? { ...e, [field]: value } : e));
|
||
};
|
||
|
||
const addEducation = () => {
|
||
setEducation([...education, {
|
||
id: Date.now().toString(),
|
||
degree: "New Degree",
|
||
school: "School Name",
|
||
year: "2020 - 2024"
|
||
}]);
|
||
};
|
||
|
||
const removeEducation = (id: string) => {
|
||
setEducation(education.filter(e => e.id !== id));
|
||
};
|
||
|
||
const updateEducation = (id: string, field: keyof Education, value: string) => {
|
||
setEducation(education.map(e => e.id === id ? { ...e, [field]: value } : e));
|
||
};
|
||
|
||
const addSkill = () => {
|
||
setSkills([...skills, { id: Date.now().toString(), name: "New Skill" }]);
|
||
};
|
||
|
||
const removeSkill = (id: string) => {
|
||
setSkills(skills.filter(s => s.id !== id));
|
||
};
|
||
|
||
const updateSkill = (id: string, name: string) => {
|
||
setSkills(skills.map(s => s.id === id ? { ...s, name } : s));
|
||
};
|
||
|
||
const saveVersion = () => {
|
||
const name = prompt("Enter version name:", `Resume - ${currentVersion}`);
|
||
if (name && !versions.includes(name)) {
|
||
setVersions([...versions, name]);
|
||
setCurrentVersion(name);
|
||
alert(`Version "${name}" saved!`);
|
||
}
|
||
};
|
||
|
||
const loadVersion = (version: string) => {
|
||
setCurrentVersion(version);
|
||
};
|
||
|
||
const deleteVersion = (version: string) => {
|
||
if (versions.length > 1) {
|
||
setVersions(versions.filter(v => v !== version));
|
||
if (currentVersion === version) {
|
||
setCurrentVersion(versions[0]);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handlePrint = () => {
|
||
window.print();
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-950 text-white">
|
||
<BackToMC />
|
||
|
||
{/* Toolbar */}
|
||
<div className="sticky top-0 z-50 bg-slate-800 border-b border-slate-700 px-6 py-3 flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
<h1 className="text-xl font-bold">📄 Resume Builder</h1>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setActiveTab("edit")}
|
||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||
activeTab === "edit" ? "bg-blue-600 text-white" : "bg-slate-700 text-slate-300"
|
||
}`}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("preview")}
|
||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||
activeTab === "preview" ? "bg-blue-600 text-white" : "bg-slate-700 text-slate-300"
|
||
}`}
|
||
>
|
||
Preview
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<select
|
||
value={currentVersion}
|
||
onChange={(e) => loadVersion(e.target.value)}
|
||
className="bg-slate-700 text-white px-3 py-2 rounded-lg text-sm"
|
||
>
|
||
{versions.map(v => (
|
||
<option key={v} value={v}>{v}</option>
|
||
))}
|
||
</select>
|
||
<button
|
||
onClick={saveVersion}
|
||
className="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||
>
|
||
💾 Save Version
|
||
</button>
|
||
<button
|
||
onClick={handlePrint}
|
||
className="bg-slate-600 hover:bg-slate-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||
>
|
||
🖨️ Print/Download
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{activeTab === "edit" ? (
|
||
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
||
{/* Personal Info */}
|
||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||
<h2 className="text-lg font-bold mb-4 text-blue-400">👤 Personal Information</h2>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Full Name</label>
|
||
<input
|
||
type="text"
|
||
value={personalInfo.name}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, name: e.target.value})}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Professional Title</label>
|
||
<input
|
||
type="text"
|
||
value={personalInfo.title}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, title: e.target.value})}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Email</label>
|
||
<input
|
||
type="email"
|
||
value={personalInfo.email}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, email: e.target.value})}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Phone</label>
|
||
<input
|
||
type="tel"
|
||
value={personalInfo.phone}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, phone: e.target.value})}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Location</label>
|
||
<input
|
||
type="text"
|
||
value={personalInfo.location}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, location: e.target.value})}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Website</label>
|
||
<input
|
||
type="text"
|
||
value={personalInfo.website}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, website: e.target.value})}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4">
|
||
<label className="block text-sm text-slate-400 mb-1">Professional Summary</label>
|
||
<textarea
|
||
value={personalInfo.summary}
|
||
onChange={(e) => setPersonalInfo({...personalInfo, summary: e.target.value})}
|
||
rows={3}
|
||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Experience */}
|
||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-lg font-bold text-blue-400">💼 Experience</h2>
|
||
<button
|
||
onClick={addExperience}
|
||
className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded-lg text-sm"
|
||
>
|
||
+ Add
|
||
</button>
|
||
</div>
|
||
<div className="space-y-4">
|
||
{experiences.map((exp) => (
|
||
<div key={exp.id} className="bg-slate-700/50 rounded-lg p-4 border border-slate-600">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Job Title</label>
|
||
<input
|
||
type="text"
|
||
value={exp.title}
|
||
onChange={(e) => updateExperience(exp.id, "title", e.target.value)}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Company</label>
|
||
<input
|
||
type="text"
|
||
value={exp.company}
|
||
onChange={(e) => updateExperience(exp.id, "company", e.target.value)}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<label className="block text-sm text-slate-400 mb-1">Period</label>
|
||
<input
|
||
type="text"
|
||
value={exp.period}
|
||
onChange={(e) => updateExperience(exp.id, "period", e.target.value)}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<label className="block text-sm text-slate-400 mb-1">Description</label>
|
||
<textarea
|
||
value={exp.description}
|
||
onChange={(e) => updateExperience(exp.id, "description", e.target.value)}
|
||
rows={3}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => removeExperience(exp.id)}
|
||
className="mt-2 text-red-400 text-sm hover:text-red-300"
|
||
>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Education */}
|
||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-lg font-bold text-blue-400">🎓 Education</h2>
|
||
<button
|
||
onClick={addEducation}
|
||
className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded-lg text-sm"
|
||
>
|
||
+ Add
|
||
</button>
|
||
</div>
|
||
<div className="space-y-4">
|
||
{education.map((edu) => (
|
||
<div key={edu.id} className="bg-slate-700/50 rounded-lg p-4 border border-slate-600">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Degree</label>
|
||
<input
|
||
type="text"
|
||
value={edu.degree}
|
||
onChange={(e) => updateEducation(edu.id, "degree", e.target.value)}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">School</label>
|
||
<input
|
||
type="text"
|
||
value={edu.school}
|
||
onChange={(e) => updateEducation(edu.id, "school", e.target.value)}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-slate-400 mb-1">Year</label>
|
||
<input
|
||
type="text"
|
||
value={edu.year}
|
||
onChange={(e) => updateEducation(edu.id, "year", e.target.value)}
|
||
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => removeEducation(edu.id)}
|
||
className="mt-2 text-red-400 text-sm hover:text-red-300"
|
||
>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Skills */}
|
||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-lg font-bold text-blue-400">🛠️ Skills</h2>
|
||
<button
|
||
onClick={addSkill}
|
||
className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded-lg text-sm"
|
||
>
|
||
+ Add
|
||
</button>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{skills.map((skill) => (
|
||
<div key={skill.id} className="flex items-center gap-2 bg-slate-700 px-3 py-1 rounded-lg">
|
||
<input
|
||
type="text"
|
||
value={skill.name}
|
||
onChange={(e) => updateSkill(skill.id, e.target.value)}
|
||
className="bg-transparent border-none text-white text-sm w-24"
|
||
/>
|
||
<button
|
||
onClick={() => removeSkill(skill.id)}
|
||
className="text-slate-400 hover:text-red-400 text-lg"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Languages */}
|
||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||
<h2 className="text-lg font-bold text-blue-400 mb-4">🌍 Languages</h2>
|
||
<div className="flex flex-wrap gap-2">
|
||
{languages.map((lang) => (
|
||
<div key={lang.id} className="bg-slate-700 px-3 py-2 rounded-lg text-sm">
|
||
{lang.name} - {lang.level}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Resume Preview */
|
||
<div className="p-8">
|
||
<style>{`
|
||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
|
||
.resume-preview {
|
||
width: 850px;
|
||
min-height: 1100px;
|
||
background: white;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
margin: 0 auto;
|
||
display: flex;
|
||
font-family: 'Roboto', sans-serif;
|
||
}
|
||
.resume-sidebar {
|
||
width: 280px;
|
||
background-color: #f3f3f3;
|
||
padding: 2.5rem 1.5rem;
|
||
}
|
||
.resume-main {
|
||
flex: 1;
|
||
padding: 3.5rem 3rem;
|
||
background: white;
|
||
}
|
||
.resume-photo {
|
||
width: 176px;
|
||
height: 176px;
|
||
border-radius: 9999px;
|
||
border: 10px solid #e2e2e2;
|
||
background: #d1d1d1;
|
||
margin: 0 auto 2rem;
|
||
}
|
||
.resume-section-header {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
color: #1a1a1a;
|
||
border-bottom: 1px solid #ccc;
|
||
padding-bottom: 4px;
|
||
margin-bottom: 12px;
|
||
text-transform: capitalize;
|
||
}
|
||
.resume-main-header {
|
||
font-size: 1.6rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.15em;
|
||
color: #1a1a1a;
|
||
border-bottom: 2px solid #1a1a1a;
|
||
padding-bottom: 8px;
|
||
margin-bottom: 24px;
|
||
text-transform: uppercase;
|
||
}
|
||
@media print {
|
||
body { background: white; }
|
||
.resume-preview { box-shadow: none; margin: 0; }
|
||
}
|
||
`}</style>
|
||
|
||
<div className="resume-preview">
|
||
{/* Sidebar */}
|
||
<div className="resume-sidebar">
|
||
<div className="resume-photo" />
|
||
|
||
{/* Contact Section */}
|
||
<div className="mb-6">
|
||
<h3 className="resume-section-header">Contact</h3>
|
||
<div className="space-y-2 text-sm text-gray-700">
|
||
<p>📧 {personalInfo.email}</p>
|
||
<p>📱 {personalInfo.phone}</p>
|
||
<p>📍 {personalInfo.location}</p>
|
||
<p>🌐 {personalInfo.website}</p>
|
||
<p>💼 {personalInfo.linkedin}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Skills Section */}
|
||
<div className="mb-6">
|
||
<h3 className="resume-section-header">Skills</h3>
|
||
<div className="space-y-1">
|
||
{skills.map((skill) => (
|
||
<div key={skill.id} className="text-sm text-gray-700">
|
||
• {skill.name}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Languages Section */}
|
||
<div className="mb-6">
|
||
<h3 className="resume-section-header">Languages</h3>
|
||
<div className="space-y-1">
|
||
{languages.map((lang) => (
|
||
<div key={lang.id} className="text-sm text-gray-700">
|
||
• {lang.name} ({lang.level})
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Content */}
|
||
<div className="resume-main">
|
||
<h1 className="resume-main-header">{personalInfo.name}</h1>
|
||
<p className="text-lg text-gray-600 mb-6 uppercase tracking-wide">{personalInfo.title}</p>
|
||
|
||
{/* Summary */}
|
||
<div className="mb-6">
|
||
<h3 className="resume-section-header">Profile</h3>
|
||
<p className="text-sm text-gray-700 leading-relaxed">{personalInfo.summary}</p>
|
||
</div>
|
||
|
||
{/* Experience */}
|
||
<div className="mb-6">
|
||
<h3 className="resume-section-header">Experience</h3>
|
||
{experiences.map((exp) => (
|
||
<div key={exp.id} className="mb-4">
|
||
<div className="flex justify-between items-start">
|
||
<div>
|
||
<h4 className="font-bold text-gray-800">{exp.title}</h4>
|
||
<p className="text-gray-600">{exp.company}</p>
|
||
</div>
|
||
<span className="text-sm text-gray-500">{exp.period}</span>
|
||
</div>
|
||
<p className="text-sm text-gray-700 mt-2 leading-relaxed">{exp.description}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Education */}
|
||
<div>
|
||
<h3 className="resume-section-header">Education</h3>
|
||
{education.map((edu) => (
|
||
<div key={edu.id} className="mb-3">
|
||
<h4 className="font-bold text-gray-800">{edu.degree}</h4>
|
||
<p className="text-gray-600">{edu.school} • {edu.year}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|