Contact

React Art Image Gallery Screenshot

How to Build an Art Gallery with React – Part 6: Wrap Up

Congratulations! Your hard work is done. Now it is time to package it and share your work with the world. But, before we do that, let’s review what we’ve done.

Check Your Project Directory

Your list of folders and files should look like this:

react_art_gallery
├── README.md
├── node_modules
|   └── All the installed package files live here
├── package-lock.json
├── package.json
├── .gitignore
├── public
|   ├── images
|   |   └── Your artwork that's featured in your gallery
│   ├── index.html
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── ImageCard.js
    ├── ImageGallery.js
    ├── images.json
    ├── index.css
    └── index.js

Double-Check Your Work

Click the file listed below to review the completed code for the component, stylesheet, etc.

App.css

This CSS file holds all styles for your App, ImageGallery, and ImageCard components.

.App {
  text-align: center;
  padding: 20px;
  min-height: 100vh;
}

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 8px;
  justify-items: center;
}

.image-card {
  text-align: center;
  max-width: 200px;
}

.image-card img {
  max-width: 100%;
  border-radius: 10px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, .1);
  cursor: pointer;
}

.categories {
  margin-top: 10px;
  margin-bottom: 20px;
  display: flex;
  flex-flow: row wrap;
  gap: 1em;
  justify-content: center;
}

.category-pill {
  padding: 5px 12px;
  background-color: #333;
  color: #f1f1f1;
  border: 1px solid #f1f1f1;
  border-radius: 20px;
  font-size: 14px;
  font-weight: 500;
  transition: all .3s, color .3s;
  cursor: default;
}

.category-pill:hover,
.category-pill.active {
  background-color: #428645;
}

.popover {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, .7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s ease, visibility 0s .3s;
}

.popover.show {
  opacity: 1;
  visibility: visible;
  transition: opacity .3s ease, visibility 0s 0s;
}

.popover-content {
  background-color: rgba(0, 0, 0, .7);
  padding: 25px 50px;
  border-radius: 8px;
  position: relative;
}

.popover-content img {
  max-width: 100%;
  max-height: 80vh;
  object-fit: contain;
  box-shadow: -5px 5px 10px rgba(0, 0, 0, .7);
}

.close-button {
  position: absolute;
  font-size: 1.25em;
  font-weight: bold;
  top: 10px;
  right: 10px;
  background-color: #f96e46;
  color: white;
  border: none;
  border-radius: 50%;
  padding: .25em .5em;
  cursor: pointer;
}
App.js

This JavaScript file is the stage for all your other components and includes all high-level functionality to properly pass along properties and capture/process events.

import { useEffect, useState } from 'react';
import './App.css';
import ImageGallery from './ImageGallery';
import data from './images.json';

const shuffleArray = (array) => {
  const shuffledArray = [...array];
  for (let i = shuffledArray.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
  }
  return shuffledArray;
}

function App(){

  const [filters, setFilters] = useState([]);
  const [images, setImages] = useState([]);
  const [selectedImage, setSelectedImage] = useState(null);
  
  useEffect(() =>{
    const shuffledData = shuffleArray(data);
    setImages(shuffledData);
  }, []);
  
  const toggleFilter = (category) => {
    setFilters((prevFilters) => {
      if (prevFilters.includes(category)) {
        return prevFilters.filter((filter) => filter !== category);
      } else {
        return [...prevFilters, category];
      }
    });
  }

  const uniqueCategories = [...new Set(images.flatMap((image) => image.categories))].sort();

  const filteredImages = filters.length === 0
    ? images
    : images.filter((image) => filters.every((filter) => image.categories.includes(filter)));
	
  const [selectedImage, setSelectedImage] = useState(null);

  const openPopover = (image) => { setSelectedImage(image); }
  const closePopover = () => { setSelectedImage(null); }
  
  return (
    <div className='App'>
      <h1>Art Gallery</h1>
      <p>Select categories to filter</p>
      <div className='categories'>
        {uniqueCategories.map((category) => (
          <button
            key={category}
            className={`category-pill`}
            onClick={() => toggleFilter(category)}
          >{category}</button>
        ))}
      </div>
      {
         filteredImages.length === 0
           ? (
             <p style={{ color: 'red' }}><em>No images found for the selected categories.</em></p>
           )
           : (
             <ImageGallery
               images={filteredImages}
               toggleFilter={toggleFilter}
             />
           )
      }
      
      {
         filteredImages.length === 0
           ? (
             <p style={{ color: 'red' }}><em>No images found for the selected categories.</em></p>
           )
           : (
             <ImageGallery
               images={filteredImages}
               toggleFilter={toggleFilter}
               onImageClick={openPopover}
             />
           )
      }

      {
        selectedImage && (
          <div
            className={`popover ${selectedImage ? 'show' : ''}`}
            onClick={closePopover}
          >
            <div
              className='popover-content'
              onClick={
                (e) => e.stopPropagination()
              }
            >
              <img
                src={`${process.env.PUBLIC_URL}${selectedImage.src}`}
                alt={selectedImage.title}
              />
              <p>
                {selectedImage.title}
              </p>
              <button
                className='close-button'
                onClick={closePopover}
              >
                &times;
              </button>
            </div>
          </div>
        )
      }
    </div>
  );
}

export default App;
ImageCard.js

This is the child component of ImageGallery.js. This component renders the individual images along with their corresponding categories and titles. It also triggers the event to display the image’s full-screen Popover.

const ImageCard = ({ image, toggleFilter, onImageClick }) => {
  return (
    <div className="image-card">
      <img
        src={`${process.env.PUBLIC_URL}${image.src}`}
        alt={image.title}
        onClick=(() => onImageClick(image))
      />
      <p><strong>{image.title}</strong></p>
      <div className="categories">
        {
          image.categories.length > 0
            ? (
              image.categories.map((category) => (
                <span
                  key={category}
                  className="category-pill"
                  onClick={
                    (e) => {
                      e.stopPropagination();
                      toggleFilter(category)
                    }
                  }
                >{category}</span>
              ))
            )
            : (<span>Add a category</span>);
        }
      </div>
    </div>
  );
}

export default ImageCard;
ImageGallery.js

This is the child of the App component and ImageCard’s parent. This mostly transitory component contains all ImageCard components that are rendered based on the data fed to it from the App component & images.json.

import ImageCard from './ImageCard';

const ImageGallery = ({ images, toggleFilter, onImageClick }) => {
  return (
    <div className="gallery">
      {images.map((image, i) => (
        <ImageCard
          key={i}
          image={image}
          toggleFilter={toggleFilter}
          onImageClick={toggleFilter}
        />
      ))}
    </div>
  );
}

export default ImageGallery;
images.json

This is your data file. Provide your image’s location, title, and any categories it belongs to. Your images.json file can contain as few or as many images as you like, as long as your JSON follows this pattern (replace all values with those that match your images):

[
  {
    "src": "/path/to/image.png",
    "categories": [
      "tag 1",
      "another descriptor",
      "3rd tag",
      "tag the fourth"
    ],
    "title": "Image Title"
  },
  {
    //same as before
  }
]
index.css

This CSS file contains all general styles that affect the app and the page that contains it. Use this file to define general tags like body, code, button, a, h1-h5, p, etc.

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #2f2f2f;
  color: #ddd;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
index.js

This is React’s entry point to your app. Your app can’t be built or rendered properly without this file. Your App component must be contained within the <React.StrictMode>, which is inside the root of your document.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App.js';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Build For Launch

Open a command prompt terminal of your choice and navigate to your project directory. Type the following command:

npm run build

This will execute React’s build program, where it creates a fully optimized, compiled version of your project. In doing so, it checks your code for any confusions and errors. If it finds no errors, you should see a message similar to this:

NPM Run Build Screenshot

Open your newly created build folder to see a file directory similar to:

build
├── images
|   └── Your artwork that's featured in your gallery
└── static
    ├── css
    |   ├── main.abcdef01.css
    |   └── main.abcdef01.css.map
    ├── js
    |   ├── 123.4567890a.chunk.js
    |   ├── 123.4567890a.chunk.js.map
    |   ├── main.01234567.js
    |   ├── main.01234567.js.LICENSE.txt
    |   └── main.01234567.js.map
    ├── asset-manifest.json
    ├── index.html
    └── robots.txt

Take all contents of this build folder and upload them to wherever you want your app to live. Once you do, you should be able to play around with it.

Add-Ons & Challenges

If you want to continue with this project, here are some ideas you can add to further improve your app’s usability:

  1. Add keyboard functionality to cycle through images when the Popover is visible. Also add clickable Previous / Next buttons to turn the Popover into a more dynamic carousel.
  2. Add thumbnails to the Popover of a few images that share the selected image’s category tags, giving higher placement to those that share the most categories.
  3. Add a text-input search bar that shows images with titles that match or contain the input’s value.
  4. Expert Challenge! Figure out how this can be integrated into WordPress or some other CMS as a plugin.

CONGRATS ON A JOB WELL DONE!

Whether you take on any of the additional challenges or not, you should be proud of yourself. React can be confusing and complicated in the beginning, but once you understand how to build components, utilize useState and useEffect methods, and pass around properties and events, you can build anything your mind conceives!

In the next series, I’ll show how to expand on this Art Gallery with an interactive, timed slideshow.


About the Author

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *