Build Contact Phone Directory with NextJS using Xata and Cloudinary

Fatima Olasunkanmi-Ojo - Nov 29 '22 - - Dev Community

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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

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
Enter fullscreen mode Exit fullscreen mode

Next, run the command below to authenticate:

xata auth login
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

xata init

Tailwind Setup

Install Tailwind CSS in your project with the command below:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

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: [],
 }
Enter fullscreen mode Exit fullscreen mode

Next, add the @tailwind directives for each of Tailwind’s layers to your styles/global.css file.

 @tailwind base;
 @tailwind components;
 @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Cloudinary Setup

Add Cloudinary dependencies to your project with the command below:

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

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>
Enter fullscreen mode Exit fullscreen mode

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.

cloudinary console

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

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();
  };
Enter fullscreen mode Exit fullscreen mode

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
  • 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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

//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;
Enter fullscreen mode Exit fullscreen mode

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 } };
}
Enter fullscreen mode Exit fullscreen mode

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.

//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);
  };
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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:

//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,
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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("/");
    });
  };
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .