chore: initiate local project to github
This commit is contained in:
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"template": "bolt-vite-react-ts"
|
||||||
|
}
|
8
.bolt/prompt
Normal file
8
.bolt/prompt
Normal 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
24
.gitignore
vendored
Normal 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
28
eslint.config.js
Normal 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
13
index.html
Normal 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
4212
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
59
package.json
Normal file
59
package.json
Normal 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
2699
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
102
src/App.tsx
Normal file
102
src/App.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
49
src/components/ResumeForm.tsx
Normal file
49
src/components/ResumeForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
18
src/components/common/Footer.tsx
Normal file
18
src/components/common/Footer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
18
src/components/common/PrintCredit.tsx
Normal file
18
src/components/common/PrintCredit.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
72
src/components/forms/AchievementsForm.tsx
Normal file
72
src/components/forms/AchievementsForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
89
src/components/forms/CertificationsForm.tsx
Normal file
89
src/components/forms/CertificationsForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
288
src/components/forms/EducationForm.tsx
Normal file
288
src/components/forms/EducationForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
90
src/components/forms/ExperienceForm.tsx
Normal file
90
src/components/forms/ExperienceForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
65
src/components/forms/HobbiesForm.tsx
Normal file
65
src/components/forms/HobbiesForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
47
src/components/forms/ImageUpload.tsx
Normal file
47
src/components/forms/ImageUpload.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
106
src/components/forms/PersonalInfoForm.tsx
Normal file
106
src/components/forms/PersonalInfoForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
65
src/components/forms/SkillsForm.tsx
Normal file
65
src/components/forms/SkillsForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
46
src/components/forms/SocialLinks.tsx
Normal file
46
src/components/forms/SocialLinks.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
58
src/components/templates/ClassicTemplate.tsx
Normal file
58
src/components/templates/ClassicTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
118
src/components/templates/CreativeTemplate.tsx
Normal file
118
src/components/templates/CreativeTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
87
src/components/templates/ElegantTemplate.tsx
Normal file
87
src/components/templates/ElegantTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
112
src/components/templates/GradientTemplate.tsx
Normal file
112
src/components/templates/GradientTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
54
src/components/templates/MinimalTemplate.tsx
Normal file
54
src/components/templates/MinimalTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
95
src/components/templates/ModernTemplate.tsx
Normal file
95
src/components/templates/ModernTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
132
src/components/templates/ProfessionalTemplate.tsx
Normal file
132
src/components/templates/ProfessionalTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
141
src/components/templates/TechTemplate.tsx
Normal file
141
src/components/templates/TechTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
202
src/components/templates/VibrantTemplate.tsx
Normal file
202
src/components/templates/VibrantTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
22
src/constants/initialState.ts
Normal file
22
src/constants/initialState.ts
Normal 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
18
src/index.css
Normal 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
10
src/main.tsx
Normal 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
72
src/types/resume.ts
Normal 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';
|
40
src/utils/summaryGenerator.ts
Normal file
40
src/utils/summaryGenerator.ts
Normal 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
136
src/utils/wordExport.ts
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal 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
24
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal 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
10
vite.config.ts
Normal 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'],
|
||||||
|
},
|
||||||
|
});
|
Reference in New Issue
Block a user