How to build portfolio gallery with GatsbyJS & React Hooks

tutorialsMarch 27, 2020

When I was building my personal webpage, I figured out that I need to build component to show my recent work. Browsing through internet I found a lot of useful example but usualy based on stateful components which was approach I tried to avoid. Today I show you how to build one for your own use.

You can find working example of this component here & check the repository

portfolio

Sourcing the content

In this example we will source data from JSON file. You can find the plugin with great documentation here.

In my personal portofilo I'm sourcing content from Netlify CMS (docs here). It depends on your project from where you source your data but the approach should be similar.

plugins: [
  `gatsby-transformer-json`,
  {
    resolve: `gatsby-source-filesystem`,
    options: {
      path: `./src/data/`,
    },
  },
]

As you can see in the gatsby-config.js example above. I put my json data to /src/data/ folder.

Adding content

In data.json we make object with all our portfolio items.

I added four attributes to each item:

  • Name
  • Image
  • Technolgies object
  • Link to website
[
  {
    "name": "Portfolio item 1",
    "img": "img/1.jpg",
    "link": "https://www.pexels.com/photo/mountain-surrounded-with-fog-1772973/",
    "technologies": [
      "React",
      "GatsbyJS"
    ]
  },
  {
    "name": "Portfolio item 2",
    "img": "img/2.jpg",
    "link": "https://www.pexels.com/photo/aerial-photography-of-mountain-1321059/",
    "technologies": [
      "React",
      "GatsbyJS",
      "Netlify CMS"
    ]
  }
  ...
  ...
}

Don't forget to put images to subfolder next to you data.json. In this example it is /src/data/img/

Making the component

First of all we need to source our data from GraphQl. We can find our data in allDataJson query. Gatsby Transformer Sharp plugin will make from our image path string object so we can use it with Gatsby Image plugin.

We also need to import useStaticQuery, graphql and gatsby-image plugins.

import React from 'react'
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"

export default () => {
  const data = useStaticQuery(graphql`
    query HeaderQuery {
      allDataJson {
        edges {
          node {
              name
              img {
                childImageSharp {
                    fluid(maxWidth: 4000) {
                    ...GatsbyImageSharpFluid_noBase64
                    }
                }
              }
              link
              technologies
          }
        }
      }
    }
  `)

  //Portfolio items
  const items = data.allDataJson.edges;

  return (
  )
}

Ok, let's add some content. We will use map function to loop through all portfolio items.

return (
 <section className="portfolio">

   // Loop through portfolio items
   {items.map(({ node }, index) =>

      <div
         key={index}
         className="portfolio__item">


            // Check if the image object exists
            {node.img &&
               <div className="item__image">
                 // Using gatsby-image plugin
                 <Img
                    fluid={node.img.childImageSharp.fluid}
                    alt={`${node.name} image`} />
               </div>
             }

             <h2>{node.name}</h2>


             // Check if the technolgies object exists
             {node.technologies &&
                <div className="item__technologies">
                   // Loop through technologies object
                   {node.technologies.map((technology, index) =>
                        <p key={index}>{technology}</p>
                    )}
                </div>
             }


             // Check if link exists
             {node.link &&
                <a href={node.link}>Open website</a>
             }

        </div>
    )}
 </section>
)

No let's make same State Hooks. We will need three all together:

// Boolean state for modal opening/closing
const [modal, setModal] = React.useState(false)
// State for storing selected portfolio index
const [currentIndex, setCurrentIndex] = React.useState(0)
// State storing side to which animation of slider should go
const [slideSide, setSlideSide] = React.useState(null)

Now lets add function which opens modal after click on portfolio item.
As you can see below we set the modal state to true and store portfolio item index.

const onClick = index => {
  setModal(true)
  setCurrentIndex(index)
}

Next we will make previous and next function to get through slides in modal.

const previous = () => {

  // Set slide animation to left
  setSlideSide("left") 
  
  // If first item or not
  currentIndex > 0
    ? setCurrentIndex(currentIndex - 1)
    : setCurrentIndex(items.length - 1)
}

const next = () => {

  // Set slide animation to right
  setSlideSide("right") 
  
  // If last item or not
  currentIndex < items.length - 1
    ? setCurrentIndex(currentIndex + 1)
    : setCurrentIndex(0)
}

Lets build the modal now. We need to check if modal is set to true first.

// Check if modal state === true
modal &&
  <div
    className={`portfolio__modal`}>
      // Clickable overlay to close modal
      <div
        className="modal__overlay"
        onClick={() => setModal(false)} />

      // Previous item button
      <button
        onClick={() => previous()}
        className="modal__previous"/>

        // slideSide state and accordingly class changes depending             
        // on which button clicked            
        // onAnimationEnd slideSide state is set back to null
        <div
          className={`modal__content modal__content--slide-{slideSide}`}
          onAnimationEnd={() => setSlideSide(null)}>
            
          // Modal closing button
          <button
          onClick={() => setModal(!modal)}
          className="modal__close"/>

          // Active item image
          {activeSlide.img &&
            <div className="item__image">
              <Img
                fluid={activeSlide.img.childImageSharp.fluid}
                alt={`${activeSlide.name} image`} />
            </div>
          }
                      
          // Active item name
          <h2>{activeSlide.name}</h2>                     
            
          // Active item technologies
          {activeSlide.technologies &&
            <div className="item__technologies">
              {activeSlide.technologies.map((technology, index) =>
                <p key={index}>{technology}</p>
              )}
            </div>
          }

          // Active item link
          {activeSlide.link &&
            <a href={activeSlide.link}>Open website</a>
          }

      </div>

      // Next item button
      <button
        onClick={() => next()}
        className="modal__next"/>
  </div>

I put here example of left right slider animation also.

.portfolio__modal .modal__content--slide-left {
  animation: slideLeft 0.2s ease-out;
}
.portfolio__modal .modal__content--slide-right {
  animation: slideRight 0.2s ease-out;
}

@keyframes slideRight {
  0% {
    transform: translateX(5%) translateY(-50%);
    opacity: 0.2;
  }
  100% {
    transform: translateX(0) translateY(-50%);
    opacity: 1;
  }
}

@keyframes slideLeft {
  0% {
    transform: translateX(-5%) translateY(-50%);
    opacity: 0.2;
  }

  100% {
    transform: translateX(0) translateY(-50%);
    opacity: 1;
  }
}

And when we put it all together we have very basic example of portfolio component. I hope that I made it at least slightly understendable and you find useful.

p.s.: apologize my gramatics, I did my best but english is not my native language

You can find repository here and live example here

Latest articles