Learn how to build a professional, open-source CV/Resume builder application with real-time preview, PDF export, and Firebase integration.
🎯 Introduction
In this comprehensive guide, I'll walk you through building a modern CV/Resume builder application from scratch. This project demonstrates advanced React patterns, TypeScript best practices, Firebase integration, and performance optimization techniques that you can apply to your own projects.
What we're building:
- A full-featured CV builder with 11 beautiful templates
- Real-time preview with side-by-side editing
- PDF export and import functionality
- Firebase authentication and data persistence
- Performance-optimized components
- A 6-step guided wizard for resume creation
Repository: GitHub - resume-studio
🚀 Project Overview
This CV builder is a production-ready application that showcases modern web development practices. It's built with React 19, TypeScript, and Firebase, providing a seamless user experience for creating professional resumes.
Key Features
- ✨ 11 Professional Templates - From minimalist to modern designs
- 🎨 Real-time Preview - See changes instantly as you type
- 📄 PDF Export/Import - Export your CV or upload existing PDFs
- 🔐 Firebase Authentication - Email, Google, and Twitter sign-in
- 💾 Auto-save - Your progress is automatically saved
- ⚡ Performance Optimized - Memoized components prevent unnecessary re-renders
- 📱 Responsive Design - Works perfectly on all devices
🛠️ Tech Stack
Here's what powers this application:
- React 19 - Latest React with hooks and memoization
- TypeScript - Type-safe development
- Vite 7 - Lightning-fast build tool
- Tailwind CSS - Utility-first styling
- Firebase - Authentication, Firestore, and Storage
- React Router v7 - Client-side routing
- jsPDF & html2canvas - PDF generation
- pdfjs-dist - PDF parsing and data extraction
📐 Architecture & Design Patterns
1. Component Structure
The application follows a modular component architecture:
src/
├── components/
│ ├── common/ # Reusable UI components
│ ├── dashboard/ # Dashboard components
│ ├── layout/ # Layout components
│ ├── templates/ # 11 CV template components
│ └── wizard/ # Wizard step components
├── hooks/ # Custom React hooks
├── services/ # Firebase services
├── contexts/ # React contexts
└── utils/ # Utility functions
2. Custom Hooks for State Management
We use custom hooks to encapsulate state logic and make components cleaner:
// src/hooks/useCVData.ts
import { useState, useEffect, useCallback } from 'react';
import type { CVData } from '../types/cv.types';
import { saveCVData, loadCVData } from '../utils/dataPersistence';
export const useCVData = () => {
const [cvData, setCvData] = useState<CVData>(freshCVData);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const savedData = loadCVData();
if (savedData) {
setCvData(savedData);
}
setIsLoading(false);
}, []);
useEffect(() => {
if (!isLoading) {
saveCVData(cvData);
}
}, [cvData, isLoading]);
const updateCVData = useCallback((newData: CVData) => {
setCvData(newData);
}, []);
return { cvData, updateCVData, isLoading };
};
This hook provides:
- Automatic loading from localStorage
- Auto-save functionality
- Stable function references with
useCallback
3. Performance Optimization with React.memo
One of the critical challenges in a real-time editor is preventing unnecessary re-renders. Here's how we solved it:
The Problem
When typing in an input field, every keystroke would trigger a state update, causing the entire component tree to re-render. This led to:
- Input fields losing focus
- Poor typing performance
- Unnecessary DOM updates
The Solution
We memoized components and used refs to maintain stable function references:
// src/components/common/Input.tsx
import { memo, useMemo } from 'react';
import type { FC, InputHTMLAttributes } from 'react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
}
const Input: FC<InputProps> = ({
label,
error,
helperText,
className = '',
id,
...props
}) => {
const inputId = useMemo(() =>
id || `input-${Math.random().toString(36).substr(2, 9)}`,
[id]
);
// ... rest of component
};
export default memo(Input);
CollapsibleSection Optimization
The biggest performance win came from moving CollapsibleSection outside the parent component:
// ❌ Before: Defined inside component (recreated on every render)
const CVEditorCollapsible = ({ data, onChange }) => {
const CollapsibleSection = ({ ... }) => ( ... ); // Recreated!
return <CollapsibleSection ... />;
};
// ✅ After: Defined outside (stable component identity)
const CollapsibleSection: FC<CollapsibleSectionProps> = memo(({
title, icon, iconColor, children, isExpanded, onToggle
}) => (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden mb-6">
{/* ... */}
</div>
));
const CVEditorCollapsible = ({ data, onChange }) => {
return <CollapsibleSection ... />;
};
Why this matters: When CollapsibleSection was defined inside the parent, React saw it as a new component type on every render, causing all children to unmount and remount. Moving it outside gives it a stable identity, allowing React to properly optimize re-renders.
4. Firebase Integration
Authentication Setup
// src/config/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
Service Layer Pattern
We use a service layer to abstract Firebase operations:
// src/services/authService.ts
import {
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signInWithPopup,
GoogleAuthProvider,
TwitterAuthProvider,
onAuthStateChanged,
} from 'firebase/auth';
import { auth } from '../config/firebase';
export const signInUser = async (email: string, password: string) => {
return await signInWithEmailAndPassword(auth, email, password);
};
export const signUpUser = async (email: string, password: string) => {
return await createUserWithEmailAndPassword(auth, email, password);
};
export const signInWithGoogle = async () => {
const provider = new GoogleAuthProvider();
return await signInWithPopup(auth, provider);
};
export const onAuthStateChange = (callback: (user: any) => void) => {
return onAuthStateChanged(auth, callback);
};
This pattern provides:
- Centralized Firebase logic
- Easy testing and mocking
- Consistent error handling
- Type safety
🎨 Key Features Implementation
1. Real-time Preview
The editor uses a two-column layout with synchronized state:
// src/pages/EditorPage.tsx
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Left: Editor */}
<div className="overflow-y-auto pr-2">
<CVEditorCollapsible
data={cvData}
onChange={(newData) => updateCVData(newData)}
/>
</div>
{/* Right: Preview */}
<div className="overflow-y-auto">
<div id="cv-export-content">
{renderTemplate(selectedTemplate, cvData)}
</div>
</div>
</div>
Every change in the editor updates cvData, which triggers a re-render of the preview panel.
2. PDF Export
PDF export uses html2canvas and jsPDF:
// src/utils/exportPDF.ts
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
export const exportToPDF = async (
elementId: string,
filename: string
): Promise<void> => {
const element = document.getElementById(elementId);
if (!element) throw new Error('Element not found');
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
logging: false,
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 width in mm
const imgHeight = (canvas.height * imgWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
pdf.save(filename);
};
3. PDF Import & Data Extraction
Users can upload existing PDFs and extract data:
// src/utils/exportPDF.ts (simplified)
import * as pdfjsLib from 'pdfjs-dist';
export const extractTextFromPDF = async (file: File): Promise<string> => {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += pageText + '\n';
}
return fullText;
};
4. 6-Step Wizard
The wizard guides users through resume creation:
// src/pages/ResumeWizard.tsx
const steps = [
{ id: 'personal', title: 'Personal Information', component: PersonalInfoStep },
{ id: 'work', title: 'Work Experience', component: WorkExperienceStep },
{ id: 'education', title: 'Education', component: EducationStep },
{ id: 'skills', title: 'Skills', component: SkillsStep },
{ id: 'summary', title: 'Summary', component: SummaryStep },
{ id: 'review', title: 'Review', component: ReviewStep },
];
const handleComplete = () => {
updateCVData(wizardData);
const selectedTemplate = localStorage.getItem('selectedTemplate') || '1';
navigate(`/editor/${selectedTemplate}`);
};
Each step validates input before allowing progression, ensuring data quality.
🔧 Performance Optimizations
1. Component Memoization
We memoized all frequently re-rendered components:
-
Input,Textarea,Date- UsingReact.memo -
CollapsibleSection- Moved outside and memoized - Event handlers - Using
useCallback
2. Stable Function References
const handlePersonalInfoChange = useCallback((field: string, value: string) => {
const currentData = dataRef.current;
onChangeRef.current({
...currentData,
personalInfo: {
...currentData.personalInfo,
[field]: value,
},
});
}, []); // Empty deps - uses refs instead
By using refs (dataRef, onChangeRef), we can keep the dependency array empty, ensuring the function reference never changes.
3. Local Storage Caching
CV data is automatically cached in localStorage:
useEffect(() => {
if (!isLoading) {
saveCVData(cvData); // Auto-save on every change
}
}, [cvData, isLoading]);
This provides:
- Instant loading on page refresh
- Offline capability
- Reduced Firebase read operations
📱 Getting Started
Prerequisites
- Node.js 16+
- npm or yarn
- Firebase account (free tier works)
Installation
# Clone the repository
git clone https://github.com/AhmadFaraz-crypto/resume-studio.git
cd resume-studio
# Install dependencies
npm install
# Set up environment variables
cp env.example .env.local
# Add your Firebase config to .env.local
VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
VITE_FIREBASE_APP_ID=your-app-id
# Start development server
npm run dev
Firebase Setup
-
Create Firebase Project
- Go to Firebase Console
- Create a new project
-
Enable Services
- Authentication: Enable Email/Password, Google, Twitter
- Firestore: Create database in test mode
- Storage: Create storage bucket in test mode
-
Get Configuration
- Project Settings → Your apps → Web app
- Copy config values to
.env.local
🎯 Key Learnings
1. Performance Matters
Memoization isn't just about optimization—it's about user experience. Preventing unnecessary re-renders ensures:
- Smooth typing experience
- No focus loss
- Better perceived performance
2. Component Architecture
Moving components outside their parents when they're recreated on every render is crucial. React needs stable component identities for proper optimization.
3. TypeScript is Your Friend
Strong typing catches bugs early and makes refactoring safer:
interface CVData {
personalInfo: {
name: string;
email: string;
phone: string;
location: {
city: string;
zipCode: string;
country: string;
};
linkedin?: string;
summary: string;
};
workExperience: WorkExperience[];
education: Education[];
skills: string[];
}
4. Service Layer Pattern
Abstracting Firebase operations into services provides:
- Better testability
- Easier maintenance
- Consistent error handling
🚀 Deployment
The application can be deployed to any static hosting platform:
Netlify
# Build command
npm run build
# Publish directory
dist
Add environment variables in Netlify dashboard.
Vercel
# Framework: Vite
# Build command: npm run build
# Output directory: dist
Environment variables are configured in Vercel dashboard.
🤝 Contributing
Contributions are welcome! Areas for contribution:
- 🎨 New template designs
- 🐛 Bug fixes
- 📚 Documentation improvements
- ♿ Accessibility enhancements
- 🌍 Internationalization
- 🧪 Tests
- 🎯 Performance optimizations
📚 Resources
🎉 Conclusion
Building this CV builder taught me valuable lessons about:
- React performance optimization
- TypeScript best practices
- Firebase integration patterns
- Component architecture
- User experience design
The application is fully open-source and production-ready. You can use it as:
- A learning resource
- A starting point for your own projects
- A portfolio showcase
- A contribution opportunity
Star the repository if you find it helpful: GitHub - resume-studio
💬 Questions?
Have questions or suggestions? Feel free to:
- Open an issue on GitHub
- Start a discussion
- Submit a pull request
Top comments (0)