{"id":47,"date":"2025-07-01T14:26:03","date_gmt":"2025-07-01T18:26:03","guid":{"rendered":"https:\/\/www.shawntgray.com\/blog\/?p=47"},"modified":"2025-07-01T14:26:59","modified_gmt":"2025-07-01T18:26:59","slug":"how-to-build-an-art-gallery-with-react-part-5-add-cool-features","status":"publish","type":"post","link":"https:\/\/www.shawntgray.com\/blog\/how-to-build-an-art-gallery-with-react-part-5-add-cool-features\/","title":{"rendered":"How to Build an Art Gallery with React \u2013 Part 5: Add Cool Features"},"content":{"rendered":"\n<p>We&#8217;re almost there! In this part, we&#8217;ll cover 2 topics:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Add more functionality to your <strong>App <\/strong>and <strong>ImageCard <\/strong>components that will allow you to view images in a full-screen <strong><em>Popover <\/em><\/strong>modal.<\/li>\n\n\n\n<li>Add a <strong><em>shuffle <\/em><\/strong>feature that randomizes your images each time you load your app.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Let&#8217;s See Those Big Images<\/h2>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"802\" height=\"796\" src=\"https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/popover.png\" alt=\"Popover\" class=\"wp-image-67\" srcset=\"https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/popover.png 802w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/popover-300x298.png 300w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/popover-150x150.png 150w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/popover-768x762.png 768w\" sizes=\"auto, (max-width: 802px) 100vw, 802px\" \/><\/figure>\n\n\n\n<p>Click an image. A <strong><em>popover <\/em><\/strong>modal appears, displaying the full-size image, its title, and a close button, while darkening the background to give it the spotlight.<\/p>\n\n\n\n<p>To do this, we&#8217;ll first modify the <strong>App <\/strong>component.<\/p>\n\n\n\n<p><strong>App.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Imports...\n\nfunction App(){\n  \/\/ previously defined filters, images, toggleFilter, uniqueCategories, and filteredImages\n\n  const &#91;selectedImage, setSelectedImage] = useState(null);\n\n  const openPopover = (image) => { setSelectedImage(image); }\n  const closePopover = () => { setSelectedImage(null); }\n  \n  return (\n    &lt;div className='App'>\n      {\/* Previously built h1, paragraph, and categories container *\/}\n      \n      {\n         filteredImages.length === 0\n           ? (\n             &lt;p style={{ color: 'red' }}>&lt;em>No images found for the selected categories.&lt;\/em>&lt;\/p>\n           )\n           : (\n             &lt;ImageGallery\n               images={filteredImages}\n               toggleFilter={toggleFilter}\n               onImageClick={openPopover}\n             \/>\n           )\n      }\n\n      {\n        selectedImage &amp;&amp; (\n          &lt;div\n            className={`popover ${selectedImage ? 'show' : ''}`}\n            onClick={closePopover}\n          >\n            &lt;div\n              className='popover-content'\n              onClick={\n                (e) => e.stopPropagination()\n              }\n            >\n              &lt;img\n                src={`${process.env.PUBLIC_URL}${selectedImage.src}`}\n                alt={selectedImage.title}\n              \/>\n              &lt;p>\n                {selectedImage.title}\n              &lt;\/p>\n              &lt;button\n                className='close-button'\n                onClick={closePopover}\n              >\n                &amp;times;\n              &lt;\/button>\n            &lt;\/div>\n          &lt;\/div>\n        )\n      }\n    &lt;\/div>\n  );\n}\n\nexport default App;<\/code><\/pre>\n\n\n\n<p>Here&#8217;s what&#8217;s been added:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Added a new <strong><em>useState <\/em><\/strong>with an initial value of null for the selected image.<\/li>\n\n\n\n<li>Defined the two states of the popover using <strong><em>openPopover <\/em><\/strong>and <strong><em>closePopover<\/em><\/strong>.\n<ul class=\"wp-block-list\">\n<li><strong><em>openPopover<\/em><\/strong> sets the selected image to whatever image the user clicked<\/li>\n\n\n\n<li><strong><em>closePopover <\/em><\/strong>resets the selected image to null, removing any chance the app will become confused by what image is actually selected.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Inside the <strong>App <\/strong>component function, we updated the <strong>&lt;ImageGallery><\/strong> element to pass along the <strong><em>onImageClick <\/em><\/strong>action. This action will then be sent down to the <strong>ImageCard <\/strong>component &#8211; the level where the event will actually be triggered by the user&#8217;s click action.<\/li>\n\n\n\n<li>Finally, when an image is clicked and the <strong><em>openPopover <\/em><\/strong>function is triggered, we build a new Popover that contains the full-size image, its title, and a close button.\n<ul class=\"wp-block-list\">\n<li>If the user clicks the close button OR clicks outside the Popover window, the <strong><em>closePopover <\/em><\/strong>function will be triggered and the Popover will disappear and the <strong><em>selectedImage <\/em><\/strong>variable will be reset to null.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Let&#8217;s quickly update the <strong>ImageGallery <\/strong>component to allow it to pass along the <strong><em>onImageClick <\/em><\/strong>action to the <strong>ImageCard <\/strong>component.<\/p>\n\n\n\n<p><strong>ImageGallery.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import ImageCard from '.\/ImageCard';\n\nconst ImageGallery = ({ images, toggleFilter, onImageClick }) => {\n  return (\n    &lt;div className=\"gallery\">\n      {images.map((image, i) => (\n        &lt;ImageCard\n          key={i}\n          image={image}\n          toggleFilter={toggleFilter}\n          onImageClick={toggleFilter}\n        \/>\n      ))}\n    &lt;\/div>\n  );\n}\n\nexport default ImageGallery;<\/code><\/pre>\n\n\n\n<p><strong>ImageGallery<\/strong> now accepts the <strong><em>onImageClick <\/em><\/strong>parameter and simply passes it along to the <strong>&lt;ImageCard><\/strong> element.<\/p>\n\n\n\n<p>Next, let&#8217;s update the <strong>ImageCard <\/strong>component to trigger the <em><strong>openPopover <\/strong><\/em>function when it is clicked.<\/p>\n\n\n\n<p><strong>ImageCard.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const ImageCard = ({ image, toggleFilter, onImageClick }) => {\n  return (\n    &lt;div className=\"image-card\">\n      &lt;img\n        src={`${process.env.PUBLIC_URL}${image.src}`}\n        alt={image.title}\n        onClick=(() => onImageClick(image))\n      \/>\n      &lt;p>&lt;strong>{image.title}&lt;\/strong>&lt;\/p>\n      {\/* categories *\/}\n    &lt;\/div>\n  );\n}\n\nexport default ImageCard;<\/code><\/pre>\n\n\n\n<p>This component now accepts the <strong><em>onImageClick <\/em><\/strong>parameter and triggers the function when the image is clicked, passing the matching <em><strong>image <\/strong><\/em>data back up to the <strong>App <\/strong>component.<\/p>\n\n\n\n<p>Finally, let&#8217;s style the new <strong><em>Popover <\/em><\/strong>in our<strong> App.css<\/strong> file.<\/p>\n\n\n\n<p><strong>App.css<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/* ...Previously defined styles... *\/\n\n.popover {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-color: rgba(0, 0, 0, .7);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 1000;\n  opacity: 0;\n  visibility: hidden;\n  transition: opacity .3s ease, visibility 0s .3s;\n}\n\n.popover.show {\n  opacity: 1;\n  visibility: visible;\n  transition: opacity .3s ease, visibility 0s 0s;\n}\n\n.popover-content {\n  background-color: rgba(0, 0, 0, .7);\n  padding: 25px 50px;\n  border-radius: 8px;\n  position: relative;\n}\n\n.popover-content img {\n  max-width: 100%;\n  max-height: 80vh;\n  object-fit: contain;\n  box-shadow: -5px 5px 10px rgba(0, 0, 0, .7);\n}\n\n.close-button {\n  position: absolute;\n  font-size: 1.25em;\n  font-weight: bold;\n  top: 10px;\n  right: 10px;\n  background-color: #f96e46;\n  color: white;\n  border: none;\n  border-radius: 50%;\n  padding: .25em .5em;\n  cursor: pointer;\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Shuffle &amp; Deal &#8216;Em Out<\/h2>\n\n\n\n<p>For a bit of fun randomness, let&#8217;s add a shuffle function to keep the app interesting on repeat visits. This bit is only added to the <strong>App <\/strong>component and, honestly, is completely optional. But, it is a good way to get introduced to React&#8217;s <strong><em>useEffect <\/em><\/strong>method.<\/p>\n\n\n\n<p><strong>App.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { useEffect, useState } from 'react';\n\/\/ Other imports\n\nconst shuffleArray = (array) => {\n  const shuffledArray = &#91;...array];\n  for (let i = shuffledArray.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    &#91;shuffledArray&#91;i], shuffledArray&#91;j]] = &#91;shuffledArray&#91;j], shuffledArray&#91;i]];\n  }\n  return shuffledArray;\n}\n\nfunction App() {\n  \/\/ Previously defined filters, images, selectedImage\n  \n  useEffect(() =>{\n    const shuffledData = shuffleArray(data);\n    setImages(shuffledData);\n  }, &#91;]);\n\n  \/\/ Previously defined toggleFilter, uniqueCategories,\n  \/\/ filteredImages, openPopover, and closePopover\n\n  return ( ... );\n}\nexport default App;<\/code><\/pre>\n\n\n\n<p>What changed?<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We added a general function named <strong><em>shuffleArray <\/em><\/strong>that takes in an array of stuff, shuffles it using a randomizing <strong><em>for <\/em><\/strong>loop\n<ul class=\"wp-block-list\">\n<li>This loop contains a <strong><em>Math.random()<\/em><\/strong> method that takes the current placement of an array item and assigns it to a different, randomly chosen placement that hasn&#8217;t already been occupied.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>We then removed <strong><em>const images = data<\/em><\/strong> and are now redefining the <strong><em>images <\/em><\/strong>variable through the <strong><em>shuffleArray <\/em><\/strong>function.<\/li>\n\n\n\n<li>Nothing else needs to change in the other variables, functions, or the returned HTML.<\/li>\n<\/ul>\n\n\n\n<p>That&#8217;s all there is to the shuffle functionality! Now, the more images you add to your gallery, the more unique each subsequent visit will be!<\/p>\n\n\n\n<p>In the next part, we&#8217;ll wrap up this project by reviewing what we&#8217;ve built and prepare the project to launch in a live environment.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to add functionality to view images in full-screen popup windows. Also, add a shuffle function for some extra fun flair.<\/p>\n","protected":false},"author":1,"featured_media":34,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[9],"tags":[],"class_list":["post-47","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-art-gallery"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts\/47","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/comments?post=47"}],"version-history":[{"count":2,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts\/47\/revisions"}],"predecessor-version":[{"id":68,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts\/47\/revisions\/68"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/media\/34"}],"wp:attachment":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/media?parent=47"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/categories?post=47"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/tags?post=47"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}