You can manage and store your connections' names, addresses, and phone numbers using a contact phone directory. Telephone directories save callers time and energy by making it easier to find a specific contact.
This article discusses how to create a contact phone directory application with Next.js, Cloudinary, and Xata. Cloudinary is a cloud-based video and image management platform which provides services for maintaining and converting uploaded media for web applications. Xata is a Serverless Data platform that simplifies how developers work with data by providing the power of a traditional database with built-in powerful search and analytics.
Demo and Source Code
Check out the complete source code here and access the live demo here.
Prerequisite
Understanding this article requires the following:
- Installation of Node.js
- Knowledge of JavaScript and React.js
- A Cloudinary account(sign up here)
- A Xata account(sign up here)
- Understanding of Tailwind CSS for styling
Installation and Setup
Run this command on your terminal to create a Next app.
npx create-next-app <app-name>
After successful installation, navigate to the folder created and start the server on port 3000
with the command below:
cd <app-name>
npm run dev
Creating a Xata Database
After creating an account and logging in to Xata, create a new database called contact-phone-directory by clicking on the add database plus icon.
Next, create a table called contacts as shown in the schema section below, which will contain the following fields: location, firstName, lastName, phoneNumber, email, imageUrl, and id - which is auto-generated by Xata.
Setup Xata Instance
To use Xata in your project, you must first install the Xata Software Development Kit (SDK) globally to use all of the Xata commands. Install the Xata CLI globally with the command below:
npm i -g @xata.io/cli
Next, run the command below to authenticate:
xata auth login
The command above presents you with two choices via a prompt in your terminal, you should go for creating a new existing API key by opening a browser, this will redirect you to another browser, give the API a name and click the Create API key button.
Next, initialize your project with Xata using the command below. The command provides you with a series of prompts to successfully complete the Xata setup.
xata init
Tailwind Setup
Install Tailwind CSS in your project with the command below:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
The commands will create two files in the root directory of your project, tailwind.config.js,
and postcss.config.js
.
In your tailwind.config.js
, add the paths to all your template files with the code below.
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Next, add the @tailwind
directives for each of Tailwind’s layers to your styles/global.css file
.
@tailwind base;
@tailwind components;
@tailwind utilities;
Cloudinary Setup
Add Cloudinary dependencies to your project with the command below:
npm i @cloudinary/url-gen @cloudinary/react
The Cloudinary upload widget is an interactive user interface that enables users to upload files from a variety of sources to your application. You will use the upload widget to add images to your Xata database. To use the widget, you must include the Cloudinary widget JavaScript file in the Head section of your pages/contact/add-new-contact.js file
.
//pages/contact/add-new-contact.js
import Head from "next/head";
<Head>
<script
src='https://upload-widget.cloudinary.com/global/all.js'
type='text/javascript'
/>
</Head>
Setup upload widget into your app using upload preset
Upload presets enable you to centrally define a set of asset upload options instead of specifying them in each upload call. To create an upload preset, navigate to your Cloudinary Console and click on the Settings tab.
Next, click on the Upload tab and scroll down to the Upload presets section of the page.
Click on Add upload preset or use the upload preset name by Cloudinary, you need to rename the upload preset and change the Signing Mode to 'Unsigned' by clicking on Edit to change the option and Save button to effect the change.
Next, copy the upload preset name and add it to your code. Create an openupWidget()
function to add and open up widgets. In your pages/contact/add-new-contact.js file
, paste the code below:
//pages/contact/add-new-contact.js
const openupWidget = () => {
window.cloudinary
.openUploadWidget(
{ cloud_name: "Your Cloud Name", upload_preset: "ml_default" },
(error, result) => {
if (!error && result && result.event === "success") {
setImageUrl(result.info.url);
}
}
)
.open();
};
In the code block above:
- The
openUploadWidget()
method receives two parameters, yourcloud_name
and your upload preset. To get yourcloud_name
, check the Cloudinary Dashboard - Depending on whether the upload was successful or not, the picture URL or the error detected is logged into the console
On your "Upload Image" button, add the openupWidget()
code to an onClick
event listener. When you click the button, the onClick
event listener launches your openupWidget()
function.
<button
className='bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded'
type='button'
onClick={openupWidget}
>
Upload Image
</button>
Saving New Contacts to the Phone Directory
Create a form to collect contact information and add a new contact to your directory inside pages/contact/add-new-contact.js
file.
/* eslint-disable @next/next/no-sync-scripts */
...
import Link from "next/link";
import { useRouter } from "next/router";
...
const AddContact = () => {
const router = useRouter();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [phoneNumber, setPhoneNumber] = useState();
const [location, setLocation] = useState("");
const [email, setEmail] = useState("");
const [imageUrl, setImageUrl] = useState("");
//open openupWidget function
const handleSubmit = (e) => {
e.preventDefault();
fetch("/api/add-contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
firstName,
lastName,
phoneNumber,
location,
email,
imageUrl: imageUrl,
}),
}).then(() => router.push("/"));
};
return (
<>
...
<section className='mx-auto w-[50%]'>
<form>
<p className='mt-[30px] mb-[30px] font-medium'>Add New Contact</p>
<div className='mb-5'>
<label htmlFor='firstName' className='block'>
First Name
</label>
<input
type='text'
name='firstName'
onChange={(e) => setFirstName(e.target.value)}
required
className='border-2 p-1 w-full mt-1'
/>
</div>
<div className='mb-5'>
<label htmlFor='lastName' className='block'>
Last Name
</label>
<input
type='text'
name='lastName'
id='lastName'
required
onChange={(e) => setLastName(e.target.value)}
className='border-2 p-1 w-full mt-1'
/>
</div>
<div className='mb-5'>
<label htmlFor='phoneNumber' className='block'>
Phone Number
</label>
<input
type='text'
name='phoneNumber'
id='phoneNumber'
required
onChange={(e) => setPhoneNumber(e.target.value)}
className='border-2 p-1 w-full mt-1'
/>
</div>
<div className='mb-5'>
<label htmlFor='location' className='block'>
Location
</label>
<input
type='text'
name='location'
id='location'
required
onChange={(e) => setLocation(e.target.value)}
className='border-2 p-1 w-full mt-1'
/>
</div>
<div className='mb-5'>
<label htmlFor='email' className='block'>
Email Address
</label>
<input
type='email'
name='email'
id='email'
required
onChange={(e) => setEmail(e.target.value)}
className='border-2 p-1 w-full mt-1'
/>
</div>
<div className='mb-5'>
<button
className='bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded'
type='button'
onClick={openupWidget}
>
Upload Image
</button>
</div>
<div className="flex justify-end">
<button
type='submit'
onClick={handleSubmit}
className='bg-blue-600 text-gray-50 py-2 px-6 tracking-wide whitespace-nowrap mb-10'
>
Submit
</button>
</div>
</form>
</section>
...
</>
);
};
export default AddContact;
In the code above, you did the following:
- Imported the necessary dependencies and components
- Created initial variables for the input on lines 9-14
- Created the input fields for First Name, Last Name, Phone Number, Location, Email Address, Upload Image button, and Submit button on lines 46-121
- Submitted all input values to the Xata database and redirect to the home page with lines 18-34
Adding Contact Information to the Database
Inside pages/api/add-contact.js
paste the code below:
//pages/api/add-contact.js
import { getXataClient } from "../../src/xata";
const xata = getXataClient();
const handler = async (req, res) => {
const { firstName, lastName, phoneNumber, location, email, imageUrl } =
req.body;
await xata.db.contacts.create({
firstName,
lastName,
phoneNumber,
location,
email,
imageUrl,
});
res.end();
};
export default handler;
In the code block above, you did the following:
- Imported the
getXataClient
from thexata
file that was created during initialization and also created a new instance with thegetXataClient
object - Got the
firstName
,lastName
,phoneNumber
,location
,email
,imageUrl
variables from the body of your request to add to the database - Added the variables in the Xata
create
method to create data on your database. When passing the variables in as a parameter, it is important to note these things:- The
create()
is on thecontacts
object because your database's name iscontacts
- The variables are the same as the tables on your database
- The
- Send the resulting data to the client side after saving the data in your database
Fetching Contact Information from the Database
To display all contacts information on the home page, first create your components/LandingPage.js
file and add the following code snippet:
//components/LandingPage.js
import Footer from "./Footer";
import Header from "./Header";
const LandingPage = ({ children }) => {
return (
<div className='min-h-screen flex flex-col'>
<div className='border-b border-gray-100'>
<Header />
</div>
<main className='flex-grow md:max-w-7xl md:mx-auto md:w-4/5 my-5 '>
{children}
</main>
<div className='border-t border-gray-100'>
<Footer />
</div>
</div>
);
};
export default LandingPage;
You need to import the getXataClient
from the src/xata.js
file. Create a pages/index.js
file and add the following code:
//pages/index.js
import Head from "next/head";
import { useState } from "react";
import LandingPage from "../components/LandingPage";
import { getXataClient } from "../src/xata";
import SearchList from "../components/SearchList/SearchList";
import AllContactsList from "../components/AllContactsLists/AllContactsLists";
export default function Home({ phoneBooks }) {
const [searchWord, setSearchWord] = useState();
const [searchContacts, setSearchContacts] = useState();
const handleSearch = async (e) => {
e.preventDefault();
};
return (
<div>
<Head>
<title>Contacts Phone Directory</title>
<meta name='description' content='Job board app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<LandingPage>
<div className='flex justify-center mb-4'>
<input
className='placeholder:italic placeholder:text-slate-400 block bg-white w-[50%] border border-slate-300 rounded-md py-2 pl-9 pr-3 shadow-sm focus:outline-none focus:border-sky-500 focus:ring-sky-500 focus:ring-1 sm:text-sm'
placeholder='Search for contact...'
type='text'
onChange={(e) => setSearchWord(e.target.value)}
/>
<button
className='bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded'
type='submit'
onClick={handleSearch}
>
Search
</button>
</div>
{searchContacts ? <SearchList allContacts={searchContacts} /> : ""}
{searchContacts ? "" : <AllContactsList allContacts={phoneBooks} />}
</LandingPage>
</div>
);
}
export async function getServerSideProps() {
const xata = getXataClient();
const phoneBooks = await xata.db.contacts.getAll();
return { props: { phoneBooks } };
}
In the code block above, you did the following:
- Imported the
getXataClient
from thesrc/xata.js
file insidepages/index.js
file to query all records from Xata and returned the data as phoneBooks prop toAllContactsList
andSearchList
- Used a conditional statement to render
AllContactsList
andSearchList
as children props of LandingPage insidepages/index.js
file - Created input search field for searching contact information
- Declared variable to store the initial state for
searchWord
andsearchContacts
- Passed the landing page component to
components/LandingPage.js
file
Searching for Contact Information in Database
Update the handleSearch ()
searchWord
function in pages/index.js
file with the code below.
//pages/index.js
const handleSearch = async (e) => {
e.preventDefault();
const result = await fetch("/api/search-contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
searchWord,
}),
}).then((r) => r.json());
setSearchContacts(result);
};
In the code above, you did the following:
- Make a search request using Xata’s REST API
- Retrieved the input value in the search field and pass it to the body of the request.
- Updated the
searchContacts
with the result of the searched word coming from xata database
Next, create search-contact.js
file inside pages/api
folder. Make sure the name of the created file tallies with the API endpoint used to make search request on line 3 ("pages/api/search-contact.js
")
Copy and paste the code below to pages/api/search-contact.js
file
//pages/api/search-contact.js
import { getXataClient } from '../../src/xata';
const xata = getXataClient();
const handler = async (req, res) => {
const { searchWord } = req.body;
const results = await xata.search.all(searchWord);
res.send(results);
};
export default handler;
In the code block above, you did the following:
- Imported the
getXataClient
from thexata
file and also created a new instance with thegetXataClient
object - Got the
searchWord
variable from the input field and used it to search the Xata database if it exists or not. - Send the resulting data to the client side after saving the data in your database
Next, copy and paste the code in the github gist inside your components/SearchList/SearcList.js
https://gist.github.com/fatima-ola/00541b18715247fc7d7b23b84d4b5914
In the code block above, you did the following:
- Retrieved the
allContacts
props passed to theSearchList
components - Extracted it into an array of objects
- Map through the
contactInfo
array and display the searched information
Single Contact List Creation
To get more information about individual contact displayed on the landing page when it is clicked, Create a new file, [id].js
, in a folder called contact
within the pages
directory. Paste the following code:
//pages/contact/[id].js
import { getXataClient } from "../../src/xata";
import Footer from "../../components/Footer";
import Header from "../../components/Header";
import Image from "next/image";
import { useRouter } from "next/router";
import { MdWifiCalling3 } from "react-icons/md";
import { BiMessageDetail } from "react-icons/bi";
import { BiVideo } from "react-icons/bi";
const Contact = ({ data }) => {
const router = useRouter();
const deleteContact = (id) => {
});
};
function getFirstLetter(character) {
return character.charAt(0).toUpperCase();
}
return (
<div className='min-h-screen flex flex-col'>
<div className='border-b border-gray-100 mb-6'>
<Header />
</div>
<section className='mx-auto w-[30%] sm:w-[50%] xs:w-[60%] p-8 shadow-sm border-2 hover:shadow-lg rounded-lg'>
<div className='flex justify-center'>
{data.imageUrl === "" ? (
<p className='self-center bg-pink-200 font-bold py-2 px-4 mr-2 rounded-full'>
{getFirstLetter(data.firstName)}
</p>
) : (
<Image
width={80}
height={20}
className='h-20 w-30 rounded-full'
src={data.imageUrl}
alt=''
/>
)}
</div>
<div className='text-center font-semibold'>
<p>{`${data.firstName} ${data.lastName}`}</p>
</div>
<div className='flex justify-center'>
<ul className='list-none flex m-4'>
<li className='p-3 text-blue-600 text-[22px]'>
<MdWifiCalling3 className='cursor-pointer' />
</li>
<li className='p-3 text-red-400 text-[22px]'>
<BiMessageDetail className='cursor-pointer' />
</li>
<li className='p-3 text-purple-400 text-[22px]'>
<BiVideo className='cursor-pointer' />
</li>
</ul>
</div>
<div className=''>
<div className='my-3'>
<p className='font-semibold'>Mobile:</p>
<p>{data.phoneNumber}</p>
</div>
<div className='my-3'>
<p className='font-semibold'>Location:</p>
<p>{data.location}</p>
</div>
<div className='my-3'>
<p className='font-semibold'>Email:</p>
<p className="">{data.email}</p>
</div>
</div>
<div className='flex justify-center my-6'>
<button
type='submit'
className=' bg-red-300 text-gray-50 hover:bg-red-600 py-2 px-6 rounded-full'
onClick={() => deleteContact(data.id)}
>
Delete
</button>
</div>
</section>
<div className='border-t border-gray-100 mt-6'>
<Footer />
</div>
</div>
);
};
export default Contact;
export async function getServerSideProps(context) {
const { id } = context.query;
const xata = await getXataClient();
const contact = await xata.db.contacts.read(id);
return {
props: {
data: contact,
},
};
}
The code above makes use of the getServerSideProps
with the context parameter, and this object results in using the query string. After which, the function returns props reading the dynamic route of the id
and getting one record from Xata using the read()
.
Clicking on one of the contacts listings opens a new page with the contact URL with an id like this:http://localhost:3000/contact/rec_cdt5k7ft80rj82cd2kpg
.
Deleting Contact Information from Database
Create a pages/api/delete-contact.js
file and add this code below to delete a contact from your database using the id
you got from the request body:
import { getXataClient } from '../../src/xata';
const xata = getXataClient();
const handler = async (req, res) => {
const { id } = req.body;
await xata.db.contacts.delete(id);
res.end();
};
export default handler;
Next, update the deleteContact()
function in pages/contact/[id].js
file with the code below.
const deleteContact = (id) => {
fetch("/api/delete-contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: id }),
}).then(() => {
router.push("/");
});
};
In the code above, you did the following
- Created a function to query your
/api/delete-product
endpoint. - Passed the
id
of the product you want to delete as parameter. The/api/delete-product
handler needs theid
to find and successfully delete a contact. After deleting the product, you route back to landing page.
In the end, this is how your final application should look like:
https://www.loom.com/share/9546ccd0a23c4a02ad6c22bb681a2cfe
Conclusion
In this article, you learned how to use Xata and Cloudinary to build a contact phone directory that can add a new contact to the directory, view individual contacts, search for contact and delete the contact.
Resources
- Xata Documentation
- Cloudinary Upload Preset
- Setup Upload Widget