DEV Community

Cover image for How to Create a Resume Builder App with Xata and Cloudinary Using NextJs
Femi Akinyemi for Hackmamba

Posted on • Edited on

11

How to Create a Resume Builder App with Xata and Cloudinary Using NextJs

Introduction

Resumes play an essential role in getting a job, as it aids in ascertaining a candidate's level of competency for a job role. That said, it needs to be built with care.

In this article, we will learn how to use Xata, a serverless, branch-able, scalable, consistent, highly available, searchable database to build a resume builder’s backend, and Cloudinary, a cloud-based asset management service, to manage all assets. At the same time, Next.js will be used to handle the front-end development.

Here’s a sneak peek at what we’ll be building

The source code of the project is available here on GitHub

Here is a working demo on CodeSandbox

Project Demo

The video below shows what we will be building in this article.

Why Xata and Cloudinary

Xata allows us to combine a Serverless Relational Database, a search engine, and an analytics engine, all behind a single consistent API. There’s also support for edge caching, a workflow for zero-downtime schema migration, and first-class branch support.
At the same time, Cloudinary gives us a lot of power to optimize and transform images automatically, which makes it easy to build any application.

Prerequisites

To follow along, we will need the following:

  1. Basic knowledge of Next.js.
  2. Basic knowledge of JavaScript/TypeScript
  3. Node.js installed.

Project Overview

In this project, we will examine how Xata works as a serverless database that can power the backend of any application. Also, to explore the beauty of assets management Cloudinary has to offer.

Below are the packages we will use in this project:

  1. Next.js: A React Framework to build our Frontend.
  2. Xata: A Serverless Data Platform to build our backend
  3. Cloudinary: To power and manage our assets.
  4. Tailwind CSS: A utility-first CSS framework to support and develop our design
  5. React-Hook-Form: To handle all our form input
  6. React Icons: To add beautiful Icons to our pages.
  7. Base64: Passwords are encoded using Base64.
  8. ReactToPrint: To print our components in the browser
  9. jsPDF: A library to generate our PDFs in JavaScript.
  10. Yup: A JavaScript schema builder to parse and validate our form input values.
  11. React-hook-form validation resolver: A JavaScript schema builder to parse and validate our form input values.
  12. HeroIcons

Project Pages

In the Project, we will have the following Pages

  1. Home/Landing Page
  2. Signup Page
  3. Login Page
  4. Dashboard Page
  5. My Resume Page
    • Edit Resume
    • Download Resume
  6. Create Resume Page

Getting Started

In this section, we will be doing the following:

  1. Creating a Database on Xata
  2. Building the front end with Next.js
  3. Cloudinary Setup
  4. Linking our Database with our Frontend and Cloudinary
  5. Deploying Our App to Netlify

Creating a Database on Xata

To create our Database, we need to login into our Xata dashboard
by clicking any preferred login option

Sign In Button

After logging into our dashboard, click on the Add a database button to create a database.

Clicking the Add a database button, we can now specify the name and location of our database.

Workspace Image

add database

For this project, we will name our database “basic_info,” as shown in the image below. We can change it to any other name if we want.

database

Creating tables

Now, we need to create the tables for our project. In our project, we will need two tables, namely:

  1. basic_info
  2. user table

Xata represents data as tables, columns, and rows. Schemas describe this structure. Click on "+ Add a Table" and enter a name to add a table.

Created table

Defining schema
We can now define our project schema for our table by clicking the “+” icon on the table header. By default, all tables contain one column: the ID column, which gives each row a unique identifier. It can have any value that is unique across all rows of data.
In adding columns, we find that each column has various types.

We will use string throughout our project schemas, as shown below.

The User table looks like this after we have created our schema columns.

schema columns

In our basic_info table, we need to create our schema columns with the names:

Full_name,Email,Address, Phone_number, Role, Profile_Photo_Url, Public_id, user, unique_id, Job_Title_Ex, City_town_Ex,
Employer_Ex, Start_date_ex, End_Date_Ex, Achievement_one_Ex, Achievement_two_Ex, Achievement_three_Ex, Degree_Ed,
City_Ed, School_Ed, Start_date_Ed, End_date_Ed, Award_one_Ed, Award_two_Ed, Award_three_Ed, Hobby, Company_name_Rfx,
Contact_person_Rfx, Phone_number_Rfx, Email_Address_Rfx, Skill, Level_sk, Cert_Img_one_url, Cert_Img_two_url,
Language, Cert_Public_Id
view raw TableSchema hosted with ❤ by GitHub

schema columns

Working with our data
We are now ready to process requests for data from clients and insert incoming data into our database after setting it up as described above.
To look at how we can use our data, we now have to proceed to step two under the Getting started above: Building our front end with Next.js.

Building the front end with Next.js

For fast development, we will clone the Example repo from Xata.
This example showcases how to use Next.js with Xata as our data layer.
With this template, we will get out-of-the-box:

  1. API Route to connect to your Xata database
  2. Type-safe Codegen
  3. Accessibility-Ready
  4. Dark/Light mode
  5. Respect prefers-reduce-motion for CSS Transitions

Bootstrapping our Next.js App

To proceed, we copy the command below and paste it into our project folder in the terminal to bootstrap the Xata Example repo.

npx create-next-app --example with-xata with-xata-app
Enter fullscreen mode Exit fullscreen mode

Now, we can navigate to the project directory by running the following:

 cd with-xata-app
Enter fullscreen mode Exit fullscreen mode

Also, we can type git clone and then paste the URL below to clone the example repo.

git clone https://github.com/vercel/next.js.git
Enter fullscreen mode Exit fullscreen mode

then we navigate into the project directory by running:

cd next.js
Enter fullscreen mode Exit fullscreen mode

then

cd examples
Enter fullscreen mode Exit fullscreen mode

then

cd with-xata
Enter fullscreen mode Exit fullscreen mode

Linking Xata Workspace and Running Codegen

The next step is to link our Xata Database to our client side. To accomplish this, we need to install Xata CLI
It will help us manage our databases more effectively.

To install the Xata CLI globally, we run the command below:

npm i --location=global @xata.io/cli
Enter fullscreen mode Exit fullscreen mode

To use the Xata CLI, we need to authenticate it to access our workspace and database. We will be doing this globally by running the following:

xata auth login
Enter fullscreen mode Exit fullscreen mode

In our terminal.

Using the xata auth login gives us two options:

  1. To create a new API Key: This will open our browser and when we’re logged in, Xata allows us to create a new API key for use with the CLI.

  2. Use an existing API Key: This will prompt for an existing key we have, which we can paste into our terminal. We will go with the first option for this project, creating a new API key.

Upon providing an API key, a global configuration of the CLI will be made, which stores our API key in

~/.config/xata/credentials.

Now that we know all the steps to set up our Xata CLI let us run Xata auth login in our terminal using the code below.

xata auth login
Enter fullscreen mode Exit fullscreen mode

Here is what our project folders should look like

folder structure

Installation
The next step is to install all the packages we will use for our project, starting with Tailwind CSS by following the Installation guild here
Next is to install other packages listed above by running the below command in our terminal

npm install react-hook-form react-icons base-64 react-to-print jspdf yup 
Enter fullscreen mode Exit fullscreen mode

It is essential to verify the dependencies in the package.json file to confirm whether they have been installed.

This should be the current state of our dependencies.

dependencies

Cloudinary setup

To set up our Cloudinary account, we must follow the Cloudinary React image transformations documentation in this guide

Next is to install Cloudinary React, which allows us to quickly and easily integrate our application with Cloudinary and Cloudinary URL-gen. This enables us to create Cloudinary URLs for our images and videos by running the command below:

npm i cloudinary-react @cloudinary/url-gen 
Enter fullscreen mode Exit fullscreen mode

Linking our Database with our Frontend and Cloudinary

Our Components
Below is the list of components we will use in our project:

components/
|-- Download.jsx
|-- Downloadandpreview.js
|-- Editor.js
|-- Education.js
|-- Extras.js
|-- Imagesandmedia.js
|-- Interest.js
|-- Nav.jsx
|-- References.js
|-- Skills.js
`-- TopMenu.jsx

Download.jsx

Here is our preview resume component.

// . . .other entries
return (
<>
<div className={Previewstyle.titleandcontacts}>
<div className={Previewstyle.contacts}>
<div className={Previewstyle.mobile}>
<div>
<BsFillTelephoneFill />
</div>
<div>{x?.Phone_number}</div>
</div>
<div className={Previewstyle.email}>
<div>
<MdOutlineAlternateEmail />
</div>
<div>{x?.Email}</div>
</div>
</>
)
export default Download
// . . .other entries
view raw Download.jsx hosted with ❤ by GitHub

DownloadandPreview.jsx

This is our Download and Edit component Button

// . . .
return (
<div className="flex mt-10 justify-between ">
<div onClick={onClick} className={Previewstyle.preview}>
Download
</div>
<div
onClick={() =>
router.push({
pathname: '/edit',
query: {
userdata: {
data: alldata[0],
},
useravailabledata: `${alldata}`,
},
})
}
className={Previewstyle.preview}
>
Edit
</div>
</div>
)
// . . .
view raw Preview.jsx hosted with ❤ by GitHub

Education Component

This is the Education section in our resume

// . . .
return (
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Degree
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g Bachelor of Mathematics"
{...register('degree')}
defaultValue=""
/>
</div>
)
// . . .
view raw Education.jsx hosted with ❤ by GitHub

Extras.js

This is the Extras section of our resume

//...
return (
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Language
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g Yoruba"
{...register('language')}
/>
</div>
)
//...
view raw Extras.jsx hosted with ❤ by GitHub

Imagesandmedia.js

This is the Image and media section of our resume

//...
return (
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Upload your certificate
</label>
<input
className={SteptwoStyle.getstarted_input}
placeholder="Select an Image"
{...register('certificateone')}
type="file"
multiple
accept="image/*"
/>
</div>
)
//...

Interest.js

This is the Interest section of our resume

//...
return (
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Hobby
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g Driving"
{...register('interest')}
/>
</div>
)
//...
view raw Interest.jsx hosted with ❤ by GitHub

Nav.jsx

Nav Section of our pages

//...
return (
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden ">
<Disclosure.Button className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
)
//...
view raw Nav.jsx hosted with ❤ by GitHub

References.js

Reference Section of our Resume

//...
return (
<div className="tell_us_title">
<h1 className={SteptwoStyle.main_tell_title}>
References
</h1>
</div>
)
//...
view raw References.js hosted with ❤ by GitHub

Skills.js

Skills Section of our Resume

//...
return (
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Skill
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g. Microsoft Word"
{...register('skill')}
/>
</div>
)
//..
view raw Skills.jsx hosted with ❤ by GitHub

TopMenu.jsx

Top Menu section of our Resume

//...
return (
<div
onClick={() => router.push('/')}
className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start"
>
<div className="flex flex-shrink-0 items-center">
<div className="w-10 h-10 notshow bg-[#DA552F] text-[#fff] rounded-full border-2 border-[#DA552F] flex justify-center items-center">
<p>QCV</p>
</div>
</div>
</div>
)
//...
view raw TopMenu.jsx hosted with ❤ by GitHub

The next folder is our context folder, which has the following structure:

context/
|-- globalContext.tsx
`-- signupContext.tsx
view raw ContextFolder hosted with ❤ by GitHub

Inside our globalContext.jsx, we have

\\...
export const GlobalContextProvider = (props) => {
const [currentUser, setCurrentUser] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [mainUser, setMainUser] = useState({});
return (
<GlobalContext.Provider
value={{
user: currentUser,
loading: isLoading,
setUser: setCurrentUser,
setLoading: setIsLoading,
loggedinuser: mainUser,
setLoggedinuser: setMainUser
}}
>
{props.children}
</GlobalContext.Provider>
);
};
\\...

and signupContext.jsx , we have

\\...
export const SignupContextProvider = (props) => {
const [Userid, setUserId] = useState({});
const [isLoading, setIsLoading] = useState(true);
return (
<signupContext.Provider
value={{
signupid: Userid,
setsignupid: setUserId,
}}
>
{props.children}
</signupContext.Provider>
);
};
\\...

After creating our components and state context, we can move on to creating our project APIs, which have the following structure.

api/
|-- edit.js
|-- fetchall.js
|-- getuser.js
|-- signup.js
`-- upload.js
view raw api folder hosted with ❤ by GitHub

edit.js
This handles our CV editing

\\...
import { getXataClient } from '../../utils/xata.codegen'
const handler = async (req, res) => {
try {
const xata = getXataClient()
const record = await xata.db.basic_info.update(`${req.body.id}`, {
Full_name: req.body.Full_name,
Email: req.body.Email,
})
return res.status(201).json(record)
} catch (error) {
console.error(error)
return res.status(500).send(error)
}
}
export default handler
//...
view raw edit.js hosted with ❤ by GitHub

fetchall.js
Fetch all user API

//...
import { getXataClient } from '../../utils/xata.codegen'
const handler = async (req, res) => {
try {
const xata = getXataClient()
const records = await xata.db.basic_info.getAll()
return res.status(200).json(records)
} catch (error) {
console.error(error)
return res.status(500).send(error)
}
}
export default handler
//...
view raw fetchalluser.js hosted with ❤ by GitHub

getuser
Get all user's API

import { getXataClient } from '../../utils/xata.codegen'
const handler = async (req, res) => {
try {
const xata = getXataClient()
const records = await xata.db.users.getAll()
return res.status(200).json(records)
} catch (error) {
console.error(error)
return res.status(500).send(error)
}
}
export default handler
view raw getuser.js hosted with ❤ by GitHub

signup.js
Signup API

import { getXataClient } from '../../utils/xata.codegen'
import { Base64 } from 'js-base64'
const handler = async (req, res) => {
try {
const xata = getXataClient()
const hashedPassword = Base64.encode(req.body.password)
const record = await xata.db.users.create({
email: req.body.email,
password: hashedPassword,
username: req.body.username,
})
return res.status(201).json(record)
} catch (error) {
console.error(error)
return res.status(500).send(error)
}
}
export default handler
view raw Signup.js hosted with ❤ by GitHub

upload.js
This handle or CV Form creation

import { getXataClient } from '../../utils/xata.codegen'
const handler = async (req, res) => {
try {
const xata = getXataClient()
const record = await xata.db.basic_info.create({
Full_name: req.body.Full_name,
Email: req.body.Email,
Address: req.body.Address,
Phone_number: req.body.Phone_number,
Role: req.body.Role,
Profile_Photo_Url: req.body.Profile_Photo_Url,
Public_id: req.body.Public_id,
user: req.body.user,
unique_id: req.body.unique_id,
Job_Title_Ex: req.body.Job_Title_Ex,
City_town_Ex: req.body.City_town_Ex,
Employer_Ex: req.body.Employer_Ex,
Start_date_ex: req.body.Start_date_ex,
End_Date_Ex: req.body.End_date_Ed,
Achievement_one_Ex: req.body.Achievement_one_Ex,
Achievement_two_Ex: req.body.Achievement_two_Ex,
Achievement_three_Ex: req.body.Achievement_three_Ex,
Degree_Ed: req.body.Degree_Ed,
City_Ed: req.body.City_Ed,
School_Ed: req.body.School_Ed,
Start_date_Ed: req.body.Start_date_Ed,
End_date_Ed: req.body.End_date_Ed,
Award_one_Ed: req.body.Award_one_Ed,
Award_two_Ed: req.body.Award_two_Ed,
Award_three_Ed: req.body.Award_three_Ed,
Hobby: req.body.Hobby,
Company_name_Rfx: req.body.Company_name_Rfx,
Contact_person_Rfx: req.body.Contact_person_Rfx,
Phone_number_Rfx: req.body.Phone_number_Rfx,
Email_Address_Rfx: req.body.Email_Address_Rfx,
Skill: req.body.Skill,
Level_sk: req.body.Level_sk,
Cert_Img_one_url: req.body.Cert_Img_one_url,
Cert_Img_two_url: req.body.Cert_Img_two_url,
Language: req.body.Language,
Cert_Public_Id: req.body.Cert_Public_Id
})
return res.status(201).json(record)
} catch (error) {
console.error(error)
return res.status(500).send(error)
}
}
export default handler
view raw CreateCVAPI.js hosted with ❤ by GitHub

Our Pages
Also, most of the content here will be code snippets from our code. Please find the code here on GitHub.

Pages Structure

pages/
|-- _app.tsx
|-- afteredit.js
|-- edit.js
|-- index.js
|-- login.js
|-- preview.js
|-- signup.js
|-- stepone.js
`-- steptwo.js
view raw Pages Folder hosted with ❤ by GitHub

index.js

Our landing pages

return (
<div className={Home.resume_main}>
<div className={Home.left_image}>
<h1 className={Home.formtitle}>
In just minutes, create a job-ready resume
</h1>
</div>
<div className={Home.right_form}>
<h1 className={Home.form_title}>Create my Resume</h1>
<p className={Home.sub_title}>
With quick resume, you can build the right resume today.
</p>
<div className={Home.resume_form}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Email
</label>
<input
required
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('email')}
placeholder="Email"
type="email"
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Username
</label>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('name')}
placeholder="Username"
type="text"
required
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Password
</label>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('password')}
placeholder="password"
type="password"
required
/>
</div>
{loading ? (
<button
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Loading...
</button>
) : (
<button
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Get started
</button>
)}
</form>
</div>
</div>
</div>
)
view raw index.js hosted with ❤ by GitHub

signup.js

<div className={Signupstyles.updateform_main}>
<div>
<h1 className={Signupstyles.form_title}> Please SignUp</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<div className={Signupstyles.product_link_info}>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('email')}
placeholder="Email"
type="email"
/>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('name')}
placeholder="Username"
type="text"
/>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('password')}
placeholder="password"
type="password"
/>
</div>
{
loading ? <button
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Loading...
</button> : <button
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
SignUp
</button>
}
</form>
<span>
Already Registered?{' '}
<span
style={{ color: 'red' }}
onClick={() => router.push('/login')}
>
Login
</span>{' '}
</span>
</div>
</div>
view raw signup.jsx hosted with ❤ by GitHub

login.jsx

//...
<div className={Signupstyles.updateform_main}>
<div>
<h1 className={Signupstyles.form_title}> Please Login</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<div className={Signupstyles.product_link_info}>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('email')}
placeholder="Email"
type="text"
required
/>
<input
defaultValue=""
className={Signupstyles.getstartedinput}
{...register('password')}
placeholder="password"
type="password"
/>
</div>
{
loading ? <button
style={{
marginBottom: '20px',
}}
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Loading...
</button> : <button
style={{
marginBottom: '20px',
}}
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Login
</button>
}
</form>
<span>
Don't have an account yet?{' '}
<span
style={{ color: 'red' }}
onClick={() => router.push('/signup')}
>
Signup
</span>{' '}
</span>
<div style={{ marginTop: '20px', color: 'red' }}>
{passwordcheck && <span>Wrong Details!</span>}
</div>
</div>
</div>
//...
view raw login.jsx hosted with ❤ by GitHub

stepone.jsx

Dashboard.jsx

//...
<>
<div className={Home.resume_body}>
<div className="top_nav">
<Nav />
{/* <TopMenu /> */}
</div>
<div className={Home.resume_main}>
<div className={Home.left_image}>
<h1 className={Home.formtitle}>
In just minutes, create a job-ready resume
</h1>
</div>
<div className={Home.right_form}>
<h1 className={Home.form_title}>Create my Resume</h1>
<p className={Home.sub_title}>
With quick Resume, you can build the right resume today.
</p>
<div className={Home.resume_form}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Full Name
</label>
<input
defaultValue=""
required
className={Home.getstartedinput}
{...register('full_name')}
placeholder="FullName"
type="text"
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Role
</label>
<input
defaultValue=""
required
className={Home.getstartedinput}
{...register('role')}
placeholder="e.g. Software Engineer"
type="text"
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Phone number
</label>
<input
defaultValue=""
required
className={Home.getstartedinput}
{...register('phonenumber')}
placeholder="+23470..."
type="text"
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Email
</label>
<input
defaultValue=""
required
className={Home.getstartedinput}
{...register('email')}
placeholder="abc@gmail.com"
type="email"
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Address
</label>
<input
defaultValue=""
required
className={Home.getstartedinput}
{...register('address')}
placeholder="Lagos, Nigeria"
type="text"
/>
</div>
<div className={Home.resumelinkinfo}>
<label className={Home.getstartedlabel} htmlFor="">
Profile Photo
</label>
<input
required
type="file"
className={Home.getstartedinput}
placeholder="Select an Image"
multiple
accept="image/*"
{...register('MyImage')}
/>
</div>
{
loadingstate ? <button
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Loading...
</button> :
<button
type="submit"
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Get started
</button>
}
</form>
</div>
</div>
</div>
</div>
</>
//...
view raw stepone.jsx hosted with ❤ by GitHub

steptwo.jsx

Edit Page

//...
<div className={SteptwoStyle.product_form}>
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Job Title
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g Cloud Engineer"
{...register('jobtitle')}
// defaultValue=''
/>
</div>
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
City/Town
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g Lagos Nigeria"
{...register('cityortown')}
/>
</div>
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Employer
</label>
<input
className={SteptwoStyle.getstarted_input}
type="text"
placeholder="e.g Hackmamba"
{...register('employer')}
/>
</div>
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Start Date
</label>
<input
className={SteptwoStyle.getstarted_input}
type="date"
placeholder="e.g Hackmamba"
{...register('startdate')}
/>
</div>
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
End Date
</label>
<input
className={SteptwoStyle.getstarted_input}
type="date"
placeholder="e.g Hackmamba"
{...register('enddate')}
/>
</div>
<div className={SteptwoStyle.product_link_info}>
<label
className={SteptwoStyle.getstartedlabel}
htmlFor=""
>
Description
</label>
<textarea
className={SteptwoStyle.getstarted_input}
type="date"
placeholder="e.g Duty at work "
{...register('achievement1')}
/>
<textarea
className={SteptwoStyle.getstarted_input}
type="date"
placeholder="e.g Duty at work "
{...register('achievement2')}
/>
<textarea
className={SteptwoStyle.getstarted_input}
type="date"
placeholder="e.g Duty at work "
{...register('achievement3')}
/>
</div>
<button
onClick={() => {
return setActive('Education and Qualification')
}}
className="bg-[#f64900] hover:bg-[#f64900] text-[#fff] font-semibold hover:text-[#fff] py-2 px-4 border border-[#f64900] hover:border-transparent rounded"
>
Next step: Education and Qualification
</button>
</div>
//...
view raw steptwo.jsx hosted with ❤ by GitHub

Preview.jsx

The page to download our Resume

//...
<>
<Nav />
<div className={Previewstyle.previewcontainer}>
<Downloadandpreview onClick={handlePrint} />
<Download ref={componentRef} />
</div>
</>
//...
view raw Preview.jsx hosted with ❤ by GitHub

Our Stylesheets

Also, most of the content here will be code snippets from our code. Please find the code here on GitHub.

Pages Structure

styles/
|-- Header.module.css
|-- Home.module.css
|-- Index.module.css
|-- Nav.module.css
|-- Preview.module.css
|-- root.css
|-- SignUp.module.css
`-- Steptwo.module.css
view raw Styles hosted with ❤ by GitHub

Home.module.css

//...
.updateform_main {
display: flex;
justify-content: center;
align-items: center;
margin-top: 100px;
}
.form_title {
margin-bottom: 20px;
}
.resume_main {
display: flex;
}
.left_image {
flex: 1;
background: #fef6f2;
}
.right_form {
background: #fff;
flex: 1;
height: 100vh;
}
.right_form,
.left_image {
padding: 5%;
}
.form_title {
font-weight: 700;
font-size: 32px;
line-height: 40px;
color: #21293c;
}
.formtitle {
font-weight: 700;
font-size: 70px;
color: #21293c;
}
.sub_title {
color: #4b587c;
font-size: 16px;
margin-top: 24px;
}
.getstartedbtn {}
.resume_form {
margin-top: 24px;
}
.resumelinkinfo {
display: flex;
flex-direction: column;
}
.getstartedinput {
text-indent: 10px;
border: 1px solid #d9e1ec;
border-radius: 4px;
background-color: rgba(245, 248, 255, .3);
box-sizing: border-box;
margin-bottom: 24px;
height: 40px;
}
.getstartedlabel {
margin-bottom: 4px !important;
}
@media only screen and (max-width: 600px) {
.resume_main {
flex-direction: column;
}
.formtitle {
font-size: 40px;
}
}
//...
view raw Home.css hosted with ❤ by GitHub

Nav*.module.css*

.dropdownmenu {
background: #fff;
padding: 5px;
}
.dropdownmenu>div {
margin: 10px 0px;
padding: 0px 0px 0px 3px;
color: #4b587c;
}
view raw Nav.module.css hosted with ❤ by GitHub

Preview.module.css

.previewcontainer {
/* background: lawngreen; */
margin: 0 auto;
width: 100%;
max-width: 70%;
}
.preview {
/* color: yellow; */
cursor: pointer;
}
.top_summary {
margin-top: 40px;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.contacts {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.mobile,
.email,
.address {
display: flex;
align-items: center;
gap: 5px;
}
.full_nameandrole {
margin-bottom: 5px;
}
.fullname {
margin-bottom: 5px;
}
.skills_section {
margin-top: 30px;
}
.skills_items {
margin-top: 10px;
}
.title {
color: #E12E0D;
}
.date {
color: #666666;
font-family: 9pt;
}
.sub_title {
color: #000000;
font-weight: 700;
font-size: 11pt;
line-height: 1.0;
}
.inner_sub_title {
color: #000000;
font-weight: 400;
text-decoration: none;
vertical-align: baseline;
font-size: 11pt;
font-family: "Playfair Display";
font-style: italic;
}

root.module.css

@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #fff;
}
.wrapperclass {
padding: 1rem;
border: 1px solid red;
}
.editorclass {
background-color: yellowgreen;
padding: 1rem;
border: 1px solid blue;
}
.toolbarclass {
border: 1px solid black;
}
@layer base {
ul,
ol {
list-style: revert;
}
}
view raw root.css hosted with ❤ by GitHub

Signup.module.css

.updateform_main {
display: flex;
justify-content: center;
align-items: center;
margin-top: 100px;
}
.form_title {
margin-bottom: 20px;
}
.product_main {
display: flex;
}
.left_image {
flex: 1;
background: #fef6f2;
/* height: 100vh; */
}
.right_form {
background: #fff;
flex: 1;
height: 100vh;
}
.right_form,
.left_image {
padding: 5%;
}
.form_title {
font-weight: 700;
font-size: 32px;
line-height: 40px;
color: #21293c;
}
.formtitle {
font-weight: 700;
font-size: 70px;
color: #21293c;
/* line-height: 40px; */
}
.sub_title {
color: #4b587c;
font-size: 16px;
margin-top: 24px;
}
.getstartedbtn {}
.product_form {
/* background: red; */
margin-top: 24px;
}
.product_link_info {
display: flex;
flex-direction: column;
}
.getstartedinput {
text-indent: 10px;
border: 1px solid #d9e1ec;
border-radius: 4px;
background-color: rgba(245, 248, 255, .3);
box-sizing: border-box;
margin-bottom: 24px;
height: 40px;
}
.getstartedlabel {
margin-bottom: 4px !important;
}
@media only screen and (max-width: 600px) {
.product_main {
flex-direction: column;
}
.formtitle {
font-size: 40px;
}
}

steptwo.module.css

.product_body_container {
padding: 0 5%;
margin: 0 auto;
width: 100%;
max-width: 1220px;
}
.menu_item_container>div {
padding: 12px;
}
.menu_content {
display: flex;
position: relative;
}
.product_side_menu {
height: 100vh;
flex: 1;
position: sticky !important;
}
.product_form_content {
background: #fff;
flex: 2;
}
.images_info_content {
color: #4b587c;
font-size: 16px;
line-height: 24px;
}
.draft_title {
margin-bottom: 30px;
border-bottom: 1px solid #d9e1ec;
}
.active {
background-color: #fef6f2;
padding: 12px;
width: 100%;
max-width: 280px;
}
.main_tell_title {
color: #21293c;
font-size: 24px;
line-height: 32px;
margin-bottom: 15px;
font-weight: 700;
}
.sub_tell_us_content {
color: #4b587c;
font-size: 16px;
line-height: 24px;
}
.link_container {
margin-top: 30x;
}
.hr_text {
border-bottom: 1px solid #d9e1ec;
margin-bottom: 30px;
}
.link_title {
color: #21293c;
font-size: 24px;
font-weight: 700;
line-height: 32px;
margin-bottom: 24px;
}
.form {
margin-bottom: 30px;
}
.thumbnail_img {
width: 80px !important;
height: 80px !important;
object-fit: cover;
max-width: 100%;
}
.image_wrapper {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.options {
margin-right: 10px;
}
.radio {
margin-bottom: 10px;
}
@media only screen and (max-width: 600px) {
.product_body_container {
max-width: 50%;
}
.MainContent_Container {}
}
.product_main {
display: flex;
}
.left_image {
flex: 1;
background: #fef6f2;
}
.right_form {
background: #fff;
flex: 1;
height: 100vh;
}
.right_form,
.left_image {
padding: 5%;
}
.form_title {
font-weight: 700;
font-size: 32px;
line-height: 40px;
color: #21293c;
}
.formtitle {
font-weight: 700;
font-size: 70px;
color: #21293c;
/* line-height: 40px; */
}
.sub_title {
color: #4b587c;
font-size: 16px;
margin-top: 24px;
}
.getstartedbtn {}
.product_form {
/* background: red; */
margin-top: 24px;
}
.product_link_info {
display: flex;
flex-direction: column;
}
.getstarted_input {
text-indent: 10px;
border: 1px solid #d9e1ec;
border-radius: 4px;
background-color: rgba(245, 248, 255, .3);
box-sizing: border-box;
margin-bottom: 24px;
height: 40px;
}
.getstartedlabel {
margin-bottom: 4px !important;
}
@media only screen and (max-width: 600px) {
.product_main {
flex-direction: column;
}
.formtitle {
font-size: 40px;
}
}
.wrapperclass {
padding: 1rem;
border: 1px solid red;
}
.editorclass {
background-color: yellowgreen;
padding: 1rem;
border: 1px solid blue;
}
.toolbarclass {
border: 1px solid black;
}

Deploying Our App to Netlify

Our app must now be deployed to Netlify using this guide after we have linked our front end to our database.

Conclusion

This post discussed creating a Resume Builder App with Xata and Cloudinary Using NextJs. I hope you enjoyed reading this article.

Here is a list of what we did in this article

  1. Set up a Next.js project.
  2. Make use of Xata for backend services
  3. Manage assets with Cloudinary
  4. Use CSS and Tailwind CSS in a Next.js project
  5. Manage state using the Context API in a Next.js project.
  6. Use NPM packages Project Overview.

Resources and References

These resources might be helpful:

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (2)

Collapse
 
fadhilsaheer profile image
Fadhil ⚡

nice work

Collapse
 
femi_akinyemi profile image
Femi Akinyemi

Thank you @fadhilsaheer

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →