Build Contact Phone Directory with NextJS using Xata and Cloudinary
Tags: javascript, tutorial, webdev, xata
Published: 2022-11-29 06:57:34

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](https://cloudinary.com/documentation/upload_presets?utm_source=hackmamba&utm_medium=hackmamba-blog&utm_campaign=hackmamba-hackathon) is a cloud-based video and image management platform which provides services for maintaining and converting uploaded media for web applications.  [Xata](https://xata.io/docs/overview?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is a [Serverless Data](https://xata.io/docs/intro/serverless-data-platform??utm_source=hackmamba&utm_medium=hackmamba-blog&utm_campaign=hackmamba-hackathon) 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](https://github.com/fatima-ola/contact-phone-directory) and access the live demo [here](https://contact-phone-directory.vercel.app/).

## **Prerequisite**
Understanding this article requires the following:
- Installation of Node.js
- Knowledge of JavaScript and React.js
- A Cloudinary account(sign up [here](https://cloudinary.com/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog))
- A Xata account(sign up [here](https://xata.io/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog))
- Understanding of [Tailwind CSS](https://tailwindcss.com/docs/installation?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) for styling

## **Installation and Setup**
Run this command on your terminal to create a Next app.

```javascript
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:

```javascript
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**.

![create xata database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t8xm0mugn51faljme6xa.png)

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.


![xata schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r647qc64lm7h7e247kta.png)


## **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:

```javascript
npm i -g @xata.io/cli
```
Next, run the command below to authenticate:

```javascript
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.

```javascript
xata init
```

![xata init](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t9cfdgw3c0vkzxb7i6nc.PNG)


## **Tailwind Setup**

Install Tailwind CSS in your project with the command below:

```javascript
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.


```javascript
 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`.


```javascript
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
```

## **Cloudinary Setup**

Add Cloudinary dependencies to your project with the command below:

```javascript
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`.


```javascript
//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](https://cloudinary.com/console?utm_source=hackmamba&utm_medium=hackmamba-blog&utm_campaign=hackmamba-hackathon) and click on the **Settings** tab.
Next, click on the **Upload** tab and scroll down to the **Upload presets** section of the page.

![cloudinary console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tshwryc2esa6zm7nkc6z.png)

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.


![upload preset page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qa7b4shh8nfn4tc34evg.png)

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:


```javascript
//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, your `cloud_name` and your upload preset. To get your `cloud_name`, check the [Cloudinary Dashboard](https://cloudinary.com/console?utm_source=hackmamba&utm_medium=hackmamba-blog&utm_campaign=hackmamba-hackathon)
- 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.



```javascript
<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. 

```javascript

/* 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:

```javascript
//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 the `xata` file that was created during initialization and also created a new instance with the `getXataClient` 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 the `contacts` object because your database's name is `contacts`
    - The variables are the same as the tables on your database
- 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:

```javascript
//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:

```javascript
//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 the `src/xata.js` file inside `pages/index.js` file to query all records from Xata and returned the data as phoneBooks prop to `AllContactsList` and `SearchList`
- Used a conditional statement to render `AllContactsList` and `SearchList` as children props of LandingPage inside `pages/index.js` file
- Created input search field for searching contact information
- Declared variable to store the initial state for `searchWord` and `searchContacts`
- 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.

```javascript
//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


```javascript
//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 the `xata` file and also created a new instance with the `getXataClient` 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](https://gist.github.com/fatima-ola/00541b18715247fc7d7b23b84d4b5914
)

In the code block above, you did the following:

- Retrieved the `allContacts`  props passed to the `SearchList` 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:


```javascript
//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:

```javascript
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.


```javascript
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 the `id` 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:

![final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7yfgf9xte42om8j0zpim.png)
[https://www.loom.com/share/9546ccd0a23c4a02ad6c22bb681a2cfe](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](https://xata.io/docs/overview?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
    - [Cloudinary Upload Preset](https://cloudinary.com/documentation/upload_presets?utm_source=hackmamba&utm_medium=hackmamba-blog&utm_campaign=hackmamba-hackathon)
    - [Setup Upload Widget](https://cloudinary.com/documentation/upload_widget?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)