8cd30cf5dc
- All 4 experience entries populated - Both education entries populated - Skills and languages populated - Photo upload with edit icon - LocalStorage version system - Print to PDF button - Edit/Preview tabs
717 lines
29 KiB
TypeScript
717 lines
29 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import BackToMC from "@/components/mission-control/BackToMC";
|
|
|
|
interface ResumeData {
|
|
name: string;
|
|
phone: string;
|
|
email: string;
|
|
linkedin: string;
|
|
location: string;
|
|
about: string;
|
|
education: { degree: string; school: string; year: string }[];
|
|
skills: string[];
|
|
languages: string[];
|
|
experience: {
|
|
title: string;
|
|
company: string;
|
|
period: string;
|
|
location: string;
|
|
description: string[];
|
|
}[];
|
|
photoUrl: string;
|
|
}
|
|
|
|
const initialResume: ResumeData = {
|
|
name: "HAITHAM KHALIFA",
|
|
phone: "+34 614 821 331",
|
|
email: "Haitham@Khalifa.se",
|
|
linkedin: "linkedin.com/in/haithamekhalifa/",
|
|
location: "Málaga, Spain",
|
|
about: "A results-oriented leader with 15+ years of experience in driving e-commerce growth, optimizing online platforms, and implementing successful digital marketing strategies. Proven ability to increase online sales, improve customer engagement, and manage cross-functional teams to achieve business objectives.",
|
|
education: [
|
|
{ degree: "Intensive Software Development Academy", school: "Lund University", year: "Sweden, Lund - 2019" },
|
|
{ degree: "Bachelor of Computer Science", school: "Modern Academy Maadi", year: "Egypt, Cairo - 2001 - 2006" }
|
|
],
|
|
skills: ["E-commerce Strategy", "Digital Marketing", "SEO/SEM", "Product Ownership", "Team Leadership"],
|
|
languages: ["Arabic", "English", "Swedish"],
|
|
experience: [
|
|
{
|
|
title: "Product Owner & IT Consultant",
|
|
company: "Protein.com",
|
|
period: "2020-2025",
|
|
location: "Sweden, Malmö",
|
|
description: [
|
|
"Managed product backlog to drive e-commerce sales.",
|
|
"Developed/executed SEO strategies (+20% organic traffic).",
|
|
"Implemented/managed MarTech for e-commerce performance.",
|
|
"Drove Agile practices.",
|
|
"Utilized data analytics to optimize e-commerce growth.",
|
|
"Managed product data feeds."
|
|
]
|
|
},
|
|
{
|
|
title: "Founder & CEO",
|
|
company: "HostPioneers.com",
|
|
period: "2012-2021",
|
|
location: "Denmark, Copenhagen",
|
|
description: [
|
|
"Led e-commerce platform development/optimization.",
|
|
"Implemented MarTech to drive engagement/revenue.",
|
|
"Developed/executed e-commerce marketing strategies.",
|
|
"Managed all aspects of the online business.",
|
|
"Executed e-commerce marketing (SEO, email, social).",
|
|
"Oversaw customer journey."
|
|
]
|
|
},
|
|
{
|
|
title: "Social Media Manager (Volunteer)",
|
|
company: "Danish Red Cross - Newtimes.dk",
|
|
period: "2013-2016",
|
|
location: "Denmark",
|
|
description: [
|
|
"EU-funded project to raise awareness of asylum seekers and policymakers.",
|
|
"Developed and managed the website, enhancing SEO and user engagement.",
|
|
"Wrote articles and managed social media to increase brand visibility."
|
|
]
|
|
},
|
|
{
|
|
title: "C.I.O Deputy",
|
|
company: "Cayenne Technologies",
|
|
period: "2007-2012",
|
|
location: "Egypt, Cairo",
|
|
description: [
|
|
"Web Business Manager & Deputy Chief Information Officer.",
|
|
"Managed projects in the web technology department.",
|
|
"Developed and presented web projects to clients and stakeholders.",
|
|
"Installed and supported web servers and applications.",
|
|
"Improved website traffic and SEO scores for clients."
|
|
]
|
|
}
|
|
],
|
|
photoUrl: ""
|
|
};
|
|
|
|
export default function ResumeBuilderPage() {
|
|
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
|
|
const [versions, setVersions] = useState<string[]>(["Main Resume"]);
|
|
const [currentVersion, setCurrentVersion] = useState("Main Resume");
|
|
const [resume, setResume] = useState<ResumeData>(initialResume);
|
|
const [photoPreview, setPhotoPreview] = useState<string>("");
|
|
|
|
// Load from localStorage on mount
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem('haitham_resumes_v2');
|
|
if (saved) {
|
|
try {
|
|
const data = JSON.parse(saved);
|
|
const keys = Object.keys(data);
|
|
if (keys.length > 0) {
|
|
setVersions(keys);
|
|
setCurrentVersion(keys[0]);
|
|
setResume(data[keys[0]]);
|
|
if (data[keys[0]].photoUrl) {
|
|
setPhotoPreview(data[keys[0]].photoUrl);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load saved resumes", e);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const saveToDisk = () => {
|
|
const saved = localStorage.getItem('haitham_resumes_v2');
|
|
let allResumes: Record<string, ResumeData> = {};
|
|
if (saved) {
|
|
try {
|
|
allResumes = JSON.parse(saved);
|
|
} catch (e) {}
|
|
}
|
|
allResumes[currentVersion] = { ...resume, photoUrl: photoPreview };
|
|
localStorage.setItem('haitham_resumes_v2', JSON.stringify(allResumes));
|
|
localStorage.setItem('haitham_current_profile_v2', currentVersion);
|
|
};
|
|
|
|
const loadProfile = (name: string) => {
|
|
const saved = localStorage.getItem('haitham_resumes_v2');
|
|
if (saved) {
|
|
try {
|
|
const data = JSON.parse(saved);
|
|
if (data[name]) {
|
|
setResume(data[name]);
|
|
setPhotoPreview(data[name].photoUrl || "");
|
|
setCurrentVersion(name);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
};
|
|
|
|
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const url = event.target?.result as string;
|
|
setPhotoPreview(url);
|
|
setResume({ ...resume, photoUrl: url });
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const createNewProfile = () => {
|
|
const name = prompt("Name this version (e.g., Geely IT Manager):");
|
|
if (name && !versions.includes(name)) {
|
|
const newResume = { ...resume, photoUrl: photoPreview };
|
|
const saved = localStorage.getItem('haitham_resumes_v2');
|
|
let allResumes: Record<string, ResumeData> = {};
|
|
if (saved) {
|
|
try {
|
|
allResumes = JSON.parse(saved);
|
|
} catch (e) {}
|
|
}
|
|
allResumes[name] = newResume;
|
|
localStorage.setItem('haitham_resumes_v2', JSON.stringify(allResumes));
|
|
setVersions([...versions, name]);
|
|
setCurrentVersion(name);
|
|
setResume(newResume);
|
|
} else if (name && versions.includes(name)) {
|
|
alert("This name already exists.");
|
|
}
|
|
};
|
|
|
|
const deleteProfile = () => {
|
|
if (versions.length <= 1) return alert("You need at least one profile.");
|
|
if (confirm(`Delete "${currentVersion}"?`)) {
|
|
const saved = localStorage.getItem('haitham_resumes_v2');
|
|
let allResumes: Record<string, ResumeData> = {};
|
|
if (saved) {
|
|
try {
|
|
allResumes = JSON.parse(saved);
|
|
} catch (e) {}
|
|
}
|
|
delete allResumes[currentVersion];
|
|
localStorage.setItem('haitham_resumes_v2', JSON.stringify(allResumes));
|
|
const remaining = Object.keys(allResumes);
|
|
if (remaining.length > 0) {
|
|
setVersions(remaining);
|
|
loadProfile(remaining[0]);
|
|
}
|
|
}
|
|
};
|
|
|
|
const showStatus = (msg: string) => {
|
|
alert(msg);
|
|
};
|
|
|
|
const updateExperience = (index: number, field: keyof typeof resume.experience[0], value: string) => {
|
|
const newExp = [...resume.experience];
|
|
newExp[index] = { ...newExp[index], [field]: value };
|
|
setResume({ ...resume, experience: newExp });
|
|
};
|
|
|
|
const updateEducation = (index: number, field: string, value: string) => {
|
|
const newEdu = [...resume.education];
|
|
newEdu[index] = { ...newEdu[index], [field]: value };
|
|
setResume({ ...resume, education: newEdu });
|
|
};
|
|
|
|
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 text-blue-400">Haitham's Resume Manager</h1>
|
|
<select
|
|
value={currentVersion}
|
|
onChange={(e) => loadProfile(e.target.value)}
|
|
className="bg-gray-700 text-white px-3 py-1 rounded border border-gray-600 outline-none"
|
|
>
|
|
{versions.map(v => (
|
|
<option key={v} value={v}>{v}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={createNewProfile} className="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded text-sm font-medium transition">
|
|
New Version
|
|
</button>
|
|
<button onClick={() => { saveToDisk(); showStatus('Changes Saved'); }} className="bg-green-600 hover:bg-green-700 px-4 py-1 rounded text-sm font-medium transition">
|
|
Save Changes
|
|
</button>
|
|
<button onClick={() => window.print()} className="bg-gray-600 hover:bg-gray-700 px-4 py-1 rounded text-sm font-medium transition">
|
|
Print to PDF
|
|
</button>
|
|
<button onClick={deleteProfile} className="bg-red-600 hover:bg-red-700 px-4 py-1 rounded text-sm font-medium transition">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="bg-slate-900/50 border-b border-slate-700 px-6 py-2 flex gap-4">
|
|
<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>
|
|
|
|
{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={resume.name}
|
|
onChange={(e) => setResume({...resume, 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">Phone</label>
|
|
<input
|
|
type="text"
|
|
value={resume.phone}
|
|
onChange={(e) => setResume({...resume, 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">Email</label>
|
|
<input
|
|
type="email"
|
|
value={resume.email}
|
|
onChange={(e) => setResume({...resume, 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">LinkedIn</label>
|
|
<input
|
|
type="text"
|
|
value={resume.linkedin}
|
|
onChange={(e) => setResume({...resume, linkedin: e.target.value})}
|
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="block text-sm text-slate-400 mb-1">Location</label>
|
|
<input
|
|
type="text"
|
|
value={resume.location}
|
|
onChange={(e) => setResume({...resume, location: e.target.value})}
|
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="block text-sm text-slate-400 mb-1">About Me</label>
|
|
<textarea
|
|
value={resume.about}
|
|
onChange={(e) => setResume({...resume, about: e.target.value})}
|
|
rows={4}
|
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Photo Upload */}
|
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
|
<h2 className="text-lg font-bold mb-4 text-blue-400">📷 Photo</h2>
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative">
|
|
<div className="w-44 h-44 rounded-full border-8 border-gray-300 overflow-hidden bg-gray-200">
|
|
{photoPreview ? (
|
|
<img src={photoPreview} alt="Profile" className="w-full h-full object-cover" />
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center text-gray-400 text-4xl">📷</div>
|
|
)}
|
|
</div>
|
|
<label className="absolute bottom-0 right-0 w-10 h-10 bg-blue-600 hover:bg-blue-500 rounded-full flex items-center justify-center cursor-pointer shadow-lg">
|
|
<span className="text-xl">✏️</span>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handlePhotoUpload}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div className="text-sm text-slate-400">
|
|
<p>Click the pencil icon to upload your photo</p>
|
|
<p>Recommended: Square image, at least 300x300px</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Experience */}
|
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
|
<h2 className="text-lg font-bold mb-4 text-blue-400">💼 Experience</h2>
|
|
<div className="space-y-4">
|
|
{resume.experience.map((exp, index) => (
|
|
<div key={index} 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">Title</label>
|
|
<input
|
|
type="text"
|
|
value={exp.title}
|
|
onChange={(e) => updateExperience(index, "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(index, "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>
|
|
<label className="block text-sm text-slate-400 mb-1">Period</label>
|
|
<input
|
|
type="text"
|
|
value={exp.period}
|
|
onChange={(e) => updateExperience(index, "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>
|
|
<label className="block text-sm text-slate-400 mb-1">Location</label>
|
|
<input
|
|
type="text"
|
|
value={exp.location}
|
|
onChange={(e) => updateExperience(index, "location", 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 (one bullet per line)</label>
|
|
<textarea
|
|
value={exp.description.join('\n')}
|
|
onChange={(e) => updateExperience(index, "description", e.target.value.split('\n'))}
|
|
rows={5}
|
|
className="w-full bg-slate-600 border border-slate-500 rounded-lg px-3 py-2 text-white text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Education */}
|
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
|
<h2 className="text-lg font-bold mb-4 text-blue-400">🎓 Education</h2>
|
|
<div className="space-y-4">
|
|
{resume.education.map((edu, index) => (
|
|
<div key={index} className="bg-slate-700/50 rounded-lg p-4 border border-slate-600">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm text-slate-400 mb-1">Degree</label>
|
|
<input
|
|
type="text"
|
|
value={edu.degree}
|
|
onChange={(e) => updateEducation(index, "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">Year</label>
|
|
<input
|
|
type="text"
|
|
value={edu.year}
|
|
onChange={(e) => updateEducation(index, "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 className="col-span-3">
|
|
<label className="block text-sm text-slate-400 mb-1">School</label>
|
|
<input
|
|
type="text"
|
|
value={edu.school}
|
|
onChange={(e) => updateEducation(index, "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>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Skills */}
|
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
|
<h2 className="text-lg font-bold mb-4 text-blue-400">🛠️ Skills</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
{resume.skills.map((skill, index) => (
|
|
<div key={index} className="flex items-center gap-2 bg-slate-700 px-3 py-1 rounded-lg">
|
|
<input
|
|
type="text"
|
|
value={skill}
|
|
onChange={(e) => {
|
|
const newSkills = [...resume.skills];
|
|
newSkills[index] = e.target.value;
|
|
setResume({...resume, skills: newSkills});
|
|
}}
|
|
className="bg-transparent border-none text-white text-sm w-32"
|
|
/>
|
|
<button
|
|
onClick={() => setResume({...resume, skills: resume.skills.filter((_, i) => i !== index)})}
|
|
className="text-slate-400 hover:text-red-400 text-lg"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
))}
|
|
<button
|
|
onClick={() => setResume({...resume, skills: [...resume.skills, "New Skill"]})}
|
|
className="bg-blue-600 hover:bg-blue-500 px-3 py-1 rounded-lg text-sm"
|
|
>
|
|
+ Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Languages */}
|
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
|
<h2 className="text-lg font-bold mb-4 text-blue-400">🌍 Languages</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
{resume.languages.map((lang, index) => (
|
|
<div key={index} className="flex items-center gap-2 bg-slate-700 px-3 py-1 rounded-lg">
|
|
<input
|
|
type="text"
|
|
value={lang}
|
|
onChange={(e) => {
|
|
const newLangs = [...resume.languages];
|
|
newLangs[index] = e.target.value;
|
|
setResume({...resume, languages: newLangs});
|
|
}}
|
|
className="bg-transparent border-none text-white text-sm w-24"
|
|
/>
|
|
<button
|
|
onClick={() => setResume({...resume, languages: resume.languages.filter((_, i) => i !== index)})}
|
|
className="text-slate-400 hover:text-red-400 text-lg"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
))}
|
|
<button
|
|
onClick={() => setResume({...resume, languages: [...resume.languages, "New Language"]})}
|
|
className="bg-blue-600 hover:bg-blue-500 px-3 py-1 rounded-lg text-sm"
|
|
>
|
|
+ Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* Resume Preview */
|
|
<div className="p-8">
|
|
<style>{`
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&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: 'Inter', sans-serif;
|
|
}
|
|
.sidebar {
|
|
width: 280px;
|
|
background-color: #f3f3f3;
|
|
padding: 2.5rem 1.5rem;
|
|
}
|
|
.main-content {
|
|
flex: 1;
|
|
padding: 3.5rem 3rem;
|
|
background: white;
|
|
}
|
|
.circle-frame {
|
|
width: 176px;
|
|
height: 176px;
|
|
border-radius: 9999px;
|
|
border: 10px solid #e2e2e2;
|
|
overflow: hidden;
|
|
margin: 0 auto 2rem;
|
|
background: #d1d1d1;
|
|
}
|
|
.circle-frame img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.section-header {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: #1a1a1a;
|
|
border-bottom: 1px solid #ccc;
|
|
padding-bottom: 4px;
|
|
margin-bottom: 12px;
|
|
text-transform: capitalize;
|
|
}
|
|
.main-header {
|
|
font-size: 24px;
|
|
font-weight: 800;
|
|
letter-spacing: 0.05em;
|
|
color: #1a1a1a;
|
|
border-bottom: 2px solid #1a1a1a;
|
|
padding-bottom: 8px;
|
|
margin-bottom: 24px;
|
|
text-transform: uppercase;
|
|
}
|
|
.experience-item {
|
|
position: relative;
|
|
padding-left: 32px;
|
|
border-left: 2px solid #e5e5e5;
|
|
padding-bottom: 24px;
|
|
}
|
|
.experience-dot {
|
|
position: absolute;
|
|
left: -9px;
|
|
top: 4px;
|
|
width: 16px;
|
|
height: 16px;
|
|
background-color: #333;
|
|
border: 3px solid white;
|
|
border-radius: 9999px;
|
|
z-index: 10;
|
|
}
|
|
@media print {
|
|
body { background: white; }
|
|
.resume-preview { box-shadow: none; margin: 0; }
|
|
}
|
|
`}</style>
|
|
|
|
<div className="resume-preview">
|
|
{/* Sidebar */}
|
|
<div className="sidebar">
|
|
<div className="circle-frame">
|
|
{photoPreview ? (
|
|
<img src={photoPreview} alt={resume.name} />
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center text-gray-400">📷</div>
|
|
)}
|
|
</div>
|
|
|
|
<h1 className="text-2xl font-black text-center text-gray-900 mb-8 tracking-tighter uppercase leading-none">
|
|
{resume.name}
|
|
</h1>
|
|
|
|
{/* Contact Section */}
|
|
<div className="space-y-3 text-[11px] text-gray-700 mb-10">
|
|
<div className="flex items-center gap-3">
|
|
<span className="w-5 h-5 flex items-center justify-center bg-gray-800 text-white rounded-full text-[10px]">📞</span>
|
|
<span className="font-medium">{resume.phone}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="w-5 h-5 flex items-center justify-center bg-gray-800 text-white rounded-full text-[10px]">✉️</span>
|
|
<span className="font-medium">{resume.email}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="w-5 h-5 flex items-center justify-center bg-gray-800 text-white rounded-full text-[10px] font-bold">in</span>
|
|
<span className="font-medium">{resume.linkedin}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="w-5 h-5 flex items-center justify-center bg-gray-800 text-white rounded-full text-[10px]">📍</span>
|
|
<span className="font-medium">{resume.location}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* About Me */}
|
|
<div className="mb-10">
|
|
<h2 className="section-header">About Me</h2>
|
|
<div className="text-[10px] leading-relaxed text-gray-600 font-medium text-justify">
|
|
{resume.about}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Skills Section */}
|
|
<div className="mb-10">
|
|
<h2 className="section-header">Skills</h2>
|
|
<div className="text-[11px] text-gray-700 space-y-1 font-medium">
|
|
{resume.skills.map((skill, i) => (
|
|
<p key={i}>• {skill}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Languages Section */}
|
|
<div>
|
|
<h2 className="section-header">Language</h2>
|
|
<div className="text-[11px] text-gray-700 space-y-1 font-medium">
|
|
{resume.languages.map((lang, i) => (
|
|
<p key={i}>• {lang}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="main-content">
|
|
<h1 className="main-header">{resume.name}</h1>
|
|
|
|
{/* Experience */}
|
|
<div className="mb-10">
|
|
<h2 className="section-header">Experience</h2>
|
|
{resume.experience.map((exp, i) => (
|
|
<div key={i} className="experience-item">
|
|
<div className="experience-dot" />
|
|
<div className="flex justify-between items-baseline mb-1">
|
|
<h3 className="text-lg font-bold text-gray-800 leading-none">{exp.title}</h3>
|
|
<span className="text-xs font-bold text-gray-400">{exp.period}</span>
|
|
</div>
|
|
<p className="text-sm font-bold text-blue-600 mb-3">{exp.company} | {exp.location}</p>
|
|
<div className="text-[11px] text-gray-600 space-y-1.5 leading-snug">
|
|
{exp.description.map((desc, j) => (
|
|
<p key={j}>• {desc}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Education */}
|
|
<div>
|
|
<h2 className="section-header">Education</h2>
|
|
{resume.education.map((edu, i) => (
|
|
<div key={i} className="mb-4">
|
|
<p className="text-[11px] font-bold text-gray-800 leading-tight">{edu.degree}</p>
|
|
<p className="text-[10px] text-gray-600">{edu.school}</p>
|
|
<p className="text-[9px] text-gray-400 font-bold">{edu.year}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|