feat(resume): use Haitham's actual data and design
- 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
This commit is contained in:
+355
-310
@@ -1,171 +1,221 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import BackToMC from "@/components/mission-control/BackToMC";
|
||||
|
||||
interface Experience {
|
||||
id: string;
|
||||
title: string;
|
||||
company: string;
|
||||
period: string;
|
||||
location: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Education {
|
||||
id: string;
|
||||
degree: string;
|
||||
school: string;
|
||||
year: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
interface Skill {
|
||||
id: string;
|
||||
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;
|
||||
}
|
||||
|
||||
export default function ResumeBuilderPage() {
|
||||
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
|
||||
|
||||
// Personal Info
|
||||
const [personalInfo, setPersonalInfo] = useState({
|
||||
name: "HAITHAM KHALIFA",
|
||||
title: "Senior Full-Stack Developer & Entrepreneur",
|
||||
email: "Haitham@Khalifa.se",
|
||||
phone: "+34 614 821 331",
|
||||
location: "Málaga, Spain",
|
||||
linkedin: "linkedin.com/in/haithamekhalifa/",
|
||||
website: "haithamkhalifa.com",
|
||||
summary: "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."
|
||||
});
|
||||
|
||||
// Experience
|
||||
const [experiences, setExperiences] = useState<Experience[]>([
|
||||
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: [
|
||||
{
|
||||
id: "1",
|
||||
title: "Product Owner & IT Consultant",
|
||||
company: "Protein.com",
|
||||
period: "2020-2025",
|
||||
location: "Sweden, Malmö",
|
||||
description: "• Managed product backlog to drive e-commerce sales.\n• Developed/executed SEO strategies (+20% organic traffic).\n• Implemented/managed MarTech for e-commerce performance.\n• Drove Agile practices.\n• Utilized data analytics to optimize e-commerce growth.\n• Managed product data feeds."
|
||||
}
|
||||
]);
|
||||
|
||||
// Education
|
||||
const [education, setEducation] = useState<Education[]>([
|
||||
{
|
||||
id: "1",
|
||||
degree: "Intensive Software Development Academy",
|
||||
school: "Lund University",
|
||||
year: "2019",
|
||||
location: "Sweden, Lund"
|
||||
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."
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
degree: "Bachelor of Computer Science",
|
||||
school: "Modern Academy Maadi",
|
||||
year: "2001 - 2006",
|
||||
location: "Egypt, Cairo"
|
||||
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: ""
|
||||
};
|
||||
|
||||
// Skills
|
||||
const [skills, setSkills] = useState<Skill[]>([
|
||||
{ id: "1", name: "E-commerce Strategy" },
|
||||
{ id: "2", name: "Digital Marketing" },
|
||||
{ id: "3", name: "SEO/SEM" },
|
||||
{ id: "4", name: "Product Ownership" },
|
||||
{ id: "5", name: "Team Leadership" }
|
||||
]);
|
||||
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>("");
|
||||
|
||||
// Languages
|
||||
const [languages, setLanguages] = useState([
|
||||
{ id: "1", name: "Arabic" },
|
||||
{ id: "2", name: "English" },
|
||||
{ id: "3", name: "Swedish" }
|
||||
]);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 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",
|
||||
location: "City, Country",
|
||||
description: "• Key responsibility.\n• Achievement."
|
||||
}]);
|
||||
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 removeExperience = (id: string) => {
|
||||
setExperiences(experiences.filter(e => e.id !== id));
|
||||
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 updateExperience = (id: string, field: keyof Experience, value: string) => {
|
||||
setExperiences(experiences.map(e => e.id === id ? { ...e, [field]: value } : 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 addEducation = () => {
|
||||
setEducation([...education, {
|
||||
id: Date.now().toString(),
|
||||
degree: "New Degree",
|
||||
school: "University",
|
||||
year: "2020 - 2024",
|
||||
location: "City, Country"
|
||||
}]);
|
||||
};
|
||||
|
||||
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 addLanguage = () => {
|
||||
setLanguages([...languages, { id: Date.now().toString(), name: "New Language" }]);
|
||||
};
|
||||
|
||||
const removeLanguage = (id: string) => {
|
||||
setLanguages(languages.filter(l => l.id !== id));
|
||||
};
|
||||
|
||||
const updateLanguage = (id: string, name: string) => {
|
||||
setLanguages(languages.map(l => l.id === id ? { ...l, name } : l));
|
||||
};
|
||||
|
||||
const saveVersion = () => {
|
||||
const name = prompt("Enter version name:", `Resume - ${currentVersion}`);
|
||||
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 loadVersion = (version: string) => {
|
||||
setCurrentVersion(version);
|
||||
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 handlePrint = () => {
|
||||
window.print();
|
||||
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 (
|
||||
@@ -176,50 +226,52 @@ export default function ResumeBuilderPage() {
|
||||
<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>
|
||||
<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"
|
||||
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>
|
||||
<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
|
||||
</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={handlePrint}
|
||||
className="bg-slate-600 hover:bg-slate-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
🖨️ Print to PDF
|
||||
<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 */}
|
||||
@@ -230,17 +282,17 @@ export default function ResumeBuilderPage() {
|
||||
<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})}
|
||||
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">Professional Title</label>
|
||||
<label className="block text-sm text-slate-400 mb-1">Phone</label>
|
||||
<input
|
||||
type="text"
|
||||
value={personalInfo.title}
|
||||
onChange={(e) => setPersonalInfo({...personalInfo, title: e.target.value})}
|
||||
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>
|
||||
@@ -248,26 +300,8 @@ export default function ResumeBuilderPage() {
|
||||
<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})}
|
||||
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>
|
||||
@@ -275,44 +309,74 @@ export default function ResumeBuilderPage() {
|
||||
<label className="block text-sm text-slate-400 mb-1">LinkedIn</label>
|
||||
<input
|
||||
type="text"
|
||||
value={personalInfo.linkedin}
|
||||
onChange={(e) => setPersonalInfo({...personalInfo, linkedin: e.target.value})}
|
||||
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 className="mt-4">
|
||||
<label className="block text-sm text-slate-400 mb-1">About Me</label>
|
||||
<textarea
|
||||
value={personalInfo.summary}
|
||||
onChange={(e) => setPersonalInfo({...personalInfo, summary: e.target.value})}
|
||||
rows={4}
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white"
|
||||
/>
|
||||
</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">
|
||||
<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>
|
||||
<h2 className="text-lg font-bold mb-4 text-blue-400">💼 Experience</h2>
|
||||
<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">
|
||||
{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">Job Title</label>
|
||||
<label className="block text-sm text-slate-400 mb-1">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={exp.title}
|
||||
onChange={(e) => updateExperience(exp.id, "title", e.target.value)}
|
||||
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>
|
||||
@@ -321,7 +385,7 @@ export default function ResumeBuilderPage() {
|
||||
<input
|
||||
type="text"
|
||||
value={exp.company}
|
||||
onChange={(e) => updateExperience(exp.id, "company", e.target.value)}
|
||||
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>
|
||||
@@ -330,7 +394,7 @@ export default function ResumeBuilderPage() {
|
||||
<input
|
||||
type="text"
|
||||
value={exp.period}
|
||||
onChange={(e) => updateExperience(exp.id, "period", e.target.value)}
|
||||
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>
|
||||
@@ -339,26 +403,20 @@ export default function ResumeBuilderPage() {
|
||||
<input
|
||||
type="text"
|
||||
value={exp.location}
|
||||
onChange={(e) => updateExperience(exp.id, "location", e.target.value)}
|
||||
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, start with •)</label>
|
||||
<label className="block text-sm text-slate-400 mb-1">Description (one bullet per line)</label>
|
||||
<textarea
|
||||
value={exp.description}
|
||||
onChange={(e) => updateExperience(exp.id, "description", e.target.value)}
|
||||
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>
|
||||
<button
|
||||
onClick={() => removeExperience(exp.id)}
|
||||
className="mt-2 text-red-400 text-sm hover:text-red-300"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -366,34 +424,17 @@ export default function ResumeBuilderPage() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-lg font-bold mb-4 text-blue-400">🎓 Education</h2>
|
||||
<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>
|
||||
{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(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)}
|
||||
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>
|
||||
@@ -402,26 +443,20 @@ export default function ResumeBuilderPage() {
|
||||
<input
|
||||
type="text"
|
||||
value={edu.year}
|
||||
onChange={(e) => updateEducation(edu.id, "year", e.target.value)}
|
||||
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>
|
||||
<label className="block text-sm text-slate-400 mb-1">Location</label>
|
||||
<div className="col-span-3">
|
||||
<label className="block text-sm text-slate-400 mb-1">School</label>
|
||||
<input
|
||||
type="text"
|
||||
value={edu.location}
|
||||
onChange={(e) => updateEducation(edu.id, "location", e.target.value)}
|
||||
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>
|
||||
<button
|
||||
onClick={() => removeEducation(edu.id)}
|
||||
className="mt-2 text-red-400 text-sm hover:text-red-300"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -429,68 +464,72 @@ export default function ResumeBuilderPage() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-lg font-bold mb-4 text-blue-400">🛠️ Skills</h2>
|
||||
<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">
|
||||
{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.name}
|
||||
onChange={(e) => updateSkill(skill.id, e.target.value)}
|
||||
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={() => removeSkill(skill.id)}
|
||||
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">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-blue-400">🌍 Languages</h2>
|
||||
<button
|
||||
onClick={addLanguage}
|
||||
className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded-lg text-sm"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold mb-4 text-blue-400">🌍 Languages</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{languages.map((lang) => (
|
||||
<div key={lang.id} className="flex items-center gap-2 bg-slate-700 px-3 py-1 rounded-lg">
|
||||
{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.name}
|
||||
onChange={(e) => updateLanguage(lang.id, e.target.value)}
|
||||
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={() => removeLanguage(lang.id)}
|
||||
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 - Haitham's Design */
|
||||
/* 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');
|
||||
@@ -573,30 +612,34 @@ export default function ResumeBuilderPage() {
|
||||
{/* Sidebar */}
|
||||
<div className="sidebar">
|
||||
<div className="circle-frame">
|
||||
<img src="Resume.jpg" alt={personalInfo.name} onError="this.src='https://via.placeholder.com/176?text=Photo'" />
|
||||
{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">
|
||||
{personalInfo.name}
|
||||
{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">{personalInfo.phone}</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">{personalInfo.email}</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">{personalInfo.linkedin}</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">{personalInfo.location}</span>
|
||||
<span className="font-medium">{resume.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -604,7 +647,7 @@ export default function ResumeBuilderPage() {
|
||||
<div className="mb-10">
|
||||
<h2 className="section-header">About Me</h2>
|
||||
<div className="text-[10px] leading-relaxed text-gray-600 font-medium text-justify">
|
||||
{personalInfo.summary}
|
||||
{resume.about}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -612,8 +655,8 @@ export default function ResumeBuilderPage() {
|
||||
<div className="mb-10">
|
||||
<h2 className="section-header">Skills</h2>
|
||||
<div className="text-[11px] text-gray-700 space-y-1 font-medium">
|
||||
{skills.map((skill) => (
|
||||
<p key={skill.id}>• {skill.name}</p>
|
||||
{resume.skills.map((skill, i) => (
|
||||
<p key={i}>• {skill}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -622,8 +665,8 @@ export default function ResumeBuilderPage() {
|
||||
<div>
|
||||
<h2 className="section-header">Language</h2>
|
||||
<div className="text-[11px] text-gray-700 space-y-1 font-medium">
|
||||
{languages.map((lang) => (
|
||||
<p key={lang.id}>• {lang.name}</p>
|
||||
{resume.languages.map((lang, i) => (
|
||||
<p key={i}>• {lang}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -631,21 +674,23 @@ export default function ResumeBuilderPage() {
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="main-content">
|
||||
<h1 className="main-header">{personalInfo.name}</h1>
|
||||
<h1 className="main-header">{resume.name}</h1>
|
||||
|
||||
{/* Experience */}
|
||||
<div className="mb-10">
|
||||
<h2 className="section-header">Experience</h2>
|
||||
{experiences.map((exp) => (
|
||||
<div key={exp.id} className="experience-item">
|
||||
{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 whitespace-pre-line">
|
||||
{exp.description}
|
||||
<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>
|
||||
))}
|
||||
@@ -654,11 +699,11 @@ export default function ResumeBuilderPage() {
|
||||
{/* Education */}
|
||||
<div>
|
||||
<h2 className="section-header">Education</h2>
|
||||
{education.map((edu) => (
|
||||
<div key={edu.id} className="mb-4">
|
||||
{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.location} - {edu.year}</p>
|
||||
<p className="text-[9px] text-gray-400 font-bold">{edu.year}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user