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:
- Basic knowledge of Next.js.
- Basic knowledge of JavaScript/TypeScript
- 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:
- Next.js: A React Framework to build our Frontend.
- Xata: A Serverless Data Platform to build our backend
- Cloudinary: To power and manage our assets.
- Tailwind CSS: A utility-first CSS framework to support and develop our design
- React-Hook-Form: To handle all our form input
- React Icons: To add beautiful Icons to our pages.
- Base64: Passwords are encoded using Base64.
- ReactToPrint: To print our components in the browser
- jsPDF: A library to generate our PDFs in JavaScript.
- Yup: A JavaScript schema builder to parse and validate our form input values.
- React-hook-form validation resolver: A JavaScript schema builder to parse and validate our form input values.
- HeroIcons
Project Pages
In the Project, we will have the following Pages
- Home/Landing Page
- Signup Page
- Login Page
- Dashboard Page
- My Resume Page
- Edit Resume
- Download Resume
- Create Resume Page
Getting Started
In this section, we will be doing the following:
- Creating a Database on Xata
- Building the front end with Next.js
- Cloudinary Setup
- Linking our Database with our Frontend and Cloudinary
- 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
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.
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.
Creating tables
Now, we need to create the tables for our project. In our project, we will need two tables, namely:
- basic_info
- 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.
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.
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 |
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:
- API Route to connect to your Xata database
- Type-safe Codegen
- Accessibility-Ready
- Dark/Light mode
- 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
Now, we can navigate to the project directory by running the following:
cd with-xata-app
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
then we navigate into the project directory by running:
cd next.js
then
cd examples
then
cd with-xata
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
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
In our terminal.
Using the xata auth login
gives us two options:
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.
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
Here is what our project folders should look like
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
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.
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
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 |
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> | |
) | |
// . . . |
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> | |
) | |
// . . . |
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> | |
) | |
//... |
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> | |
) | |
//... |
Nav.jsx
Nav Section of our pages
References.js
Reference Section of our Resume
//... | |
return ( | |
<div className="tell_us_title"> | |
<h1 className={SteptwoStyle.main_tell_title}> | |
References | |
</h1> | |
</div> | |
) | |
//... |
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> | |
) | |
//.. |
TopMenu.jsx
Top Menu section of our Resume
The next folder is our context folder, which has the following structure:
context/ | |
|-- globalContext.tsx | |
`-- signupContext.tsx |
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 |
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 | |
//... |
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 | |
//... |
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 |
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 |
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 | |
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 |
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=""> | |
</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> | |
) |
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> |
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> | |
//... |
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=""> | |
</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> | |
</> | |
//... |
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> | |
//... |
Preview.jsx
The page to download our Resume
//... | |
<> | |
<Nav /> | |
<div className={Previewstyle.previewcontainer}> | |
<Downloadandpreview onClick={handlePrint} /> | |
<Download ref={componentRef} /> | |
</div> | |
</> | |
//... |
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 |
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; | |
} | |
} | |
//... |
Nav*.module.css*
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; | |
} | |
} | |
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
- Set up a Next.js project.
- Make use of Xata for backend services
- Manage assets with Cloudinary
- Use CSS and Tailwind CSS in a Next.js project
- Manage state using the Context API in a Next.js project.
- Use NPM packages Project Overview.
Resources and References
These resources might be helpful:
Top comments (2)
nice work
Thank you @fadhilsaheer