chore: initiate local project to github

This commit is contained in:
eshanized
2024-12-26 10:20:09 +05:30
commit 2df48bad2f
43 changed files with 9476 additions and 0 deletions

3
.bolt/config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

8
.bolt/prompt Normal file
View File

@@ -0,0 +1,8 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

28
eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4212
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

59
package.json Normal file
View File

@@ -0,0 +1,59 @@
{
"name": "resume-builder",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "A professional resume builder with multiple templates and export options",
"author": {
"name": "Eshan",
"email": "eshanized@gmail.com",
"url": "https://github.com/eshanized"
},
"homepage": "https://github.com/eshanized/resume-builder",
"repository": {
"type": "git",
"url": "git+https://github.com/eshanized/resume-builder.git"
},
"bugs": {
"url": "https://github.com/eshanized/resume-builder/issues"
},
"keywords": [
"resume",
"cv",
"builder",
"react",
"typescript",
"tailwindcss"
],
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"docx": "^8.5.0",
"file-saver": "^2.0.5",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/file-saver": "^2.0.7",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}

2699
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

102
src/App.tsx Normal file
View File

@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import { Resume, TemplateType } from './types/resume';
import { ResumeForm } from './components/ResumeForm';
import { ModernTemplate } from './components/templates/ModernTemplate';
import { ClassicTemplate } from './components/templates/ClassicTemplate';
import { MinimalTemplate } from './components/templates/MinimalTemplate';
import { VibrantTemplate } from './components/templates/VibrantTemplate';
import { GradientTemplate } from './components/templates/GradientTemplate';
import { CreativeTemplate } from './components/templates/CreativeTemplate';
import { ElegantTemplate } from './components/templates/ElegantTemplate';
import { TechTemplate } from './components/templates/TechTemplate';
import { ProfessionalTemplate } from './components/templates/ProfessionalTemplate';
import { FileText, Download, FileOutput } from 'lucide-react';
import { Footer } from './components/common/Footer';
import { PrintCredit } from './components/common/PrintCredit';
import { initialResume } from './constants/initialState';
import { exportToWord } from './utils/wordExport';
export default function App() {
const [resume, setResume] = useState<Resume>(initialResume);
const [template, setTemplate] = useState<TemplateType>('vibrant');
const TemplateComponent = {
modern: ModernTemplate,
classic: ClassicTemplate,
minimal: MinimalTemplate,
vibrant: VibrantTemplate,
gradient: GradientTemplate,
creative: CreativeTemplate,
elegant: ElegantTemplate,
tech: TechTemplate,
professional: ProfessionalTemplate,
}[template];
const handleWordExport = async () => {
await exportToWord(resume);
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="container mx-auto px-4 py-8">
<header className="text-center mb-8">
<div className="flex items-center justify-center gap-2 mb-2">
<FileText className="w-8 h-8 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Resume Builder</h1>
</div>
<p className="text-gray-600">Create your professional resume with our beautiful templates</p>
<div className="text-sm text-gray-500 mt-1">Version 1.0.0</div>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-6">
<div className="backdrop-blur-lg bg-white/30 rounded-xl shadow-xl p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Choose Template</h2>
<div className="grid grid-cols-3 gap-2">
{(['modern', 'classic', 'minimal', 'vibrant', 'gradient', 'creative', 'elegant', 'tech', 'professional'] as const).map((t) => (
<button
key={t}
onClick={() => setTemplate(t)}
className={`px-3 py-2 rounded-lg capitalize text-sm ${
template === t
? 'bg-blue-600 text-white'
: 'bg-white/50 text-gray-700 hover:bg-white/70'
}`}
>
{t}
</button>
))}
</div>
</div>
<ResumeForm resume={resume} setResume={setResume} />
</div>
<div className="lg:sticky lg:top-8 space-y-4">
<div className="flex justify-end gap-4">
<button
onClick={handleWordExport}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<FileOutput size={20} />
Export to Word
</button>
<button
onClick={() => window.print()}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Download size={20} />
Download PDF
</button>
</div>
<div className="preview-wrapper overflow-auto rounded-xl shadow-xl">
<TemplateComponent resume={resume} />
<PrintCredit />
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { PersonalInfoForm } from './forms/PersonalInfoForm';
import { EducationForm } from './forms/EducationForm';
import { ExperienceForm } from './forms/ExperienceForm';
import { SkillsForm } from './forms/SkillsForm';
import { AchievementsForm } from './forms/AchievementsForm';
import { CertificationsForm } from './forms/CertificationsForm';
import { HobbiesForm } from './forms/HobbiesForm';
import { Resume } from '../types/resume';
interface ResumeFormProps {
resume: Resume;
setResume: React.Dispatch<React.SetStateAction<Resume>>;
}
export function ResumeForm({ resume, setResume }: ResumeFormProps) {
return (
<div className="space-y-6 p-6 backdrop-blur-lg bg-white/30 rounded-xl shadow-xl">
<PersonalInfoForm
personalInfo={resume.personalInfo}
setPersonalInfo={(info) => setResume({ ...resume, personalInfo: info })}
/>
<EducationForm
education={resume.education}
setEducation={(edu) => setResume({ ...resume, education: edu })}
/>
<ExperienceForm
experience={resume.experience}
setExperience={(exp) => setResume({ ...resume, experience: exp })}
/>
<SkillsForm
skills={resume.skills}
setSkills={(skills) => setResume({ ...resume, skills: skills })}
/>
<AchievementsForm
achievements={resume.achievements}
setAchievements={(achievements) => setResume({ ...resume, achievements })}
/>
<CertificationsForm
certifications={resume.certifications}
setCertifications={(certifications) => setResume({ ...resume, certifications })}
/>
<HobbiesForm
hobbies={resume.hobbies}
setHobbies={(hobbies) => setResume({ ...resume, hobbies })}
/>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Github } from 'lucide-react';
export function Footer() {
return (
<footer className="py-4 px-8 text-center text-gray-600 bg-white/50 backdrop-blur-sm mt-8">
<a
href="https://github.com/eshanized"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-gray-700 hover:text-blue-600 transition-colors"
>
<Github className="w-4 h-4" />
<span>Created by eshanized with </span>
</a>
</footer>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Github } from 'lucide-react';
export function PrintCredit() {
return (
<div className="text-center text-xs text-gray-500 mt-4 print:block hidden">
Created with Resume Builder by{' '}
<a
href="https://github.com/eshanized"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
eshanized
</a>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { Achievement } from '../../types/resume';
import { PlusCircle, Trash2 } from 'lucide-react';
interface AchievementsFormProps {
achievements: Achievement[];
setAchievements: (achievements: Achievement[]) => void;
}
export function AchievementsForm({ achievements, setAchievements }: AchievementsFormProps) {
const addAchievement = () => {
setAchievements([...achievements, { title: '', description: '', date: '' }]);
};
const removeAchievement = (index: number) => {
setAchievements(achievements.filter((_, i) => i !== index));
};
const updateAchievement = (index: number, field: keyof Achievement, value: string) => {
const newAchievements = [...achievements];
newAchievements[index] = { ...newAchievements[index], [field]: value };
setAchievements(newAchievements);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Achievements</h2>
<button
onClick={addAchievement}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
>
<PlusCircle size={20} /> Add Achievement
</button>
</div>
{achievements.map((achievement, index) => (
<div key={index} className="p-4 bg-white/40 rounded-lg space-y-3">
<div className="flex justify-between">
<h3 className="font-medium">Achievement #{index + 1}</h3>
<button
onClick={() => removeAchievement(index)}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<div className="grid gap-4">
<input
type="text"
value={achievement.title}
onChange={(e) => updateAchievement(index, 'title', e.target.value)}
placeholder="Achievement Title"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<textarea
value={achievement.description}
onChange={(e) => updateAchievement(index, 'description', e.target.value)}
placeholder="Achievement Description"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 h-24"
/>
<input
type="date"
value={achievement.date}
onChange={(e) => updateAchievement(index, 'date', e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { Certification } from '../../types/resume';
import { PlusCircle, Trash2 } from 'lucide-react';
interface CertificationsFormProps {
certifications: Certification[];
setCertifications: (certifications: Certification[]) => void;
}
export function CertificationsForm({ certifications, setCertifications }: CertificationsFormProps) {
const addCertification = () => {
setCertifications([...certifications, { name: '', issuer: '', date: '' }]);
};
const removeCertification = (index: number) => {
setCertifications(certifications.filter((_, i) => i !== index));
};
const updateCertification = (index: number, field: keyof Certification, value: string) => {
const newCertifications = [...certifications];
newCertifications[index] = { ...newCertifications[index], [field]: value };
setCertifications(newCertifications);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Certifications</h2>
<button
onClick={addCertification}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
>
<PlusCircle size={20} /> Add Certification
</button>
</div>
{certifications.map((cert, index) => (
<div key={index} className="p-4 bg-white/40 rounded-lg space-y-3">
<div className="flex justify-between">
<h3 className="font-medium">Certification #{index + 1}</h3>
<button
onClick={() => removeCertification(index)}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<div className="grid gap-4">
<input
type="text"
value={cert.name}
onChange={(e) => updateCertification(index, 'name', e.target.value)}
placeholder="Certification Name"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={cert.issuer}
onChange={(e) => updateCertification(index, 'issuer', e.target.value)}
placeholder="Issuing Organization"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="grid grid-cols-2 gap-4">
<input
type="date"
value={cert.date}
onChange={(e) => updateCertification(index, 'date', e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="date"
value={cert.expiryDate}
onChange={(e) => updateCertification(index, 'expiryDate', e.target.value)}
placeholder="Expiry Date (Optional)"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<input
type="url"
value={cert.credentialUrl}
onChange={(e) => updateCertification(index, 'credentialUrl', e.target.value)}
placeholder="Credential URL (Optional)"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,288 @@
import React from 'react';
import { Education } from '../../types/resume';
import { PlusCircle, Trash2, Plus, Minus } from 'lucide-react';
interface EducationFormProps {
education: Education[];
setEducation: (education: Education[]) => void;
}
export function EducationForm({ education, setEducation }: EducationFormProps) {
const addEducation = () => {
setEducation([
...education,
{
school: '',
degree: '',
fieldOfStudy: '',
startDate: '',
endDate: '',
gpa: '',
honors: [],
activities: [],
description: '',
location: '',
thesis: '',
advisors: [],
relevantCourses: [],
},
]);
};
const removeEducation = (index: number) => {
setEducation(education.filter((_, i) => i !== index));
};
const updateEducation = (index: number, field: keyof Education, value: any) => {
const newEducation = [...education];
newEducation[index] = { ...newEducation[index], [field]: value };
setEducation(newEducation);
};
const addArrayItem = (index: number, field: 'honors' | 'activities' | 'advisors' | 'relevantCourses') => {
const newEducation = [...education];
newEducation[index][field] = [...newEducation[index][field], ''];
setEducation(newEducation);
};
const removeArrayItem = (index: number, field: 'honors' | 'activities' | 'advisors' | 'relevantCourses', itemIndex: number) => {
const newEducation = [...education];
newEducation[index][field] = newEducation[index][field].filter((_, i) => i !== itemIndex);
setEducation(newEducation);
};
const updateArrayItem = (
index: number,
field: 'honors' | 'activities' | 'advisors' | 'relevantCourses',
itemIndex: number,
value: string
) => {
const newEducation = [...education];
newEducation[index][field][itemIndex] = value;
setEducation(newEducation);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Education</h2>
<button
onClick={addEducation}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
>
<PlusCircle size={20} /> Add Education
</button>
</div>
{education.map((edu, index) => (
<div key={index} className="p-4 bg-white/40 rounded-lg space-y-4">
<div className="flex justify-between">
<h3 className="font-medium">Education #{index + 1}</h3>
<button
onClick={() => removeEducation(index)}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
value={edu.school}
onChange={(e) => updateEducation(index, 'school', e.target.value)}
placeholder="School"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={edu.location}
onChange={(e) => updateEducation(index, 'location', e.target.value)}
placeholder="Location"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={edu.degree}
onChange={(e) => updateEducation(index, 'degree', e.target.value)}
placeholder="Degree"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={edu.fieldOfStudy}
onChange={(e) => updateEducation(index, 'fieldOfStudy', e.target.value)}
placeholder="Field of Study"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="grid grid-cols-2 gap-4">
<input
type="date"
value={edu.startDate}
onChange={(e) => updateEducation(index, 'startDate', e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="date"
value={edu.endDate}
onChange={(e) => updateEducation(index, 'endDate', e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<input
type="text"
value={edu.gpa}
onChange={(e) => updateEducation(index, 'gpa', e.target.value)}
placeholder="GPA"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-4">
<textarea
value={edu.description}
onChange={(e) => updateEducation(index, 'description', e.target.value)}
placeholder="Description"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 h-24"
/>
<input
type="text"
value={edu.thesis}
onChange={(e) => updateEducation(index, 'thesis', e.target.value)}
placeholder="Thesis Title (Optional)"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{/* Honors */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-gray-700">Honors & Awards</label>
<button
type="button"
onClick={() => addArrayItem(index, 'honors')}
className="text-blue-600 hover:text-blue-700"
>
<Plus size={16} />
</button>
</div>
{edu.honors.map((honor, honorIndex) => (
<div key={honorIndex} className="flex gap-2">
<input
type="text"
value={honor}
onChange={(e) => updateArrayItem(index, 'honors', honorIndex, e.target.value)}
placeholder="Honor or Award"
className="flex-1 px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeArrayItem(index, 'honors', honorIndex)}
className="text-red-500 hover:text-red-600"
>
<Minus size={16} />
</button>
</div>
))}
</div>
{/* Activities */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-gray-700">Activities & Societies</label>
<button
type="button"
onClick={() => addArrayItem(index, 'activities')}
className="text-blue-600 hover:text-blue-700"
>
<Plus size={16} />
</button>
</div>
{edu.activities.map((activity, activityIndex) => (
<div key={activityIndex} className="flex gap-2">
<input
type="text"
value={activity}
onChange={(e) => updateArrayItem(index, 'activities', activityIndex, e.target.value)}
placeholder="Activity"
className="flex-1 px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeArrayItem(index, 'activities', activityIndex)}
className="text-red-500 hover:text-red-600"
>
<Minus size={16} />
</button>
</div>
))}
</div>
{/* Advisors */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-gray-700">Academic Advisors</label>
<button
type="button"
onClick={() => addArrayItem(index, 'advisors')}
className="text-blue-600 hover:text-blue-700"
>
<Plus size={16} />
</button>
</div>
{edu.advisors?.map((advisor, advisorIndex) => (
<div key={advisorIndex} className="flex gap-2">
<input
type="text"
value={advisor}
onChange={(e) => updateArrayItem(index, 'advisors', advisorIndex, e.target.value)}
placeholder="Advisor Name"
className="flex-1 px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeArrayItem(index, 'advisors', advisorIndex)}
className="text-red-500 hover:text-red-600"
>
<Minus size={16} />
</button>
</div>
))}
</div>
{/* Relevant Courses */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-gray-700">Relevant Courses</label>
<button
type="button"
onClick={() => addArrayItem(index, 'relevantCourses')}
className="text-blue-600 hover:text-blue-700"
>
<Plus size={16} />
</button>
</div>
{edu.relevantCourses.map((course, courseIndex) => (
<div key={courseIndex} className="flex gap-2">
<input
type="text"
value={course}
onChange={(e) => updateArrayItem(index, 'relevantCourses', courseIndex, e.target.value)}
placeholder="Course Name"
className="flex-1 px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeArrayItem(index, 'relevantCourses', courseIndex)}
className="text-red-500 hover:text-red-600"
>
<Minus size={16} />
</button>
</div>
))}
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { Experience } from '../../types/resume';
import { PlusCircle, Trash2 } from 'lucide-react';
interface ExperienceFormProps {
experience: Experience[];
setExperience: (experience: Experience[]) => void;
}
export function ExperienceForm({ experience, setExperience }: ExperienceFormProps) {
const addExperience = () => {
setExperience([
...experience,
{ company: '', position: '', startDate: '', endDate: '', description: '' },
]);
};
const removeExperience = (index: number) => {
setExperience(experience.filter((_, i) => i !== index));
};
const updateExperience = (index: number, field: keyof Experience, value: string) => {
const newExperience = [...experience];
newExperience[index] = { ...newExperience[index], [field]: value };
setExperience(newExperience);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Experience</h2>
<button
onClick={addExperience}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
>
<PlusCircle size={20} /> Add Experience
</button>
</div>
{experience.map((exp, index) => (
<div key={index} className="p-4 bg-white/40 rounded-lg space-y-3">
<div className="flex justify-between">
<h3 className="font-medium">Experience #{index + 1}</h3>
<button
onClick={() => removeExperience(index)}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
value={exp.company}
onChange={(e) => updateExperience(index, 'company', e.target.value)}
placeholder="Company"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={exp.position}
onChange={(e) => updateExperience(index, 'position', e.target.value)}
placeholder="Position"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="grid grid-cols-2 gap-4">
<input
type="date"
value={exp.startDate}
onChange={(e) => updateExperience(index, 'startDate', e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="date"
value={exp.endDate}
onChange={(e) => updateExperience(index, 'endDate', e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<textarea
value={exp.description}
onChange={(e) => updateExperience(index, 'description', e.target.value)}
placeholder="Job Description"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 h-32"
/>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { Hobby } from '../../types/resume';
import { PlusCircle, Trash2 } from 'lucide-react';
interface HobbiesFormProps {
hobbies: Hobby[];
setHobbies: (hobbies: Hobby[]) => void;
}
export function HobbiesForm({ hobbies, setHobbies }: HobbiesFormProps) {
const addHobby = () => {
setHobbies([...hobbies, { name: '', description: '' }]);
};
const removeHobby = (index: number) => {
setHobbies(hobbies.filter((_, i) => i !== index));
};
const updateHobby = (index: number, field: keyof Hobby, value: string) => {
const newHobbies = [...hobbies];
newHobbies[index] = { ...newHobbies[index], [field]: value };
setHobbies(newHobbies);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Hobbies & Interests</h2>
<button
onClick={addHobby}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
>
<PlusCircle size={20} /> Add Hobby
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{hobbies.map((hobby, index) => (
<div key={index} className="p-4 bg-white/40 rounded-lg space-y-3">
<div className="flex justify-between">
<input
type="text"
value={hobby.name}
onChange={(e) => updateHobby(index, 'name', e.target.value)}
placeholder="Hobby Name"
className="flex-1 px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={() => removeHobby(index)}
className="ml-2 text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<textarea
value={hobby.description}
onChange={(e) => updateHobby(index, 'description', e.target.value)}
placeholder="Description (Optional)"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 h-20"
/>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Upload } from 'lucide-react';
interface ImageUploadProps {
value: string;
onChange: (value: string) => void;
}
export function ImageUpload({ value, onChange }: ImageUploadProps) {
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
onChange(reader.result as string);
};
reader.readAsDataURL(file);
}
};
return (
<div className="flex flex-col items-center gap-4">
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-white/50">
{value ? (
<img src={value} alt="Profile" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-100">
<Upload className="w-8 h-8 text-gray-400" />
</div>
)}
</div>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="hidden"
id="profile-picture"
/>
<label
htmlFor="profile-picture"
className="cursor-pointer px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm"
>
Upload Photo
</label>
</div>
);
}

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { PersonalInfo } from '../../types/resume';
import { ImageUpload } from './ImageUpload';
import { SocialLinks } from './SocialLinks';
import { Wand2 } from 'lucide-react';
import { generateSummary } from '../../utils/summaryGenerator';
interface PersonalInfoFormProps {
personalInfo: PersonalInfo;
setPersonalInfo: (info: PersonalInfo) => void;
}
export function PersonalInfoForm({ personalInfo, setPersonalInfo }: PersonalInfoFormProps) {
const handleChange = (field: keyof PersonalInfo, value: string) => {
setPersonalInfo({ ...personalInfo, [field]: value });
};
const handleGenerateSummary = async () => {
if (!personalInfo.jobTitle) {
alert('Please enter a job title first');
return;
}
const summary = await generateSummary(personalInfo.jobTitle);
handleChange('summary', summary);
};
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-800">Personal Information</h2>
<div className="flex flex-col md:flex-row gap-8">
<ImageUpload
value={personalInfo.profilePicture}
onChange={(value) => handleChange('profilePicture', value)}
/>
<div className="flex-1 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
value={personalInfo.fullName}
onChange={(e) => handleChange('fullName', e.target.value)}
placeholder="Full Name"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={personalInfo.jobTitle}
onChange={(e) => handleChange('jobTitle', e.target.value)}
placeholder="Job Title"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="email"
value={personalInfo.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="Email"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="tel"
value={personalInfo.phone}
onChange={(e) => handleChange('phone', e.target.value)}
placeholder="Phone"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={personalInfo.location}
onChange={(e) => handleChange('location', e.target.value)}
placeholder="Location"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<SocialLinks
website={personalInfo.website}
linkedin={personalInfo.linkedin}
github={personalInfo.github}
onChange={handleChange}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="block text-sm font-medium text-gray-700">Professional Summary</label>
<button
onClick={handleGenerateSummary}
className="flex items-center gap-2 px-3 py-1 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Wand2 className="w-4 h-4" />
Generate Summary
</button>
</div>
<textarea
value={personalInfo.summary}
onChange={(e) => handleChange('summary', e.target.value)}
placeholder="Professional Summary"
className="w-full px-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 h-32"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { Skill } from '../../types/resume';
import { PlusCircle, Trash2 } from 'lucide-react';
interface SkillsFormProps {
skills: Skill[];
setSkills: (skills: Skill[]) => void;
}
export function SkillsForm({ skills, setSkills }: SkillsFormProps) {
const addSkill = () => {
setSkills([...skills, { name: '', level: 3 }]);
};
const removeSkill = (index: number) => {
setSkills(skills.filter((_, i) => i !== index));
};
const updateSkill = (index: number, field: keyof Skill, value: string | number) => {
const newSkills = [...skills];
newSkills[index] = { ...newSkills[index], [field]: value };
setSkills(newSkills);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-800">Skills</h2>
<button
onClick={addSkill}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
>
<PlusCircle size={20} /> Add Skill
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{skills.map((skill, index) => (
<div key={index} className="flex items-center gap-2 bg-white/40 p-3 rounded-lg">
<input
type="text"
value={skill.name}
onChange={(e) => updateSkill(index, 'name', e.target.value)}
placeholder="Skill name"
className="flex-1 px-3 py-1 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="range"
min="1"
max="5"
value={skill.level}
onChange={(e) => updateSkill(index, 'level', parseInt(e.target.value))}
className="w-24"
/>
<button
onClick={() => removeSkill(index)}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Globe, Linkedin, Github } from 'lucide-react';
interface SocialLinksProps {
website: string;
linkedin: string;
github: string;
onChange: (field: string, value: string) => void;
}
export function SocialLinks({ website, linkedin, github, onChange }: SocialLinksProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative">
<Globe className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" />
<input
type="url"
value={website}
onChange={(e) => onChange('website', e.target.value)}
placeholder="Website"
className="w-full pl-10 pr-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="relative">
<Linkedin className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" />
<input
type="url"
value={linkedin}
onChange={(e) => onChange('linkedin', e.target.value)}
placeholder="LinkedIn URL"
className="w-full pl-10 pr-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="relative">
<Github className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" />
<input
type="url"
value={github}
onChange={(e) => onChange('github', e.target.value)}
placeholder="GitHub URL"
className="w-full pl-10 pr-4 py-2 rounded-lg bg-white/50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { Resume } from '../../types/resume';
export function ClassicTemplate({ resume }: { resume: Resume }) {
return (
<div className="max-w-4xl mx-auto bg-white shadow-lg p-8 rounded-lg">
<header className="border-b-2 border-gray-300 pb-4 mb-6">
<h1 className="text-4xl font-bold text-gray-900">{resume.personalInfo.fullName}</h1>
<div className="mt-2 text-gray-700">
<p>{resume.personalInfo.email} {resume.personalInfo.phone}</p>
<p>{resume.personalInfo.location}</p>
</div>
</header>
<section className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Professional Summary</h2>
<p className="text-gray-700">{resume.personalInfo.summary}</p>
</section>
<section className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Professional Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="mb-4">
<h3 className="text-xl font-semibold text-gray-900">{exp.position}</h3>
<p className="text-gray-700 font-medium">{exp.company}</p>
<p className="text-gray-600 italic">{exp.startDate} - {exp.endDate}</p>
<p className="text-gray-700 mt-2">{exp.description}</p>
</div>
))}
</section>
<section className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="mb-4">
<h3 className="text-xl font-semibold text-gray-900">{edu.school}</h3>
<p className="text-gray-700">{edu.degree} in {edu.fieldOfStudy}</p>
<p className="text-gray-600 italic">{edu.startDate} - {edu.endDate}</p>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4">Skills</h2>
<div className="flex flex-wrap gap-2">
{resume.skills.map((skill, index) => (
<span
key={index}
className="px-3 py-1 bg-gray-200 text-gray-800 rounded-full text-sm"
>
{skill.name}
</span>
))}
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Mail, Phone, MapPin, Globe, Linkedin, Github, Calendar, Award, Medal, Heart } from 'lucide-react';
export function CreativeTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-50">
<div className="relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-emerald-600 to-teal-600 transform -skew-y-6 origin-top-left" />
<div className="relative z-10 p-8 pt-16">
<div className="flex items-center gap-6">
{personalInfo.profilePicture && (
<img
src={personalInfo.profilePicture}
alt={personalInfo.fullName}
className="w-32 h-32 rounded-full object-cover border-4 border-white shadow-xl"
/>
)}
<div className="flex-1 text-white">
<h1 className="text-4xl font-bold">{personalInfo.fullName}</h1>
<p className="text-xl text-emerald-100 mt-1">{personalInfo.jobTitle}</p>
<div className="flex flex-wrap gap-4 mt-4">
{personalInfo.email && (
<a href={`mailto:${personalInfo.email}`} className="flex items-center gap-2 hover:text-emerald-200">
<Mail className="w-4 h-4" />
<span>{personalInfo.email}</span>
</a>
)}
{personalInfo.phone && (
<a href={`tel:${personalInfo.phone}`} className="flex items-center gap-2 hover:text-emerald-200">
<Phone className="w-4 h-4" />
<span>{personalInfo.phone}</span>
</a>
)}
</div>
</div>
</div>
</div>
</div>
<div className="p-8">
<div className="grid md:grid-cols-12 gap-8">
<div className="md:col-span-8 space-y-8">
<section className="bg-white/70 p-6 rounded-xl shadow-lg">
<h2 className="text-2xl font-bold text-emerald-800 mb-4">Professional Journey</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="mb-6 relative pl-4 border-l-2 border-emerald-300">
<div className="absolute -left-1.5 top-1.5 w-3 h-3 rounded-full bg-emerald-500" />
<h3 className="text-xl font-semibold text-emerald-700">{exp.position}</h3>
<p className="text-teal-600 font-medium">{exp.company}</p>
<div className="flex items-center text-sm text-gray-600 mt-1 mb-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-600">{exp.description}</p>
</div>
))}
</section>
<section className="bg-white/70 p-6 rounded-xl shadow-lg">
<h2 className="text-2xl font-bold text-emerald-800 mb-4">Education & Learning</h2>
{resume.education.map((edu, index) => (
<div key={index} className="mb-4 last:mb-0">
<h3 className="text-lg font-semibold text-emerald-700">{edu.school}</h3>
<p className="text-teal-600">{edu.degree} in {edu.fieldOfStudy}</p>
<div className="flex items-center text-sm text-gray-600 mt-1">
<Calendar className="w-4 h-4 mr-1" />
<span>{edu.startDate} - {edu.endDate}</span>
</div>
</div>
))}
</section>
</div>
<div className="md:col-span-4 space-y-8">
<section className="bg-white/70 p-6 rounded-xl shadow-lg">
<h2 className="text-2xl font-bold text-emerald-800 mb-4">Expertise</h2>
<div className="space-y-4">
{resume.skills.map((skill, index) => (
<div key={index}>
<div className="flex justify-between mb-1">
<span className="text-emerald-700 font-medium">{skill.name}</span>
<span className="text-teal-600">{skill.level}/5</span>
</div>
<div className="h-2 bg-emerald-100 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-emerald-500 to-teal-500"
style={{ width: `${(skill.level / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
</section>
<section className="bg-white/70 p-6 rounded-xl shadow-lg">
<h2 className="text-2xl font-bold text-emerald-800 mb-4">Certifications</h2>
{resume.certifications.map((cert, index) => (
<div key={index} className="mb-4 last:mb-0">
<div className="flex items-start gap-2">
<Medal className="w-5 h-5 text-emerald-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-emerald-700">{cert.name}</h3>
<p className="text-teal-600 text-sm">{cert.issuer}</p>
<p className="text-gray-600 text-sm">{cert.date}</p>
</div>
</div>
</div>
))}
</section>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Mail, Phone, MapPin, Globe, Linkedin, Github, Calendar, Award, Medal, Heart } from 'lucide-react';
export function ElegantTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50">
<div className="bg-gradient-to-r from-amber-900 to-orange-900 text-white p-12">
<div className="max-w-3xl mx-auto text-center">
<h1 className="text-5xl font-serif font-bold">{personalInfo.fullName}</h1>
<p className="text-2xl text-amber-200 mt-2 font-serif">{personalInfo.jobTitle}</p>
<div className="flex justify-center gap-6 mt-6">
{personalInfo.email && (
<a href={`mailto:${personalInfo.email}`} className="flex items-center gap-2 text-amber-100 hover:text-white">
<Mail className="w-5 h-5" />
<span>{personalInfo.email}</span>
</a>
)}
{personalInfo.phone && (
<a href={`tel:${personalInfo.phone}`} className="flex items-center gap-2 text-amber-100 hover:text-white">
<Phone className="w-5 h-5" />
<span>{personalInfo.phone}</span>
</a>
)}
</div>
</div>
</div>
<div className="max-w-3xl mx-auto p-8">
<section className="mb-12 text-center">
<p className="text-lg text-gray-700 leading-relaxed font-serif">{personalInfo.summary}</p>
</section>
<div className="grid md:grid-cols-2 gap-12">
<section>
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-6 text-center">Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="mb-8 relative">
<div className="absolute top-0 -left-4 h-full w-0.5 bg-amber-200" />
<div className="absolute top-2 -left-6 w-4 h-4 rounded-full border-2 border-amber-400 bg-white" />
<h3 className="text-xl font-semibold text-amber-800">{exp.position}</h3>
<p className="text-orange-700 font-medium">{exp.company}</p>
<div className="flex items-center text-sm text-gray-600 mt-1 mb-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-600">{exp.description}</p>
</div>
))}
</section>
<div className="space-y-12">
<section>
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-6 text-center">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="mb-6 text-center">
<h3 className="text-xl font-semibold text-amber-800">{edu.school}</h3>
<p className="text-orange-700">{edu.degree}</p>
<p className="text-gray-600">{edu.fieldOfStudy}</p>
<div className="flex items-center justify-center text-sm text-gray-500 mt-1">
<Calendar className="w-4 h-4 mr-1" />
<span>{edu.startDate} - {edu.endDate}</span>
</div>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-6 text-center">Skills</h2>
<div className="grid grid-cols-2 gap-4">
{resume.skills.map((skill, index) => (
<div key={index} className="text-center">
<span className="inline-block px-4 py-2 bg-amber-100 text-amber-900 rounded-full font-medium">
{skill.name}
</span>
</div>
))}
</div>
</section>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Mail, Phone, MapPin, Globe, Linkedin, Github, Calendar, Award, Medal, Heart } from 'lucide-react';
export function GradientTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-gradient-to-br from-purple-50 via-pink-50 to-red-50">
<div className="bg-gradient-to-r from-purple-600 via-pink-600 to-red-600 text-white p-8 rounded-t-lg">
<div className="flex items-center gap-6">
{personalInfo.profilePicture && (
<img
src={personalInfo.profilePicture}
alt={personalInfo.fullName}
className="w-32 h-32 rounded-full object-cover border-4 border-white/20"
/>
)}
<div className="flex-1">
<h1 className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-pink-100">
{personalInfo.fullName}
</h1>
<p className="text-xl text-pink-100 mt-1">{personalInfo.jobTitle}</p>
<div className="flex flex-wrap gap-4 mt-4 text-sm">
{personalInfo.email && (
<div className="flex items-center gap-1">
<Mail className="w-4 h-4" />
<span>{personalInfo.email}</span>
</div>
)}
{personalInfo.phone && (
<div className="flex items-center gap-1">
<Phone className="w-4 h-4" />
<span>{personalInfo.phone}</span>
</div>
)}
{personalInfo.location && (
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
<span>{personalInfo.location}</span>
</div>
)}
</div>
</div>
</div>
</div>
<div className="p-8">
<div className="grid md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-6">
<section className="bg-white/60 p-6 rounded-lg shadow-lg backdrop-blur-sm">
<h2 className="text-2xl font-bold text-purple-800 mb-3">About Me</h2>
<p className="text-gray-700 leading-relaxed">{personalInfo.summary}</p>
</section>
<section>
<h2 className="text-2xl font-bold text-purple-800 mb-4">Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="bg-white/60 p-6 rounded-lg shadow-lg backdrop-blur-sm mb-4">
<h3 className="text-xl font-semibold text-pink-700">{exp.position}</h3>
<p className="text-purple-700 font-medium">{exp.company}</p>
<div className="flex items-center text-sm text-gray-600 mt-1 mb-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-600">{exp.description}</p>
</div>
))}
</section>
</div>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold text-purple-800 mb-4">Skills</h2>
<div className="bg-white/60 p-6 rounded-lg shadow-lg backdrop-blur-sm">
{resume.skills.map((skill, index) => (
<div key={index} className="mb-3">
<div className="flex justify-between mb-1">
<span className="text-purple-700 font-medium">{skill.name}</span>
<span className="text-pink-600">{skill.level}/5</span>
</div>
<div className="h-2 bg-purple-100 rounded-full">
<div
className="h-full rounded-full bg-gradient-to-r from-purple-500 to-pink-500"
style={{ width: `${(skill.level / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
</section>
<section>
<h2 className="text-2xl font-bold text-purple-800 mb-4">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="bg-white/60 p-6 rounded-lg shadow-lg backdrop-blur-sm mb-4">
<h3 className="text-lg font-semibold text-pink-700">{edu.school}</h3>
<p className="text-purple-700">{edu.degree}</p>
<p className="text-gray-600">{edu.fieldOfStudy}</p>
<div className="flex items-center text-sm text-gray-500 mt-1">
<Calendar className="w-4 h-4 mr-1" />
<span>{edu.startDate} - {edu.endDate}</span>
</div>
</div>
))}
</section>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Resume } from '../../types/resume';
export function MinimalTemplate({ resume }: { resume: Resume }) {
return (
<div className="max-w-4xl mx-auto bg-white shadow-lg p-8 rounded-lg">
<header className="mb-8">
<h1 className="text-4xl font-light text-gray-900">{resume.personalInfo.fullName}</h1>
<div className="mt-2 text-gray-600 text-sm">
<p>{resume.personalInfo.email} {resume.personalInfo.phone} {resume.personalInfo.location}</p>
</div>
<p className="mt-4 text-gray-700">{resume.personalInfo.summary}</p>
</header>
<section className="mb-8">
<h2 className="text-lg font-medium text-gray-900 uppercase tracking-wider mb-4">Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="mb-6">
<div className="flex justify-between items-baseline mb-1">
<h3 className="text-lg font-medium text-gray-900">{exp.position}</h3>
<span className="text-sm text-gray-600">{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-700 text-sm mb-2">{exp.company}</p>
<p className="text-gray-600 text-sm">{exp.description}</p>
</div>
))}
</section>
<section className="mb-8">
<h2 className="text-lg font-medium text-gray-900 uppercase tracking-wider mb-4">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="mb-4">
<div className="flex justify-between items-baseline mb-1">
<h3 className="text-lg font-medium text-gray-900">{edu.school}</h3>
<span className="text-sm text-gray-600">{edu.startDate} - {edu.endDate}</span>
</div>
<p className="text-gray-700 text-sm">{edu.degree} in {edu.fieldOfStudy}</p>
</div>
))}
</section>
<section>
<h2 className="text-lg font-medium text-gray-900 uppercase tracking-wider mb-4">Skills</h2>
<div className="flex flex-wrap gap-x-8 gap-y-2">
{resume.skills.map((skill, index) => (
<span key={index} className="text-gray-700 text-sm">
{skill.name}
</span>
))}
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Globe, Linkedin, Github } from 'lucide-react';
export function ModernTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-white shadow-lg p-8 rounded-lg">
<header className="flex items-center gap-6 mb-8">
{personalInfo.profilePicture && (
<img
src={personalInfo.profilePicture}
alt={personalInfo.fullName}
className="w-32 h-32 rounded-full object-cover border-4 border-gray-100"
/>
)}
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-800">{personalInfo.fullName}</h1>
<p className="text-xl text-gray-600 mt-1">{personalInfo.jobTitle}</p>
<div className="text-gray-600 mt-2">
<p>{personalInfo.email} | {personalInfo.phone}</p>
<p>{personalInfo.location}</p>
</div>
<div className="flex gap-4 mt-3">
{personalInfo.website && (
<a href={personalInfo.website} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700">
<Globe className="w-5 h-5" />
</a>
)}
{personalInfo.linkedin && (
<a href={personalInfo.linkedin} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700">
<Linkedin className="w-5 h-5" />
</a>
)}
{personalInfo.github && (
<a href={personalInfo.github} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700">
<Github className="w-5 h-5" />
</a>
)}
</div>
</div>
</header>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Professional Summary</h2>
<p className="text-gray-700">{personalInfo.summary}</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="mb-4">
<div className="flex justify-between items-baseline">
<h3 className="text-xl font-medium text-gray-800">{exp.position}</h3>
<span className="text-gray-600">{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-700 font-medium">{exp.company}</p>
<p className="text-gray-600 mt-2">{exp.description}</p>
</div>
))}
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="mb-4">
<div className="flex justify-between items-baseline">
<h3 className="text-xl font-medium text-gray-800">{edu.school}</h3>
<span className="text-gray-600">{edu.startDate} - {edu.endDate}</span>
</div>
<p className="text-gray-700">{edu.degree} in {edu.fieldOfStudy}</p>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Skills</h2>
<div className="grid grid-cols-2 gap-4">
{resume.skills.map((skill, index) => (
<div key={index} className="flex items-center gap-2">
<span className="font-medium text-gray-800">{skill.name}</span>
<div className="flex-1 h-2 bg-gray-200 rounded-full">
<div
className="h-full bg-blue-600 rounded-full"
style={{ width: `${(skill.level / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,132 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Mail, Phone, MapPin, Globe, Linkedin, Github, Calendar, Award, Medal, Heart } from 'lucide-react';
export function ProfessionalTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-gradient-to-br from-slate-50 to-gray-100">
<div className="bg-gradient-to-r from-slate-800 to-gray-900 text-white">
<div className="max-w-3xl mx-auto p-8">
<div className="flex items-center gap-8">
{personalInfo.profilePicture && (
<img
src={personalInfo.profilePicture}
alt={personalInfo.fullName}
className="w-40 h-40 rounded-full object-cover border-4 border-white/10 shadow-xl"
/>
)}
<div className="flex-1">
<h1 className="text-4xl font-bold">{personalInfo.fullName}</h1>
<p className="text-xl text-slate-300 mt-2">{personalInfo.jobTitle}</p>
<div className="grid grid-cols-2 gap-4 mt-4">
{personalInfo.email && (
<div className="flex items-center gap-2 text-slate-300">
<Mail className="w-4 h-4" />
<span>{personalInfo.email}</span>
</div>
)}
{personalInfo.phone && (
<div className="flex items-center gap-2 text-slate-300">
<Phone className="w-4 h-4" />
<span>{personalInfo.phone}</span>
</div>
)}
{personalInfo.location && (
<div className="flex items-center gap-2 text-slate-300">
<MapPin className="w-4 h-4" />
<span>{personalInfo.location}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div className="max-w-3xl mx-auto p-8">
<section className="mb-12">
<h2 className="text-2xl font-bold text-slate-800 mb-4">Professional Summary</h2>
<div className="bg-white p-6 rounded-lg shadow-lg">
<p className="text-gray-700 leading-relaxed">{personalInfo.summary}</p>
</div>
</section>
<div className="grid md:grid-cols-2 gap-8">
<section>
<h2 className="text-2xl font-bold text-slate-800 mb-4">Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="bg-white p-6 rounded-lg shadow-lg mb-4">
<h3 className="text-xl font-semibold text-slate-800">{exp.position}</h3>
<p className="text-slate-600 font-medium">{exp.company}</p>
<div className="flex items-center text-sm text-gray-500 mt-1 mb-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-600">{exp.description}</p>
</div>
))}
</section>
<div className="space-y-8">
<section>
<h2 className="text-2xl font-bold text-slate-800 mb-4">Skills & Expertise</h2>
<div className="bg-white p-6 rounded-lg shadow-lg">
<div className="grid grid-cols-2 gap-4">
{resume.skills.map((skill, index) => (
<div key={index} className="p-3 bg-slate-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-slate-800">{skill.name}</span>
<span className="text-sm text-slate-600">{skill.level}/5</span>
</div>
<div className="h-1.5 bg-slate-200 rounded-full">
<div
className="h-full rounded-full bg-slate-600"
style={{ width: `${(skill.level / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold text-slate-800 mb-4">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="bg-white p-6 rounded-lg shadow-lg mb-4">
<h3 className="text-lg font-semibold text-slate-800">{edu.school}</h3>
<p className="text-slate-700">{edu.degree}</p>
<p className="text-slate-600">{edu.fieldOfStudy}</p>
<div className="flex items-center text-sm text-gray-500 mt-1">
<Calendar className="w-4 h-4 mr-1" />
<span>{edu.startDate} - {edu.endDate}</span>
</div>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-bold text-slate-800 mb-4">Certifications</h2>
<div className="bg-white p-6 rounded-lg shadow-lg">
{resume.certifications.map((cert, index) => (
<div key={index} className="mb-4 last:mb-0">
<div className="flex items-start gap-3">
<Medal className="w-5 h-5 text-slate-700 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-slate-800">{cert.name}</h3>
<p className="text-slate-600">{cert.issuer}</p>
<p className="text-sm text-gray-500">{cert.date}</p>
</div>
</div>
</div>
))}
</div>
</section>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,141 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Mail, Phone, MapPin, Globe, Linkedin, Github, Calendar, Award, Medal, Heart, Terminal } from 'lucide-react';
export function TechTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-gray-900 text-gray-300">
<div className="border-b border-gray-800">
<div className="p-8">
<div className="flex items-center gap-6">
{personalInfo.profilePicture && (
<img
src={personalInfo.profilePicture}
alt={personalInfo.fullName}
className="w-32 h-32 rounded-lg object-cover border-2 border-indigo-500"
/>
)}
<div className="flex-1">
<h1 className="text-4xl font-bold text-indigo-400">{personalInfo.fullName}</h1>
<p className="text-xl text-indigo-300 mt-1">{personalInfo.jobTitle}</p>
<div className="flex flex-wrap gap-4 mt-4">
<div className="flex items-center gap-2 text-gray-400">
<Terminal className="w-4 h-4 text-indigo-400" />
<span>{personalInfo.email}</span>
</div>
<div className="flex items-center gap-2 text-gray-400">
<Phone className="w-4 h-4 text-indigo-400" />
<span>{personalInfo.phone}</span>
</div>
<div className="flex items-center gap-2 text-gray-400">
<MapPin className="w-4 h-4 text-indigo-400" />
<span>{personalInfo.location}</span>
</div>
</div>
<div className="flex gap-4 mt-4">
{personalInfo.github && (
<a href={personalInfo.github} target="_blank" rel="noopener noreferrer"
className="text-indigo-400 hover:text-indigo-300">
<Github className="w-5 h-5" />
</a>
)}
{personalInfo.linkedin && (
<a href={personalInfo.linkedin} target="_blank" rel="noopener noreferrer"
className="text-indigo-400 hover:text-indigo-300">
<Linkedin className="w-5 h-5" />
</a>
)}
</div>
</div>
</div>
</div>
</div>
<div className="p-8">
<div className="grid md:grid-cols-3 gap-8">
<div className="md:col-span-2 space-y-8">
<section>
<div className="flex items-center gap-2 mb-6">
<Terminal className="w-6 h-6 text-indigo-400" />
<h2 className="text-2xl font-bold text-indigo-400">Experience</h2>
</div>
{resume.experience.map((exp, index) => (
<div key={index} className="mb-6 bg-gray-800 p-6 rounded-lg">
<h3 className="text-xl font-semibold text-indigo-300">{exp.position}</h3>
<p className="text-indigo-400 font-medium">{exp.company}</p>
<div className="flex items-center text-sm text-gray-500 mt-1 mb-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
<p className="text-gray-400">{exp.description}</p>
</div>
))}
</section>
<section>
<div className="flex items-center gap-2 mb-6">
<Award className="w-6 h-6 text-indigo-400" />
<h2 className="text-2xl font-bold text-indigo-400">Projects & Achievements</h2>
</div>
<div className="grid gap-6">
{resume.achievements.map((achievement, index) => (
<div key={index} className="bg-gray-800 p-6 rounded-lg">
<h3 className="text-xl font-semibold text-indigo-300">{achievement.title}</h3>
<p className="text-gray-400 mt-2">{achievement.description}</p>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{achievement.date}</span>
</div>
</div>
))}
</div>
</section>
</div>
<div className="space-y-8">
<section>
<div className="flex items-center gap-2 mb-6">
<Terminal className="w-6 h-6 text-indigo-400" />
<h2 className="text-2xl font-bold text-indigo-400">Tech Stack</h2>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
{resume.skills.map((skill, index) => (
<div key={index} className="mb-4">
<div className="flex justify-between mb-1">
<span className="text-indigo-300 font-medium">{skill.name}</span>
<span className="text-indigo-400">{skill.level}/5</span>
</div>
<div className="h-2 bg-gray-700 rounded-full">
<div
className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-indigo-400"
style={{ width: `${(skill.level / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
</section>
<section>
<div className="flex items-center gap-2 mb-6">
<Medal className="w-6 h-6 text-indigo-400" />
<h2 className="text-2xl font-bold text-indigo-400">Certifications</h2>
</div>
<div className="bg-gray-800 p-6 rounded-lg">
{resume.certifications.map((cert, index) => (
<div key={index} className="mb-4 last:mb-0">
<h3 className="text-indigo-300 font-medium">{cert.name}</h3>
<p className="text-gray-400 text-sm">{cert.issuer}</p>
<p className="text-gray-500 text-sm">{cert.date}</p>
</div>
))}
</div>
</section>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,202 @@
import React from 'react';
import { Resume } from '../../types/resume';
import { Mail, Phone, MapPin, Globe, Linkedin, Github, Calendar, Award, Medal, Heart } from 'lucide-react';
export function VibrantTemplate({ resume }: { resume: Resume }) {
const { personalInfo } = resume;
return (
<div className="max-w-4xl mx-auto bg-gradient-to-br from-blue-50 to-slate-100">
<div className="bg-gradient-to-r from-blue-800 to-blue-900 text-white p-8 rounded-t-lg">
<div className="flex items-center gap-6">
{personalInfo.profilePicture && (
<img
src={personalInfo.profilePicture}
alt={personalInfo.fullName}
className="w-32 h-32 rounded-full object-cover border-4 border-white/20"
/>
)}
<div className="flex-1">
<h1 className="text-4xl font-bold">{personalInfo.fullName}</h1>
<p className="text-xl text-blue-100 mt-1">{personalInfo.jobTitle}</p>
<div className="flex flex-wrap gap-4 mt-4 text-sm">
{personalInfo.email && (
<div className="flex items-center gap-1">
<Mail className="w-4 h-4" />
<span>{personalInfo.email}</span>
</div>
)}
{personalInfo.phone && (
<div className="flex items-center gap-1">
<Phone className="w-4 h-4" />
<span>{personalInfo.phone}</span>
</div>
)}
{personalInfo.location && (
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
<span>{personalInfo.location}</span>
</div>
)}
</div>
<div className="flex gap-4 mt-4">
{personalInfo.website && (
<a href={personalInfo.website} target="_blank" rel="noopener noreferrer"
className="text-blue-100 hover:text-white transition-colors">
<Globe className="w-5 h-5" />
</a>
)}
{personalInfo.linkedin && (
<a href={personalInfo.linkedin} target="_blank" rel="noopener noreferrer"
className="text-blue-100 hover:text-white transition-colors">
<Linkedin className="w-5 h-5" />
</a>
)}
{personalInfo.github && (
<a href={personalInfo.github} target="_blank" rel="noopener noreferrer"
className="text-blue-100 hover:text-white transition-colors">
<Github className="w-5 h-5" />
</a>
)}
</div>
</div>
</div>
</div>
<div className="p-8">
{personalInfo.summary && (
<section className="mb-8 bg-white/60 p-6 rounded-lg backdrop-blur-sm">
<h2 className="text-2xl font-bold text-slate-900 mb-3">About Me</h2>
<p className="text-slate-700 leading-relaxed">{personalInfo.summary}</p>
</section>
)}
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold text-slate-900 mb-4">Experience</h2>
{resume.experience.map((exp, index) => (
<div key={index} className="bg-white/60 p-4 rounded-lg backdrop-blur-sm mb-4">
<div className="flex justify-between items-baseline mb-2">
<h3 className="text-lg font-semibold text-blue-900">{exp.position}</h3>
<div className="flex items-center text-sm text-slate-600">
<Calendar className="w-4 h-4 mr-1" />
<span>{exp.startDate} - {exp.endDate}</span>
</div>
</div>
<p className="text-blue-800 font-medium mb-2">{exp.company}</p>
<p className="text-slate-600 text-sm">{exp.description}</p>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-bold text-slate-900 mb-4">Achievements</h2>
{resume.achievements.map((achievement, index) => (
<div key={index} className="bg-white/60 p-4 rounded-lg backdrop-blur-sm mb-4">
<div className="flex items-start gap-3">
<Award className="w-5 h-5 text-blue-700 flex-shrink-0 mt-1" />
<div>
<h3 className="text-lg font-semibold text-blue-900">{achievement.title}</h3>
<p className="text-slate-600 text-sm mt-1">{achievement.description}</p>
<div className="flex items-center text-sm text-slate-500 mt-2">
<Calendar className="w-4 h-4 mr-1" />
<span>{achievement.date}</span>
</div>
</div>
</div>
</div>
))}
</section>
</div>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold text-slate-900 mb-4">Education</h2>
{resume.education.map((edu, index) => (
<div key={index} className="bg-white/60 p-4 rounded-lg backdrop-blur-sm mb-4">
<h3 className="text-lg font-semibold text-blue-900">{edu.school}</h3>
<p className="text-blue-800">{edu.degree} in {edu.fieldOfStudy}</p>
<div className="flex items-center text-sm text-slate-600 mt-1">
<Calendar className="w-4 h-4 mr-1" />
<span>{edu.startDate} - {edu.endDate}</span>
</div>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-bold text-slate-900 mb-4">Certifications</h2>
{resume.certifications.map((cert, index) => (
<div key={index} className="bg-white/60 p-4 rounded-lg backdrop-blur-sm mb-4">
<div className="flex items-start gap-3">
<Medal className="w-5 h-5 text-blue-700 flex-shrink-0 mt-1" />
<div>
<h3 className="text-lg font-semibold text-blue-900">{cert.name}</h3>
<p className="text-blue-800">{cert.issuer}</p>
<div className="flex items-center text-sm text-slate-600 mt-1">
<Calendar className="w-4 h-4 mr-1" />
<span>{cert.date}{cert.expiryDate ? ` - ${cert.expiryDate}` : ''}</span>
</div>
{cert.credentialUrl && (
<a
href={cert.credentialUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-700 mt-1 inline-block"
>
View Credential
</a>
)}
</div>
</div>
</div>
))}
</section>
<section>
<h2 className="text-2xl font-bold text-slate-900 mb-4">Skills</h2>
<div className="bg-white/60 p-4 rounded-lg backdrop-blur-sm">
<div className="grid gap-3">
{resume.skills.map((skill, index) => (
<div key={index}>
<div className="flex justify-between items-center mb-1">
<span className="text-blue-900 font-medium">{skill.name}</span>
<span className="text-sm text-blue-700">{skill.level}/5</span>
</div>
<div className="h-2 bg-blue-100 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-600 to-blue-800 rounded-full transition-all duration-300"
style={{ width: `${(skill.level / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold text-slate-900 mb-4">Hobbies & Interests</h2>
<div className="bg-white/60 p-4 rounded-lg backdrop-blur-sm">
<div className="grid gap-4">
{resume.hobbies.map((hobby, index) => (
<div key={index} className="flex items-start gap-3">
<Heart className="w-5 h-5 text-blue-700 flex-shrink-0 mt-1" />
<div>
<h3 className="text-blue-900 font-medium">{hobby.name}</h3>
{hobby.description && (
<p className="text-slate-600 text-sm mt-1">{hobby.description}</p>
)}
</div>
</div>
))}
</div>
</div>
</section>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { Resume } from '../types/resume';
export const initialResume: Resume = {
personalInfo: {
fullName: '',
jobTitle: '',
email: '',
phone: '',
location: '',
website: '',
linkedin: '',
github: '',
summary: '',
profilePicture: '',
},
education: [],
experience: [],
skills: [],
achievements: [],
certifications: [],
hobbies: [],
};

18
src/index.css Normal file
View File

@@ -0,0 +1,18 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@media print {
body * {
visibility: hidden;
}
.preview-wrapper, .preview-wrapper * {
visibility: visible;
}
.preview-wrapper {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

72
src/types/resume.ts Normal file
View File

@@ -0,0 +1,72 @@
export interface PersonalInfo {
fullName: string;
jobTitle: string;
email: string;
phone: string;
location: string;
website: string;
linkedin: string;
github: string;
summary: string;
profilePicture: string;
}
export interface Education {
school: string;
degree: string;
fieldOfStudy: string;
startDate: string;
endDate: string;
gpa: string;
honors: string[];
activities: string[];
description: string;
location: string;
thesis?: string;
advisors?: string[];
relevantCourses: string[];
}
export interface Experience {
company: string;
position: string;
startDate: string;
endDate: string;
description: string;
}
export interface Achievement {
title: string;
description: string;
date: string;
}
export interface Certification {
name: string;
issuer: string;
date: string;
expiryDate?: string;
credentialUrl?: string;
}
export interface Hobby {
name: string;
description?: string;
}
export interface Skill {
name: string;
level: number;
}
export interface Resume {
personalInfo: PersonalInfo;
education: Education[];
experience: Experience[];
skills: Skill[];
achievements: Achievement[];
certifications: Certification[];
hobbies: Hobby[];
}
export type TemplateType = 'modern' | 'classic' | 'minimal' | 'vibrant' | 'gradient' | 'creative' | 'elegant' | 'tech' | 'professional';

View File

@@ -0,0 +1,40 @@
const summaries: Record<string, string[]> = {
developer: [
"Innovative software developer with a passion for creating efficient, scalable solutions. Experienced in full-stack development with a strong foundation in modern programming languages and frameworks. Committed to writing clean, maintainable code and staying current with emerging technologies.",
"Results-driven software developer combining technical expertise with strong problem-solving abilities. Proven track record of delivering high-quality applications while adhering to best practices and industry standards. Experienced in agile development methodologies and cross-functional team collaboration.",
"Detail-oriented software developer with expertise in building robust applications. Skilled in translating business requirements into technical solutions while ensuring optimal performance and user experience. Strong advocate for code quality and continuous improvement."
],
designer: [
"Creative UI/UX designer with a keen eye for detail and a user-centered approach. Experienced in creating intuitive, accessible interfaces that enhance user engagement and satisfaction. Proficient in modern design tools and principles.",
"Innovative digital designer specializing in creating visually compelling and functional user experiences. Combines artistic vision with technical expertise to deliver designs that exceed client expectations. Strong background in user research and iterative design processes.",
"Forward-thinking designer with a passion for creating beautiful, user-friendly interfaces. Experienced in working with cross-functional teams to deliver cohesive design solutions. Strong foundation in design principles and modern design systems."
],
manager: [
"Dynamic project manager with proven success in leading cross-functional teams and delivering complex projects on time and within budget. Skilled in stakeholder management, risk mitigation, and resource optimization.",
"Results-oriented manager with extensive experience in team leadership and strategic planning. Proven track record of improving operational efficiency and fostering a positive work environment. Strong focus on team development and organizational growth.",
"Experienced manager with a track record of successful project delivery and team leadership. Skilled in strategic planning, process improvement, and change management. Strong communicator with expertise in stakeholder management."
]
};
const defaultSummaries = [
"Dedicated professional with a strong track record of success in delivering results. Combines technical expertise with excellent communication skills to drive project success. Committed to continuous learning and professional growth.",
"Results-driven professional with expertise in implementing innovative solutions. Strong analytical and problem-solving abilities combined with excellent interpersonal skills. Proven ability to work effectively in fast-paced environments.",
"Experienced professional with a proven track record of success. Skilled in project management, team collaboration, and strategic planning. Strong focus on achieving organizational goals while maintaining high quality standards."
];
export async function generateSummary(jobTitle: string): Promise<string> {
const lowerTitle = jobTitle.toLowerCase();
let relevantSummaries = defaultSummaries;
// Match job title with available templates
for (const [key, summaryList] of Object.entries(summaries)) {
if (lowerTitle.includes(key)) {
relevantSummaries = summaryList;
break;
}
}
// Randomly select a summary from the relevant list
const randomIndex = Math.floor(Math.random() * relevantSummaries.length);
return relevantSummaries[randomIndex];
}

136
src/utils/wordExport.ts Normal file
View File

@@ -0,0 +1,136 @@
import { Document, Packer, Paragraph, TextRun } from 'docx';
import { saveAs } from 'file-saver';
import { Resume } from '../types/resume';
export async function exportToWord(resume: Resume) {
const doc = new Document({
sections: [{
properties: {},
children: [
new Paragraph({
children: [
new TextRun({
text: resume.personalInfo.fullName,
bold: true,
size: 32,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: resume.personalInfo.jobTitle,
size: 24,
color: '666666',
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: `${resume.personalInfo.email} | ${resume.personalInfo.phone} | ${resume.personalInfo.location}`,
size: 20,
}),
],
}),
new Paragraph({
children: [new TextRun({ text: '\n' })],
}),
new Paragraph({
children: [
new TextRun({
text: 'Professional Summary',
bold: true,
size: 24,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: resume.personalInfo.summary,
size: 20,
}),
],
}),
// Experience Section
...resume.experience.flatMap(exp => [
new Paragraph({
children: [
new TextRun({
text: '\nExperience',
bold: true,
size: 24,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: exp.position,
bold: true,
size: 20,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: `${exp.company} (${exp.startDate} - ${exp.endDate})`,
size: 20,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: exp.description,
size: 20,
}),
],
}),
]),
// Education Section
...resume.education.flatMap(edu => [
new Paragraph({
children: [
new TextRun({
text: '\nEducation',
bold: true,
size: 24,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: edu.school,
bold: true,
size: 20,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: `${edu.degree} in ${edu.fieldOfStudy}`,
size: 20,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: `${edu.startDate} - ${edu.endDate}`,
size: 20,
}),
],
}),
]),
],
}],
});
const blob = await Packer.toBlob(doc);
saveAs(blob, `${resume.personalInfo.fullName.replace(/\s+/g, '_')}_Resume.docx`);
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

8
tailwind.config.js Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

24
tsconfig.app.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

10
vite.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});