How to Build a CV Builder with Next.js and Tailwind CSS
In this tutorial, we will cover setting up the project, building a form for user input, displaying the data in a CV-like format, generating a PDF, downloading a CSV file, and saving data to local storage.
Creating a CV builder application using Next.js and Tailwind CSS can be a great way to streamline the process of creating professional resumes. In this tutorial, we will cover setting up the project, building a form for user input, displaying the data in a CV-like format, generating a PDF, downloading a CSV file, and saving data to local storage.
Step 1: Set Up the Project
First, we need to set up a new Next.js project and install the necessary dependencies.
Create a New Next.js Project
1npx create-next-app cv-builder
2cd cv-builder
3npm install html2canvas jspdf react-csv
Install Tailwind CSS
1npm install -D tailwindcss postcss autoprefixer
2npx tailwindcss init -p
Configure Tailwind CSS
In tailwind.config.js, configure the paths to all of your template files:
1/** @type {import('tailwindcss').Config} */
2module.exports = {
3 content: [
4 './pages/**/*.{js,ts,jsx,tsx}',
5 './components/**/*.{js,ts,jsx,tsx}',
6 ],
7 theme: {
8 extend: {},
9 },
10 plugins: [],
11}
Create a globals.css file in the styles directory and import Tailwind CSS:
1/* styles/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
Import globals.css in your _app.js:
1// pages/_app.js
2import '../styles/globals.css';
3
4function MyApp({ Component, pageProps }) {
5 return <Component {...pageProps} />;
6}
7
8export default MyApp;
Step 2: Create the Form for User Input
Create a form to collect personal details, education, work experience, skills, and projects.
Create the Form Component
Update pages/index.js:
1import { useState, useRef, useEffect } from 'react';
2import { CSVLink } from 'react-csv';
3import html2canvas from 'html2canvas';
4import jsPDF from 'jspdf';
5
6export default function Home() {
7 const [formData, setFormData] = useState({
8 name: '',
9 email: '',
10 phone: '',
11 summary: '',
12 education: '',
13 experience: '',
14 skills: '',
15 projects: ''
16 });
17
18 const [submittedData, setSubmittedData] = useState([]);
19 const pdfRef = useRef();
20
21 useEffect(() => {
22 const savedData = localStorage.getItem('cvData');
23 if (savedData) {
24 setFormData(JSON.parse(savedData));
25 setSubmittedData([JSON.parse(savedData)]);
26 }
27 }, []);
28
29 const handleChange = (e) => {
30 setFormData({ ...formData, [e.target.name]: e.target.value });
31 };
32
33 const handleSubmit = (e) => {
34 e.preventDefault();
35 setSubmittedData([formData]);
36 localStorage.setItem('cvData', JSON.stringify(formData));
37 };
38
39 const generatePDF = async () => {
40 const element = pdfRef.current;
41 const canvas = await html2canvas(element);
42 const imgData = canvas.toDataURL('image/png');
43 const pdf = new jsPDF();
44 const imgProps = pdf.getImageProperties(imgData);
45 const pdfWidth = pdf.internal.pageSize.getWidth();
46 const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
47 pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
48 pdf.save('cv.pdf');
49 };
50
51 return (
52 <div className="container mx-auto p-4">
53 <form onSubmit={handleSubmit} className="grid gap-4 max-w-lg mx-auto">
54 <input
55 name="name"
56 value={formData.name}
57 onChange={handleChange}
58 placeholder="Name"
59 required
60 className="p-2 border border-gray-300 rounded"
61 />
62 <input
63 name="email"
64 value={formData.email}
65 onChange={handleChange}
66 placeholder="Email"
67 required
68 className="p-2 border border-gray-300 rounded"
69 />
70 <input
71 name="phone"
72 value={formData.phone}
73 onChange={handleChange}
74 placeholder="Phone"
75 required
76 className="p-2 border border-gray-300 rounded"
77 />
78 <textarea
79 name="summary"
80 value={formData.summary}
81 onChange={handleChange}
82 placeholder="Professional Summary"
83 required
84 className="p-2 border border-gray-300 rounded"
85 />
86 <textarea
87 name="education"
88 value={formData.education}
89 onChange={handleChange}
90 placeholder="Education"
91 required
92 className="p-2 border border-gray-300 rounded"
93 />
94 <textarea
95 name="experience"
96 value={formData.experience}
97 onChange={handleChange}
98 placeholder="Work Experience"
99 required
100 className="p-2 border border-gray-300 rounded"
101 />
102 <textarea
103 name="skills"
104 value={formData.skills}
105 onChange={handleChange}
106 placeholder="Skills"
107 required
108 className="p-2 border border-gray-300 rounded"
109 />
110 <textarea
111 name="projects"
112 value={formData.projects}
113 onChange={handleChange}
114 placeholder="Projects"
115 required
116 className="p-2 border border-gray-300 rounded"
117 />
118 <button type="submit" className="p-2 bg-blue-500 text-white rounded">Submit</button>
119 </form>
120
121 <div className="mt-8 p-4 border border-gray-300 rounded bg-white" ref={pdfRef}>
122 {submittedData.map((data, index) => (
123 <div key={index} className="cv">
124 <h1 className="text-2xl font-bold">{data.name}</h1>
125 <p><strong>Email:</strong> {data.email}</p>
126 <p><strong>Phone:</strong> {data.phone}</p>
127 <h2 className="text-xl font-semibold mt-4">Professional Summary</h2>
128 <p>{data.summary}</p>
129 <h2 className="text-xl font-semibold mt-4">Education</h2>
130 <p>{data.education}</p>
131 <h2 className="text-xl font-semibold mt-4">Work Experience</h2>
132 <p>{data.experience}</p>
133 <h2 className="text-xl font-semibold mt-4">Skills</h2>
134 <p>{data.skills}</p>
135 <h2 className="text-xl font-semibold mt-4">Projects</h2>
136 <p>{data.projects}</p>
137 </div>
138 ))}
139 </div>
140
141 <div className="mt-4 flex gap-4">
142 <CSVLink data={submittedData} filename="cv.csv" className="p-2 bg-green-500 text-white rounded">
143 Download CSV
144 </CSVLink>
145 <button onClick={generatePDF} className="p-2 bg-red-500 text-white rounded">Download PDF</button>
146 </div>
147 </div>
148 );
149}
Explanation
1. Form Component
- Form Data State: We use the useState hook to manage the form data.
- Handle Change: The handleChange function updates the form data state as the user types in the inputs.
- Handle Submit: The handleSubmit function sets the submitted data state and saves the form data to local storage.
2. PDF Generation
- Generate PDF: The generatePDF function uses html2canvas to capture the preview and jsPDF to create a PDF.
3. CSV Download
- CSV Download: The CSVLink component from react-csv allows users to download the submitted data as a CSV file.
4. Local Storage
- Use Effect: The useEffect hook loads data from local storage when the component is mounted.
- Save to Local Storage: Data is saved to local storage every time the form is submitted.
5. Styling with Tailwind CSS
- Tailwind Classes: Utility classes from Tailwind CSS are used to style the form, preview section, and buttons.
Complete Example
Here is the complete example with Tailwind CSS and local storage integration:
1import { useState, useRef, useEffect } from 'react';
2import { CSVLink } from 'react-csv';
3import html2canvas from 'html2canvas';
4import jsPDF from 'jspdf';
5
6export default function Home() {
7 const [formData, setFormData] = useState({
8 name: '',
9 email: '',
10 phone: '',
11 summary: '',
12 education: '',
13 experience: '',
14 skills: '',
15 projects: ''
16 });
17
18 const [submittedData, setSubmittedData] = useState([]);
19 const pdfRef = useRef();
20
21 useEffect(() => {
22 const savedData = localStorage.getItem('cvData');
23 if (savedData) {
24 setFormData(JSON.parse(savedData));
25 setSubmittedData([JSON.parse(savedData)]);
26 }
27 }, []);
28
29 const handleChange = (e) => {
30 setFormData({ ...formData, [e.target.name]: e.target.value });
31 };
32
33 const handleSubmit = (e) => {
34 e.preventDefault();
35 setSubmittedData([formData]);
36 localStorage.setItem('cvData', JSON.stringify(formData));
37 };
38
39 const generatePDF = async () => {
40 const element = pdfRef.current;
41 const canvas = await html2canvas(element);
42 const imgData = canvas.toDataURL('image/png');
43 const pdf = new jsPDF();
44 const imgProps = pdf.getImageProperties(imgData);
45 const pdfWidth = pdf.internal.pageSize.getWidth();
46 const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
47 pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
48 pdf.save('cv.pdf');
49 };
50
51 return (
52 <div className="container mx-auto p-4">
53 <form onSubmit={handleSubmit} className="grid gap-4 max-w-lg mx-auto">
54 <input
55 name="name"
56 value={formData.name}
57 onChange={handleChange}
58 placeholder="Name"
59 required
60 className="p-2 border border-gray-300 rounded"
61 />
62 <input
63 name="email"
64 value={formData.email}
65 onChange={handleChange}
66 placeholder="Email"
67 required
68 className="p-2 border border-gray-300 rounded"
69 />
70 <input
71 name="phone"
72 value={formData.phone}
73 onChange={handleChange}
74 placeholder="Phone"
75 required
76 className="p-2 border border-gray-300 rounded"
77 />
78 <textarea
79 name="summary"
80 value={formData.summary}
81 onChange={handleChange}
82 placeholder="Professional Summary"
83 required
84 className="p-2 border border-gray-300 rounded"
85 />
86 <textarea
87 name="education"
88 value={formData.education}
89 onChange={handleChange}
90 placeholder="Education"
91 required
92 className="p-2 border border-gray-300 rounded"
93 />
94 <textarea
95 name="experience"
96 value={formData.experience}
97 onChange={handleChange}
98 placeholder="Work Experience"
99 required
100 className="p-2 border border-gray-300 rounded"
101 />
102 <textarea
103 name="skills"
104 value={formData.skills}
105 onChange={handleChange}
106 placeholder="Skills"
107 required
108 className="p-2 border border-gray-300 rounded"
109 />
110 <textarea
111 name="projects"
112 value={formData.projects}
113 onChange={handleChange}
114 placeholder="Projects"
115 required
116 className="p-2 border border-gray-300 rounded"
117 />
118 <button type="submit" className="p-2 bg-blue-500 text-white rounded">Submit</button>
119 </form>
120
121 <div className="mt-8 p-4 border border-gray-300 rounded bg-white" ref={pdfRef}>
122 {submittedData.map((data, index) => (
123 <div key={index} className="cv">
124 <h1 className="text-2xl font-bold">{data.name}</h1>
125 <p><strong>Email:</strong> {data.email}</p>
126 <p><strong>Phone:</strong> {data.phone}</p>
127 <h2 className="text-xl font-semibold mt-4">Professional Summary</h2>
128 <p>{data.summary}</p>
129 <h2 className="text-xl font-semibold mt-4">Education</h2>
130 <p>{data.education}</p>
131 <h2 className="text-xl font-semibold mt-4">Work Experience</h2>
132 <p>{data.experience}</p>
133 <h2 className="text-xl font-semibold mt-4">Skills</h2>
134 <p>{data.skills}</p>
135 <h2 className="text-xl font-semibold mt-4">Projects</h2>
136 <p>{data.projects}</p>
137 </div>
138 ))}
139 </div>
140
141 <div className="mt-4 flex gap-4">
142 <CSVLink data={submittedData} filename="cv.csv" className="p-2 bg-green-500 text-white rounded">
143 Download CSV
144 </CSVLink>
145 <button onClick={generatePDF} className="p-2 bg-red-500 text-white rounded">Download PDF</button>
146 </div>
147 </div>
148 );
149}
This example walks you through a simple CV builder app with features like form input, data display, PDF generation, CSV download, and local storage using Next.js and Tailwind CSS. Feel free to get creative and make it even more fun!