logo icon

OKSANA

KOROBANOVA

white printer paper on macbook pro
Back to all posts

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.


Date:


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!