fix(mission-control): restore Resume Builder and PDF Viewer implementations
- Resume Builder: Full resume editing (personal, experience, education, skills) - PDF Viewer: Upload or load from URL - Both restored from git history
This commit is contained in:
@@ -1,20 +1,155 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import BackToMC from "@/components/mission-control/BackToMC";
|
import BackToMC from "@/components/mission-control/BackToMC";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function PdfViewerPage() {
|
export default function PdfViewerPage() {
|
||||||
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pdfBase64, setPdfBase64] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (file.type !== "application/pdf") {
|
||||||
|
setError("Please select a PDF file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Read file as base64
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const base64 = event.target?.result as string;
|
||||||
|
setPdfBase64(base64);
|
||||||
|
setPdfUrl(URL.createObjectURL(file));
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
setError("Failed to read file");
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUrlSubmit = async () => {
|
||||||
|
const input = prompt("Enter PDF URL:");
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(input);
|
||||||
|
setPdfUrl(input);
|
||||||
|
setPdfBase64(null);
|
||||||
|
} catch {
|
||||||
|
setError("Invalid URL");
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950 text-white">
|
<div className="min-h-screen bg-slate-950 text-white flex flex-col">
|
||||||
<BackToMC />
|
{/* Header */}
|
||||||
<div className="p-6">
|
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex-shrink-0">
|
||||||
<div className="mb-6">
|
<h1 className="text-2xl font-bold text-white">📄 PDF Viewer</h1>
|
||||||
<h1 className="text-3xl font-bold text-white mb-2">📰 PDF Viewer</h1>
|
<p className="text-slate-400 text-sm">View and analyze PDF documents</p>
|
||||||
<p className="text-slate-400">Loading...</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-800 rounded-xl p-8 border border-slate-700">
|
|
||||||
<p className="text-slate-400">Loading...</p>
|
{/* Toolbar */}
|
||||||
|
<div className="bg-slate-900/50 border-b border-slate-800 px-6 py-3 flex-shrink-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="cursor-pointer bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
|
||||||
|
📁 Upload PDF
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleUrlSubmit}
|
||||||
|
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
🔗 Load from URL
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{pdfUrl && (
|
||||||
|
<a
|
||||||
|
href={pdfUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
↗ Open Original
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-900/50 border border-red-700 rounded-lg p-4 m-4 max-w-md">
|
||||||
|
<h3 className="text-red-400 font-bold mb-2">Error</h3>
|
||||||
|
<p className="text-red-300">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="mt-2 bg-red-700 hover:bg-red-600 text-white px-4 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!pdfUrl && !loading && (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-8xl mb-6 opacity-50">📄</div>
|
||||||
|
<h2 className="text-xl font-bold text-white mb-2">No PDF Loaded</h2>
|
||||||
|
<p className="text-slate-400 mb-6">Upload a PDF or enter a URL to view it</p>
|
||||||
|
<div className="text-slate-500 text-sm space-y-1">
|
||||||
|
<p>• Upload your resume to let Horus analyze it</p>
|
||||||
|
<p>• Supports PDF files up to 50MB</p>
|
||||||
|
<p>• Or load from any public PDF URL</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin text-6xl mb-4">⏳</div>
|
||||||
|
<p className="text-slate-400">Loading PDF...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pdfUrl && !loading && (
|
||||||
|
<div className="flex-1 overflow-auto bg-slate-800 p-4 flex justify-center">
|
||||||
|
<iframe
|
||||||
|
src={pdfUrl}
|
||||||
|
className="w-full h-full min-h-[600px] bg-white shadow-2xl rounded"
|
||||||
|
title="PDF Viewer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div className="bg-slate-900/50 border-t border-slate-800 px-6 py-2 flex-shrink-0">
|
||||||
|
<p className="text-slate-500 text-xs">
|
||||||
|
💡 Tip: Upload your resume PDF and Horus can analyze the design to recreate it
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,520 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import BackToMC from "@/components/mission-control/BackToMC";
|
import BackToMC from "@/components/mission-control/BackToMC";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface PersonalInfo {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
location: string;
|
||||||
|
linkedin: string;
|
||||||
|
website: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Experience {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
company: string;
|
||||||
|
location: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
bullets: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Education {
|
||||||
|
id: string;
|
||||||
|
degree: string;
|
||||||
|
school: string;
|
||||||
|
location: string;
|
||||||
|
graduationDate: string;
|
||||||
|
gpa: string;
|
||||||
|
honors: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ResumePage() {
|
export default function ResumePage() {
|
||||||
|
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
|
||||||
|
const [personal, setPersonal] = useState<PersonalInfo>({
|
||||||
|
name: "Your Name",
|
||||||
|
title: "Your Professional Title",
|
||||||
|
email: "your.email@email.com",
|
||||||
|
phone: "+1 (555) 000-0000",
|
||||||
|
location: "City, State",
|
||||||
|
linkedin: "linkedin.com/in/yourprofile",
|
||||||
|
website: "yourwebsite.com"
|
||||||
|
});
|
||||||
|
|
||||||
|
const [experiences, setExperiences] = useState<Experience[]>([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: "Job Title",
|
||||||
|
company: "Company Name",
|
||||||
|
location: "City, State",
|
||||||
|
startDate: "Jan 2020",
|
||||||
|
endDate: "Present",
|
||||||
|
bullets: [
|
||||||
|
"Key achievement or responsibility with measurable impact",
|
||||||
|
"Another accomplishment that demonstrates your skills",
|
||||||
|
"Additional relevant contribution"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [education, setEducation] = useState<Education[]>([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
degree: "Bachelor of Science in Computer Science",
|
||||||
|
school: "University Name",
|
||||||
|
location: "City, State",
|
||||||
|
graduationDate: "May 2016",
|
||||||
|
gpa: "3.8/4.0",
|
||||||
|
honors: "Magna Cum Laude"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [skills, setSkills] = useState<string[]>([
|
||||||
|
"JavaScript/TypeScript", "React/Next.js", "Node.js", "Python",
|
||||||
|
"AWS/Cloud Services", "Database Management", "Agile/Scrum", "Git"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [languages, setLanguages] = useState<{name: string, level: string}[]>([
|
||||||
|
{ name: "English", level: "Native" },
|
||||||
|
{ name: "Spanish", level: "Professional" },
|
||||||
|
{ name: "Arabic", level: "Native" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [certifications, setCertifications] = useState<string[]>([
|
||||||
|
"AWS Solutions Architect",
|
||||||
|
"Google Cloud Professional"
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem("haitham_resume_v2");
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(saved);
|
||||||
|
if (data.personal) setPersonal(data.personal);
|
||||||
|
if (data.experiences) setExperiences(data.experiences);
|
||||||
|
if (data.education) setEducation(data.education);
|
||||||
|
if (data.skills) setSkills(data.skills);
|
||||||
|
if (data.languages) setLanguages(data.languages);
|
||||||
|
if (data.certifications) setCertifications(data.certifications);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("haitham_resume_v2", JSON.stringify({
|
||||||
|
personal, experiences, education, skills, languages, certifications
|
||||||
|
}));
|
||||||
|
}, [personal, experiences, education, skills, languages, certifications]);
|
||||||
|
|
||||||
|
const addExperience = () => {
|
||||||
|
setExperiences([...experiences, {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
title: "", company: "", location: "", startDate: "", endDate: "", bullets: [""]
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateExperience = (id: string, field: keyof Experience, value: any) => {
|
||||||
|
setExperiences(experiences.map(e => e.id === id ? { ...e, [field]: value } : e));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBullet = (id: string) => {
|
||||||
|
setExperiences(experiences.map(e =>
|
||||||
|
e.id === id ? { ...e, bullets: [...e.bullets, ""] } : e
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBullet = (expId: string, index: number, value: string) => {
|
||||||
|
setExperiences(experiences.map(e =>
|
||||||
|
e.id === expId ? { ...e, bullets: e.bullets.map((b, i) => i === index ? value : b) } : e
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBullet = (expId: string, index: number) => {
|
||||||
|
setExperiences(experiences.map(e =>
|
||||||
|
e.id === expId ? { ...e, bullets: e.bullets.filter((_, i) => i !== index) } : e
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEducation = () => {
|
||||||
|
setEducation([...education, {
|
||||||
|
id: Date.now().toString(), degree: "", school: "", location: "",
|
||||||
|
graduationDate: "", gpa: "", honors: ""
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEducation = (id: string, field: keyof Education, value: string) => {
|
||||||
|
setEducation(education.map(e => e.id === id ? { ...e, [field]: value } : e));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrint = () => {
|
||||||
|
setActiveTab("preview");
|
||||||
|
setTimeout(() => window.print(), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputClass = "w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500";
|
||||||
|
const labelClass = "block text-slate-400 text-xs mb-1 font-medium";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950 text-white">
|
<div className="min-h-screen bg-slate-950 text-white flex flex-col">
|
||||||
<BackToMC />
|
{/* Header */}
|
||||||
<div className="p-6">
|
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex items-center justify-between">
|
||||||
<div className="mb-6">
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-white mb-2">📄 Resume Builder</h1>
|
<h1 className="text-2xl font-bold text-white">📝 Professional Resume</h1>
|
||||||
<p className="text-slate-400">Loading...</p>
|
<p className="text-slate-400 text-sm">Edit • Preview • Download PDF</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-800 rounded-xl p-8 border border-slate-700">
|
<div className="flex gap-2">
|
||||||
<p className="text-slate-400">Loading...</p>
|
<button
|
||||||
|
onClick={() => setActiveTab(activeTab === "edit" ? "preview" : "edit")}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
activeTab === "edit" ? "bg-slate-700 text-white" : "bg-blue-600 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{activeTab === "edit" ? "👁️ Preview" : "✏️ Edit"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handlePrint}
|
||||||
|
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
📄 Download PDF
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeTab === "edit" ? (
|
||||||
|
<div className="flex-1 overflow-auto p-6">
|
||||||
|
<div className="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-white font-bold mb-4">👤 Personal Information</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Full Name</label>
|
||||||
|
<input type="text" value={personal.name} onChange={e => setPersonal({...personal, name: e.target.value})} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Professional Title</label>
|
||||||
|
<input type="text" value={personal.title} onChange={e => setPersonal({...personal, title: e.target.value})} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Email</label>
|
||||||
|
<input type="email" value={personal.email} onChange={e => setPersonal({...personal, email: e.target.value})} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Phone</label>
|
||||||
|
<input type="tel" value={personal.phone} onChange={e => setPersonal({...personal, phone: e.target.value})} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Location</label>
|
||||||
|
<input type="text" value={personal.location} onChange={e => setPersonal({...personal, location: e.target.value})} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>LinkedIn</label>
|
||||||
|
<input type="text" value={personal.linkedin} onChange={e => setPersonal({...personal, linkedin: e.target.value})} className={inputClass} />
|
||||||
|
</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-white font-bold">💼 Work Experience</h2>
|
||||||
|
<button onClick={addExperience} className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm">+ Add Position</button>
|
||||||
|
</div>
|
||||||
|
{experiences.map((exp) => (
|
||||||
|
<div key={exp.id} className="bg-slate-900 rounded-lg p-4 mb-4 border border-slate-700">
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Job Title</label>
|
||||||
|
<input type="text" value={exp.title} onChange={e => updateExperience(exp.id, "title", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Company</label>
|
||||||
|
<input type="text" value={exp.company} onChange={e => updateExperience(exp.id, "company", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Location</label>
|
||||||
|
<input type="text" value={exp.location} onChange={e => updateExperience(exp.id, "location", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className={labelClass}>Start</label>
|
||||||
|
<input type="text" value={exp.startDate} onChange={e => updateExperience(exp.id, "startDate", e.target.value)} className={inputClass} placeholder="Jan 2020" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className={labelClass}>End</label>
|
||||||
|
<input type="text" value={exp.endDate} onChange={e => updateExperience(exp.id, "endDate", e.target.value)} className={inputClass} placeholder="Present" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className={labelClass}>Key Achievements</label>
|
||||||
|
{exp.bullets.map((bullet, i) => (
|
||||||
|
<div key={i} className="flex gap-2 mb-2">
|
||||||
|
<input type="text" value={bullet} onChange={e => updateBullet(exp.id, i, e.target.value)} className={inputClass} placeholder={`Achievement ${i + 1}`} />
|
||||||
|
{exp.bullets.length > 1 && (
|
||||||
|
<button onClick={() => removeBullet(exp.id, i)} className="text-red-400 hover:text-red-300 text-sm px-2">✕</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button onClick={() => addBullet(exp.id)} className="text-blue-400 hover:text-blue-300 text-xs">+ Add achievement</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-white font-bold">🎓 Education</h2>
|
||||||
|
<button onClick={addEducation} className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm">+ Add</button>
|
||||||
|
</div>
|
||||||
|
{education.map((edu) => (
|
||||||
|
<div key={edu.id} className="bg-slate-900 rounded-lg p-4 mb-4 border border-slate-700">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Degree</label>
|
||||||
|
<input type="text" value={edu.degree} onChange={e => updateEducation(edu.id, "degree", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>School</label>
|
||||||
|
<input type="text" value={edu.school} onChange={e => updateEducation(edu.id, "school", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Location</label>
|
||||||
|
<input type="text" value={edu.location} onChange={e => updateEducation(edu.id, "location", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Graduation Date</label>
|
||||||
|
<input type="text" value={edu.graduationDate} onChange={e => updateEducation(edu.id, "graduationDate", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>GPA (optional)</label>
|
||||||
|
<input type="text" value={edu.gpa} onChange={e => updateEducation(edu.id, "gpa", e.target.value)} className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Honors (optional)</label>
|
||||||
|
<input type="text" value={edu.honors} onChange={e => updateEducation(edu.id, "honors", e.target.value)} className={inputClass} placeholder="Magna Cum Laude" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skills */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||||||
|
<h2 className="text-white font-bold mb-4">🛠️ Technical Skills</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{skills.map((skill, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={skill}
|
||||||
|
onChange={e => {
|
||||||
|
const newSkills = [...skills];
|
||||||
|
newSkills[i] = e.target.value;
|
||||||
|
setSkills(newSkills);
|
||||||
|
}}
|
||||||
|
className="bg-transparent text-white text-sm w-24 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button onClick={() => setSkills(skills.filter((_, j) => j !== i))} className="text-slate-500 hover:text-red-400 text-xs">✕</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button onClick={() => setSkills([...skills, ""])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1 rounded-lg">+ Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Languages */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||||||
|
<h2 className="text-white font-bold mb-4">🌍 Languages</h2>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{languages.map((lang, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={lang.name}
|
||||||
|
onChange={e => {
|
||||||
|
const newLangs = [...languages];
|
||||||
|
newLangs[i].name = e.target.value;
|
||||||
|
setLanguages(newLangs);
|
||||||
|
}}
|
||||||
|
className="bg-transparent text-white text-sm w-24 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<span className="text-slate-500 text-xs">•</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={lang.level}
|
||||||
|
onChange={e => {
|
||||||
|
const newLangs = [...languages];
|
||||||
|
newLangs[i].level = e.target.value;
|
||||||
|
setLanguages(newLangs);
|
||||||
|
}}
|
||||||
|
className="bg-transparent text-slate-400 text-sm w-20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button onClick={() => setLanguages([...languages, {name: "", level: ""}])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-2 rounded-lg">+ Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Certifications */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||||||
|
<h2 className="text-white font-bold mb-4">📜 Certifications</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{certifications.map((cert, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cert}
|
||||||
|
onChange={e => {
|
||||||
|
const newCerts = [...certifications];
|
||||||
|
newCerts[i] = e.target.value;
|
||||||
|
setCertifications(newCerts);
|
||||||
|
}}
|
||||||
|
className="bg-transparent text-white text-sm w-40 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button onClick={() => setCertifications(certifications.filter((_, j) => j !== i))} className="text-slate-500 hover:text-red-400 text-xs">✕</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button onClick={() => setCertifications([...certifications, ""])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1 rounded-lg">+ Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* PREVIEW - Professional Resume Template */
|
||||||
|
<div className="flex-1 overflow-auto bg-slate-100 p-8">
|
||||||
|
<div className="max-w-850px mx-auto bg-white shadow-xl rounded-lg overflow-hidden" style={{maxWidth: "850px"}}>
|
||||||
|
|
||||||
|
{/* Header - Dark Blue Professional */}
|
||||||
|
<div className="bg-gradient-to-r from-slate-800 to-slate-700 text-white px-8 py-10">
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<h1 className="text-3xl font-bold tracking-wide mb-2">{personal.name || "Your Name"}</h1>
|
||||||
|
<div className="text-slate-300 text-lg font-light">{personal.title || "Professional Title"}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center flex-wrap gap-4 text-sm text-slate-300">
|
||||||
|
{personal.email && <span>✉️ {personal.email}</span>}
|
||||||
|
{personal.phone && <span>📱 {personal.phone}</span>}
|
||||||
|
{personal.location && <span>📍 {personal.location}</span>}
|
||||||
|
{personal.linkedin && <span>💼 {personal.linkedin}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="px-8 py-6 space-y-6">
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<div>
|
||||||
|
<div className="border-b-2 border-slate-800 pb-1 mb-3">
|
||||||
|
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Professional Summary</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-600 text-sm leading-relaxed">
|
||||||
|
Results-driven professional with proven expertise in delivering high-impact solutions. Skilled in modern technologies and best practices. Demonstrated ability to lead projects, optimize processes, and achieve organizational goals.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Experience */}
|
||||||
|
<div>
|
||||||
|
<div className="border-b-2 border-slate-800 pb-1 mb-3">
|
||||||
|
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Work Experience</span>
|
||||||
|
</div>
|
||||||
|
{experiences.filter(e => e.title || e.company).map((exp) => (
|
||||||
|
<div key={exp.id} className="mb-4">
|
||||||
|
<div className="flex justify-between items-baseline mb-1">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-slate-800">{exp.title || "Job Title"}</span>
|
||||||
|
<span className="text-slate-600"> | {exp.company || "Company"}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-500 text-sm">{exp.startDate} - {exp.endDate}</span>
|
||||||
|
</div>
|
||||||
|
{exp.location && <div className="text-slate-500 text-sm mb-2">{exp.location}</div>}
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{exp.bullets.filter(b => b).map((bullet, i) => (
|
||||||
|
<li key={i} className="text-slate-600 text-sm flex items-start gap-2">
|
||||||
|
<span className="text-slate-400">•</span>
|
||||||
|
<span>{bullet}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Education */}
|
||||||
|
<div>
|
||||||
|
<div className="border-b-2 border-slate-800 pb-1 mb-3">
|
||||||
|
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Education</span>
|
||||||
|
</div>
|
||||||
|
{education.filter(e => e.degree || e.school).map((edu) => (
|
||||||
|
<div key={edu.id} className="mb-3">
|
||||||
|
<div className="flex justify-between items-baseline">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-slate-800">{edu.degree || "Degree"}</span>
|
||||||
|
{edu.honors && <span className="text-slate-500"> ({edu.honors})</span>}
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-500 text-sm">{edu.graduationDate}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-600 text-sm">{edu.school}{edu.location && `, ${edu.location}`}</div>
|
||||||
|
{edu.gpa && <div className="text-slate-500 text-sm">GPA: {edu.gpa}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skills */}
|
||||||
|
<div>
|
||||||
|
<div className="border-b-2 border-slate-800 pb-1 mb-3">
|
||||||
|
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Technical Skills</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{skills.filter(s => s).map((skill, i) => (
|
||||||
|
<span key={i} className="bg-slate-100 text-slate-700 px-3 py-1 rounded text-sm">{skill}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Languages & Certs */}
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="border-b-2 border-slate-800 pb-1 mb-3">
|
||||||
|
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Languages</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{languages.filter(l => l.name).map((lang, i) => (
|
||||||
|
<div key={i} className="text-sm">
|
||||||
|
<span className="text-slate-800 font-medium">{lang.name}</span>
|
||||||
|
<span className="text-slate-500"> - {lang.level}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="border-b-2 border-slate-800 pb-1 mb-3">
|
||||||
|
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Certifications</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{certifications.filter(c => c).map((cert, i) => (
|
||||||
|
<div key={i} className="text-slate-700 text-sm">{cert}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style jsx global>{`
|
||||||
|
@media print {
|
||||||
|
body { background: white !important; }
|
||||||
|
.bg-slate-100 { background: white !important; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user