Skip to content

Building a Portfolio With Xata, Cloudinary, and Next.js.

Telling stories is one of the most effective ways to sell ourselves. In recent times, visual content has dominated the web, making storytelling with pictures an even more captivating technique.

This article aims to build a portfolio that provides an overview of storing, sending, and retrieving data and using these data to promote ourselves.

At the end of the article, your portfolio should look like this.

The live demo of this application is available here. Here’s the source code for the project.

To complete this project, you’ll need to have:

  • Knowledge of React and Next.js.
  • Node and its package manager, npm. Run the command node -v && npm -v to verify that you have them installed or install them from here.
  • Code Editor.
  • TypeScript
  • Xata account. Create one here.
  • Cloudinary account. Create one here.
Note:

To get you started, the prerequisites are attached with links to guide you. You can also catch up with crash courses.

Xata is a branchable, serverless database that offers a web UI making working with data seamless, an indefinitely scalable data API, and more.

Xata allows you to store, retrieve, update, delete, search through data, and more.

Cloudinary provides extensive video management and a cloud-based image platform while optimally creating, managing, and delivering digital experiences. With Cloudinary, you can store, upload, transform, download, and embed images.

Head to Cloudinary and log in to your account or create an account here.

Signing up or Logging into Cloudinary
Retrieving personal details from our dashboard

After successfully creating an account, Cloudinary will then redirect you to your account’s dashboard page, where you can see account details that will be useful later on, including your:

  • Cloud name
  • API Key
  • API Secret
Note:

Do not share these details with anyone.

Next, go to Media Library to create a folder to house your images.

Navigating to the media library
Creating folders

Upload images to the created folder with a simple drag and drop.

Uploading images

Next, copy the uploaded image links. You’ll use this when building your application.

Copying images link

Visit Xata to log in to your account or create an account here.

Signing up or Logging into Xata

After creating an account, Xata will redirect you to create a workspace for your project.

Creating a workspace

Next, you’ll create a database to house your project data.

Creating a database

You’ll then create tables serving as the database component holding your data.

Creating a table

Finally, you’ll add data to the table you created in the step above.

Adding data to a table
Note:

The image column content is the URLs of images copied from Cloudinary.

You’ll be bootstrapping with-xata template using the create-next-app command.

Open your Node.js terminal after you’ve navigated to the directory you want the project on:

npx create-next-app --example with-xata with-xata-appCode language: JavaScript (javascript)

You’ll get some prompts, including a prompt to create a dummy new table (nextjs_with_xata_example). If you already have an existing database, you can ignore. Otherwise, accept to get some content displayed on npm run start.

In the terminal, run npm install @xata.io/cli -g to install Xata for global use.

Then link an existing database by initializing with xata init --db=[databaseUrl]. Run npm run start:xata to get Xata started.

Let’s add our logo to the _app.tsx just above the Component declaration, as shown below.

// _app.tsx
<h2 className='logo'>MODE-EL</h2>
      <Component {...pageProps} />
Code language: HTML, XML (xml)

In the index.tsx file, replace the default syntax with the code snippet below.

    import type { InferGetServerSidePropsType } from "next";
    import { XataClient, getXataClient } from "../utils/xata.codegen";

    export default function IndexPage({
    }: InferGetServerSidePropsType<typeof getServerSideProps>) {
      // Some UI rendering
      return (
        <div className="homepage">
           {/* Take the content here out to input your own content */}
        {/* Hero Section */}

        {/* About section */}

        {/* More about section */}

        {/* Testimonial section */}

        {/* Contact section */}

        {/* Scroll to top */}

        {/* Footer section */}
        </div>
      );
    }
Code language: PHP (php)

This displays the content of different sections, as seen above.

Let’s get into defining what each section does. In the Hero section, display the images of links from Cloudinary as the first section to be seen when a viewer visits your portfolio.

    // Hero section
    <div className="homepage">
        <div className='top-images'>
           <img src="image link from cloudinary" alt=" " />
           <img src="image link from cloudinary" alt=" " />
        </div>

         <div className='top-content'>
            <h1>think <span>you've seen</span> magic?</h1>
             <div className='top-content-btn'>
               <button id="contactBtn" className='left-button'> CONTACT US </button>
               <button className='right-button'>GALLERY</button>
          </div>
     </div>
Code language: HTML, XML (xml)

Next, the About section shows some information about you, such as text, call-to-action, etc.

    // About section
    <div className='dancing-lady'>
       <img src="image link from cloudinary" alt=" "/>
    </div>

    <div className='cta-one'>
       <h2># Photography Agency</h2>
        <p>
            We are the problem solvers that will help you convey your message in
            pixels. We communicate creatively both online and offline and always
            putting a smile on your face through satisfaction.
        </p>
    </div>
Code language: HTML, XML (xml)

Also, More about section gives viewers more context about the portfolio.

    // More about section
    <div className='woman-in-glasses'>
       <img src="image link from cloudinary" alt=" " />
    </div>
    <div className='cta-two'>
       <h2>Want To Be Discovered?</h2>
        <p>
           We are the problem solvers that will help you convey your message in
            pixels. We communicate creatively both online and offline and always
            putting a smile on your face through satisfaction.
        </p>
    </div>
    <div className='guy-in-glasses'>
       <img src="image link from cloudinary" alt=" " />
    </div>
Code language: HTML, XML (xml)

In the Testimonial section, we’ll get data in the form of testimonials from Xata to display in your project.

    // Testimonial section
    <div className='testimonials'>
       <div className='testimonials-head'>
         <h2>TESTIMONIALS</h2>
       </div>
       <div className='testimonial'>
        {/* Fetched data from xata to be displayed */}
    </div>
Code language: HTML, XML (xml)

Next is the Contact section, where you’ll display a form to get viewers’ data and send it to the database.

    // Contact section
    <div id="contact">
       {/* contact form*/}

    {/* Scroll to top */}
      <i 
          className=" ">
          {/* arrow up icon */}
      </i>
    </div>
Code language: HTML, XML (xml)

The last section in the index.tsx is the Footer section, where you’ll place the content of your footer.

    // Footer section
    <footer>
       <p>
         MODE-EL &#169; 2022
       </p>
    </footer>
Code language: HTML, XML (xml)

Let’s make our contact form displayed on the index page and serve as our data collection point to be sent to Xata.
In the root folder, we’ll create a components folder, then create our ContactForm.tsx

    // components/ContactForm.tsx

    export const ContactForm = () => {
      return (
        // form to submit details
        <div className="">
          <form>
            <p>Need us!</p>
            <input
              type="text"
              name="name"
              value={}
              required
              placeholder="Name"
            />
            <input
              type="email"
              name="email"
              value={}
              required
              placeholder="Email"
            />
            <input
              type="text"
              name="message"
              value={}
              required
              placeholder="Message"
            />
            <button>SEND</button>
          </form>
        </div>
      );
    };
Code language: JavaScript (javascript)

To be able to send data to the Xata table created, you need to send the value of the inputs as responses.

In the api folder located in the pages folder, create a submit-response.ts file where you’ll call the API.

    // pages/submit-response.ts

    import { NextApiHandler } from "next";
    import { XataClient, getXataClient } from "../../utils/xata.codegen";
    import dotenv from "dotenv";
    dotenv.config();

    // connecting to xata
    const handler: NextApiHandler = async (req, res) => {
      const xata = new XataClient({ apiKey: process.env.{insert your API key} });
      const { name, email, message } = req.body;
      await xata.db.contacts.create({
        name,
        email,
        message,
      });
      res.end();
    };

    export default handler;
Code language: JavaScript (javascript)

In the ContactForm.tsx, implement your API route, submit-response.ts.

    // components/ContactForm.tsx

    import { useState } from "react";

    export const TestForm = () => {
      const [name, setName] = useState("");
      const [message, setMessage] = useState("");
      const [email, setEmail] = useState("");

      // fetch xata API
      const send = () => {
        fetch("api/submit-response", {
          method: "POST",
          headers: {
            Authorization: `Basic ${process.env.{insert your API key}}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            email: email,
            message:message,
            name: name,
          }),
        }).then(() => window.location.reload());
      };

      return (
        // form to submit details
    ...........................................................
Code language: JavaScript (javascript)

Then, import the ContactForm.tsx in your index.tsx.

    // Contact section

    <div id="contact">
    {/* Import at the top */}
      <ContactForm />
      <i 
          className=" ">
          {/* arrow up icon */}
      </i>
    </div>
Code language: HTML, XML (xml)

At this point, your ContactForm.tsx on the index.tsx should look like this:

Contact form

This contact form will send data to the Xata database. Try filling, submitting, and checking the data table in Xata, and you’ll see the table populated with what you just submitted.

Now, you’ll get the data you created on your Xata database when you created the table, as this is what you’ll display as your testimonial section.

    // index.tsx

    import type { InferGetServerSidePropsType } from "next";
    import { XataClient, getXataClient } from "../utils/xata.codegen";
    import { ContactForm } from "../components/ContactForm";
    import dotenv from "dotenv";
    dotenv.config();

    export default function IndexPage({
      // adding the props in this case links
      links,
    }: InferGetServerSidePropsType<typeof getServerSideProps>) {

      // Some UI rendering
      return (
       <div className="homepage">
         {/* different sections of the page*/}
      </div>
      );
    }

    // fetching db from xata
    export const getServerSideProps = async () => {
    const xata = new XataClient({ apiKey: process.env.{insert your API key} });
    const links = await xata.db.clients.getAll();
      return {
        props: {
          links,
        },
      };
    };
Code language: JavaScript (javascript)

Then, loop through the data from Xata’s database on the testimonial page.

          {/* testimonial section*/}
     <div className='testimonials'>
       <div className='testimonials-head'>
         <h2>TESTIMONIALS</h2>
       </div>
       <div className='testimonial'>
           {links.map((link) => {
                return (
                  <section className='testimonial-single' key={link.id}>
                    <img src={link.image} alt="testimonial image" />
                    <p><b>{link.name}</b></p>
                    <p>{link.occupation}</p>
                    <p>{link.description}</p>
                  </section>
                );
              })}
        </div>
Code language: HTML, XML (xml)

If implemented correctly, you should have this result:

testimonial section

In the index.tsx page, you’ll be writing functions for the scroll behavior.

      // index.tsx
    // scroll to top
      const onBtnClick = (e) => {
        e.preventDefault();
        setTimeout(() => {
          window.scrollTo({
            top: document.getElementById("contact").offsetTop - 60,
            behavior: "smooth",
          });
        }, 50);
      };

      // scroll to top
      const onIconClick = (e) => {
        e.preventDefault();
        setTimeout(() => {
          window.scrollTo({
            top: document.getElementById("contactBtn").offsetTop - 60,
            behavior: "smooth",
          });
        }, 50);
      };

     return (
       <div className="homepage">
         {/* different sections of the page*/}
      </div>
      );
    }

    .................................................................
Code language: JavaScript (javascript)

Then, add the functions to click events in the hero section and contact section, as shown below.

    // hero section
    <div className='top-content'>
        <h1>think <span>you've seen</span> magic?</h1>
         <div className='top-content-btn'>
           <button id="contactBtn" onClick={onBtnClick} className='left-button'> 
              CONTACT US 
            </button>
           <button className='right-button'>GALLERY</button>
         </div>
     </div>
Code language: HTML, XML (xml)
    // contact section
    <div id="contact">
      <ContactForm />
      <i
          onClick={onIconClick}
          className=" ">
          {/* arrow up icon */}
      </i>
    </div>
Code language: HTML, XML (xml)

In the hero section of the index.tsx, link the gallery button to a gallery page that you’ll create shortly.

    // hero section
    <div className='top-content'>
        <h1>think <span>you've seen</span> magic?</h1>
         <div className='top-content-btn'>
           <button id="contactBtn" onClick={onBtnClick} className='left-button'> 
              CONTACT US 
            </button>

             {/* import Link at the top */}
           <Link href="/gallery">
                <button className='right-button'>GALLERY</button>
              </Link>
         </div>
     </div>
Code language: HTML, XML (xml)

In the pages folder, create a new file gallery.tsx. This page is where the Cloudinary API and personal details collected will be used to interact with the images uploaded to Cloudinary.

    // gallery.tsx
    import type { InferGetServerSidePropsType } from "next";
    import Link from "next/link";

    export default function gallery({
      images,
    }: InferGetServerSidePropsType<typeof getServerSideProps>) {

      // Some UI rendering
      return (
        <div className="gallery">     
        </div>
      );
    }

    // Calling cloudinary API
    export const getServerSideProps = async () => {
      const results = await fetch(
        `https://api.cloudinary.com/v1_1/${process.env.{your cloudinary cloud name}}/resources/image`,
        {
          headers: {
            Authorization: `Basic ${Buffer.from(
              process.env.{your cloudinary API key} +
                ":" +
                process.env.{your cloudinary API secret}
            ).toString("base64")}`,
          },
        }
      ).then((res) => res.json());

      const { resources } = results;

      const images = resources?.map((resource) => {
        const { width, height } = resource;
        return {
          id: resource.asset_id,
          title: resource.public_id,
          image: resource.secure_url,
          width,
          height,
        };
      });
      return {
        props: {
          images,
        },
      };
    };
Code language: JavaScript (javascript)

To display the images, map through the images gotten through the API.

     // gallery.tsx 
    // Some UI rendering
      return (
    div className="gallery">
      {/* Looping through cloudinary images */}
      <div className="images">
        {images?.map((image) => {
          return (
            <div key={image.id}>
              <img src={image.image} alt={image.title} />
            </div>
          );
        })}
      </div>

      <nav>
        <Link className="link" href="/">
          HOME
        </Link>
      </nav>
    </div>
    );
    }
Code language: HTML, XML (xml)

This will render a collection of images from Cloudinary uploads, as shown below.

Gallery page

You’ve successfully built a photography portfolio using Cloudinary, Next.js, and Xata, where creators can display their work, show comments from previous clients and get feedback or messages from viewers.

If found this article helpful and want to discuss it in more detail, head over to Cloudinary Community forum and its associated Discord.

Back to top

Featured Post