DEV Community

Cover image for How to Build an Event Gallery in React With Cloudinary, AWS Rekognition, and Firebase
Pato for Cloudinary

Posted on • Edited on • Originally published at cloudinary.com

How to Build an Event Gallery in React With Cloudinary, AWS Rekognition, and Firebase

Live demo: https://eventographyapp.com/

How to Build an Event Gallery in React With Cloudinary, AWS Rekognition, and Firebase

Image optimization paired with Firebase’s realtime database + hosting creates a smooth UX: faster loads, less bandwidth, and happier users. In this tutorial we’ll build Eventography — a simple event gallery where guests upload their best shots. Cloudinary stores and optimizes images, AWS Rekognition (via Cloudinary’s auto‑moderation) keeps things safe, and Firebase handles auth, Firestore, and Hosting.

What you’ll build

  • Google SSO login
  • An Event page to upload photos via the Cloudinary Upload Widget
  • Auto‑moderation with AWS Rekognition (rejects unsafe images)
  • A Gallery page with optimized images (thumbnails + full‑size via Cloudinary React SDK)
  • A Profile page to edit event title/hashtag and share a QR link

Prereqs

  • React 18 (Vite) and Node.js ≥ 20
  • Free accounts for Firebase and Cloudinary

1) Firebase Setup (Auth, Firestore, Hosting)

  1. Create a Firebase project (Analytics optional). Firebase Create a Firebase project Firebase Create project
  2. Authentication → Get started → Sign-in method → Google → Enable.
  3. Firestore → Create database → Test mode (switch to production rules later).
  4. Hosting → Get started and install CLI: Firebase hosting

hosting conf

   npm i -g firebase-tools
Enter fullscreen mode Exit fullscreen mode

Firebase

  1. Initialize in your project root:
   firebase login
Enter fullscreen mode Exit fullscreen mode
   firebase init
   # choose: Hosting, Firestore, (and optionally Emulators)
Enter fullscreen mode Exit fullscreen mode

Firebase init

Deploy (later): firebase deploy.

Firebase deploy

Firestore security rules (tighten for prod)

Start in test mode for development, but move to restrictive rules. Example:

# firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /events/{eventId} {
      allow read: if true;              // or auth != null
      allow write: if request.auth != null && request.auth.uid == eventId;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Deploy rules:

firebase deploy --only firestore:rules
Enter fullscreen mode Exit fullscreen mode

2) Cloudinary Setup (Upload Preset + Moderation)

  1. Create a Cloudinary account.
  2. Settings → Product Environments and confirm your Cloud name.
  3. Settings → Upload → Add upload preset:
  • Signing mode: Unsigned (easy to start; switch to Signed in production)
  • Enable Use filename or externally defined Public ID and Unique filename
  • Upload control → Auto moderation: AWS Rekognition (default confidence 0.5 is fine)

⚠️ Security note: Unsigned presets are convenient but can be abused if leaked. For production, prefer signed uploads from your own backend and verify Firebase auth on server. Keep preset name private.


3) Project Scaffolding & Packages

Create your Vite app if you haven’t:

npm create vite@latest eventography -- --template react
cd eventography
npm i firebase @cloudinary/react @cloudinary/url-gen
Enter fullscreen mode Exit fullscreen mode

Add an .env file (Vite uses import.meta.env):

# .env
VITE_CLOUD_NAME=your_cloud_name
Enter fullscreen mode Exit fullscreen mode

4) Firebase Client Config

Create src/helpers/firebase.js. Find the full code in the repo:

const app = initializeApp(firebaseConfig);
export const storage = getStorage(app);
export const db = getFirestore(app);
export const auth = getAuth(app);

export const getEventData = async (eventId) => {
  const docRef = doc(db, "events", eventId);
  const docSnap = await getDoc(docRef);
  return docSnap.exists() ? docSnap.data() : null;
};

export const updateEventData = async (eventId, data) => {
  try {
    await setDoc(doc(collection(db, "events"), eventId), data);
  } catch (e) {
    console.error("Error adding document: ", e);
  }
};
Enter fullscreen mode Exit fullscreen mode

This code initializes Firebase services for your app and provides helper functions to read and update event data in Firestore. It first creates a Firebase app using firebaseConfig, then exports instances of Storage, Firestore, and Authentication so the rest of the app can use them. The getEventData function retrieves a specific event document by ID and returns its data if it exists. The updateEventData function writes or overwrites an event document in the events collection using the given ID and data, logging an error if the write fails.

Replace firebaseConfig with the values from Project settings → Your apps → Web in the Firebase console. Make sure firebase is installed (npm i firebase).


5) Auth Context (Google SSO)

Create src/AuthContext.jsx. You can find the full code of this file in the repo.

Let's explain the main parts of this file:

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [docSnap, setDocSnap] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      if (user) {
        try {
          const eventData = await getEventData(user.uid);
          setDocSnap(eventData);
        } catch (error) {
          console.error("Error fetching data:", error);
        }
      }
    };

    if (!user) {
      const unsubscribe = auth.onAuthStateChanged((u) => setUser(u));
      return () => unsubscribe();
    } else {
      fetchData();
    }
  }, [user]);

  const signInWithGoogle = async () => {
    try {
      const result = await signInWithPopup(auth, provider);
      setUser(result.user);
      window.location.href = "/profile";
    } catch (error) {
      console.error(error);
    }
  };

  const handleLogout = async () => {
    try {
      await auth.signOut();
      setUser(null);
      window.location.href = "/";
    } catch (error) {
      console.error(error);
    }
  };
Enter fullscreen mode Exit fullscreen mode

This component provides authentication state and helper methods to the rest of the app. It tracks two pieces of state: the authenticated user and that user’s associated Firestore document (docSnap). Inside a useEffect, it listens for Firebase Auth state changes when a user logs in or out, Firebase triggers a callback and updates user. Once a user is logged in, the effect fetches that user’s event data from Firestore using their UID and stores it in docSnap. The provider also exposes two actions: signInWithGoogle, which signs the user in via a Google popup and redirects them to /profile, and handleLogout, which signs them out, clears the user state, and redirects them to the home page.

6) Cloudinary Upload Widget Component

Let's build the Cloudinary Upload Widget component by creating a new file src/CloudinaryUploadWidget.jsx. The full code for the component can be found here.

Now, let's take a look at the main part of this file:

  useEffect(() => {
    if (!loaded) {
      const uwScript = document.getElementById("uw");
      if (!uwScript) {
        const script = document.createElement("script");
        script.setAttribute("async", "");
        script.setAttribute("id", "uw");
        script.src = "https://upload-widget.cloudinary.com/global/all.js";
        script.addEventListener("load", () => setLoaded(true));
        document.body.appendChild(script);
      } else {
        setLoaded(true);
      }
    }
  }, [loaded]);
Enter fullscreen mode Exit fullscreen mode

This useEffect ensures the Cloudinary Upload Widget script loads only once. If it’s not already loaded, the code checks for a script with ID "uw"—if missing, it creates the script, sets its source URL, waits for it to finish loading, and then marks loaded as true. If the script already exists, it simply updates loaded. The effect runs whenever loaded changes.

  useEffect(() => {
    const updateEventImages = async () => {
      try {
        const updatedData = {
          ...docSnap,
          images: docSnap?.images ? [...docSnap.images, ...images] : [...images],
          thumbnails: docSnap?.thumbnails ? [...docSnap.thumbnails, ...thumbnails] : [...thumbnails],
        };
        await updateEventData(getEventIdFromUrl(window.location.pathname), updatedData);
      } catch (error) {
        console.error("Error updating document: ", error);
      }
    };

    if (docSnap && images.length > 0 && thumbnails.length > 0) {
      updateEventImages();
    }
  }, [docSnap, images, thumbnails]);
Enter fullscreen mode Exit fullscreen mode

This useEffect automatically saves new uploaded images to Firestore whenever the event data (docSnap), full-size images, or thumbnails change. When all required data is present, it builds an updated event object by merging the existing Firestore document with the newly uploaded images and thumbnails. It then writes this updated object back to Firestore using updateEventData, using the event ID extracted from the page URL. If anything fails during the update, the error is logged. The effect re-runs anytime docSnap, images, or thumbnails change.

  const initializeCloudinaryWidget = async () => {
    setUploadProgress(null);
    if (loaded) {
      try {
        await window.cloudinary.openUploadWidget(uwConfig, processUploads);
      } catch (error) {
        setUploadProgress("failed");
      }
    }
  };
Enter fullscreen mode Exit fullscreen mode

This function resets the upload progress and, if the Cloudinary widget script is loaded, tries to open the upload widget with the given config and callback. If the widget fails to open, it marks the upload as "failed".

  const processUploads = useCallback((error, result) => {
    if (result?.event === "queues-end") {
      result.info.files.forEach((img) => {
        if (
          img.status !== "success" ||
          img.uploadInfo.moderation?.[0]?.status !== "approved" ||
          error !== undefined
        ) {
          setUploadProgress("failed");
        } else {
          setImages((prev) => [...prev, img.uploadInfo.url]);
          setThumbnails((prev) => [...prev, img.uploadInfo.thumbnail_url]);
          setUploadProgress("successful");
        }
      });
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

This callback runs when the Cloudinary widget finishes all uploads. It checks each file: if there's an error, a failed status, or the moderation isn't approved, it marks the upload as "failed". Otherwise, it stores the image and thumbnail URLs and marks the upload as "successful". Wrapped in useCallback, it stays stable across renders.


7) Event Page (Upload)

Create src/Event.jsx. You can find the full code to this file in our repo.

  const uwConfig = {
    cloudName: import.meta.env.VITE_CLOUD_NAME || "eventography",
    uploadPreset: "eventography", // set to your preset name
    sources: ["local"],
    multiple: true,
    folder: `${window.location.pathname}`,
    thumbnailTransformation: { width: 500, height: 500, crop: "fill" }
Enter fullscreen mode Exit fullscreen mode

This object defines the configuration for the Cloudinary Upload Widget. It sets the cloudName using an environment variable (or falls back to "eventography"), and specifies the uploadPreset that controls upload rules on Cloudinary. The widget accepts uploads only from the local device (sources: ["local"]) and allows multiple files at once. Uploaded images are stored in a Cloudinary folder matching the current URL path. Finally, thumbnailTransformation tells Cloudinary to generate a 500×500 cropped thumbnail for each upload.

const [docSnap, setDocSnap] = useState();
  const urlPath = window.location.pathname;

  useEffect(() => {
    const fetchData = async () => {
      const eventData = await getEventData(getEventIdFromUrl(urlPath));
      setDocSnap(eventData);
    };
    fetchData();
  }, [urlPath]);
Enter fullscreen mode Exit fullscreen mode

This code loads event data from Firestore based on the current URL. It stores the event document in docSnap state and reads the page’s path (window.location.pathname) to determine which event to fetch. Inside a useEffect, it calls getEventData with the event ID extracted from the URL, then saves the returned data into state. The effect runs whenever the URL path changes, ensuring the app always displays the correct event data.

The Event page passes your Upload Widget config to CloudinaryUploadWidget and renders event metadata from Firestore.


8) Gallery Page (Thumbnails + Cloudinary React SDK)

Time to display the images from our event in the gallery page. Create src/Gallery.jsx and copy paste the full code found here.

  const cld = new Cloudinary({
    cloud: { cloudName: import.meta.env.VITE_CLOUD_NAME }
  });
Enter fullscreen mode Exit fullscreen mode

In this code, we are creating a new instance and initializing Cloudinary using the cloudName from our enviroment variables.

  useEffect(() => {
    const fetchData = async () => {
      try {
        const snap = await getEventData(eventId);
        setDocSnap(snap);
        setLoadingStates(new Array(snap?.thumbnails?.length || 0).fill(true));
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };
    fetchData();
  }, [eventId]);
Enter fullscreen mode Exit fullscreen mode

This useEffect retrieves event data whenever the eventId changes. It calls getEventData, saves the returned document to docSnap, and then initializes a loadingStates array sized to the number of thumbnails in the document—each entry starts as true to indicate that every thumbnail is still loading. If fetching fails, it logs the error.

  const handleThumbnailClick = (imgUrl) => {
    const imageName = imgUrl.substring(imgUrl.lastIndexOf("/") + 1);
    const urlBuilder = `events/${eventId}/${imageName}`;
    setShowFullImage(true);
    setSelectedImage(urlBuilder);
  };
Enter fullscreen mode Exit fullscreen mode

This function runs when a thumbnail is clicked. It extracts the image’s file name from the URL, then builds the Cloudinary folder path for that image using the current eventId. After constructing the full image path, it updates state to display the full-size image setShowFullImage(true) and stores the selected image’s path setSelectedImage(urlBuilder) so it can be rendered.

  const handleImageLoad = (index) => {
    setLoadingStates((prev) => {
      const next = [...prev];
      next[index] = false;
      return next;
    });
  };
Enter fullscreen mode Exit fullscreen mode

This function updates the loading state for a specific thumbnail once it finishes loading. It takes the thumbnail’s index, creates a copy of the loadingStates array, sets that index to false to mark it as loaded, and returns the updated array. This allows the UI to show or hide loading indicators on a per-image basis.


9) Profile Page, QR Code, and Routing

To keep this post focused, grab the remaining components/styles from the public repo.

Firebase

Firebase

10) URL Helpers

Create src/helpers/urlHelpers.js:

/** Extract the eventId from app URLs like /events/<id> or /galleries/<id> */
export const getEventIdFromUrl = (urlPath) => {
  return urlPath
    .split("/")
    .filter((segment) => segment !== "")[1];
};
Enter fullscreen mode Exit fullscreen mode

11) Test Drive

  1. Log in (Google).
  2. In Profile, set your event title + hashtag and save.
  3. Go to Event, click Upload Images, select photos.
  4. Approved uploads appear in Gallery as thumbnails; click for full image.
  5. Verify assets in Cloudinary: DAM → Media Library → Assets or by Folders (events/<eventId>/...).

Production Hardening Checklist ✅

  • [ ] Switch to signed uploads through your backend.
  • [ ] Hide preset names and API keys; never expose secrets in frontend.
  • [ ] Lock down Firestore rules (see earlier section).
  • [ ] Add rate‑limiting / abuse prevention for uploads.
  • [ ] Use source lists in Upload Widget (e.g., limit to local only) and a dedicated upload folder per event.
  • [ ] Enable transformations like q_auto,f_auto on all delivery URLs.
  • [ ] Configure CSP headers on Firebase Hosting.

Wrap‑up

We covered the Cloudinary Upload Widget, image optimization, auto‑moderation via AWS Rekognition, Firebase Auth, Firestore, and Hosting. With a few components and presets, you can ship a safe, fast, and shareable event gallery.

  • Follow Cloudinary on Twitter/X
  • Looking for inspiration around Cloudinary products? Visit our app gallery

If you ship this, drop your demo in the comments — would love to see it! 🚀

Top comments (1)

Collapse
 
jenlooper profile image
Jen Looper Cloudinary

Nice!