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:

// src/helpers/firebase.js
import { initializeApp } from "firebase/app";
import { getStorage } from "firebase/storage";
import { getAuth } from "firebase/auth";
import { collection, getFirestore, setDoc, doc, getDoc } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "XXXXXXXXXXXX",
  authDomain: "XXXXXXXXXXXX",
  projectId: "XXXXXXXXXXXX",
  storageBucket: "XXXXXXXXXXXX",
  messagingSenderId: "XXXXXXXXXXXX",
  appId: "XXXXXXXXXXXX",
  measurementId: "XXXXXXXXXXXX"
};

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

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:

/* eslint-disable react-refresh/only-export-components */
import { createContext, useState, useContext, useEffect } from "react";
import { auth } from "./helpers/firebase";
import { getEventData } from "./helpers/firebase";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import PropTypes from "prop-types";

const provider = new GoogleAuthProvider();
const AuthContext = createContext();

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);
    }
  };

  return (
    <AuthContext.Provider value={{ user, docSnap, signInWithGoogle, handleLogout, setUser }}>
      {children}
    </AuthContext.Provider>
  );
};

AuthProvider.propTypes = { children: PropTypes.node.isRequired };
export const useAuth = () => useContext(AuthContext);
Enter fullscreen mode Exit fullscreen mode

6) Cloudinary Upload Widget Component

Create src/CloudinaryUploadWidget.jsx:

import { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import { updateEventData } from "./helpers/firebase";
import { getEventIdFromUrl } from "./helpers/urlHelpers";

function CloudinaryUploadWidget({ uwConfig, docSnap }) {
  const [loaded, setLoaded] = useState(false);
  const [images, setImages] = useState([]);
  const [thumbnails, setThumbnails] = useState([]);
  const [uploadProgress, setUploadProgress] = useState(null);

  // Load Cloudinary Upload Widget script
  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]);

  // Persist images to Firestore when new uploads arrive
  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]);

  const initializeCloudinaryWidget = async () => {
    setUploadProgress(null);
    if (loaded) {
      try {
        await window.cloudinary.openUploadWidget(uwConfig, processUploads);
      } catch (error) {
        setUploadProgress("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");
        }
      });
    }
  }, []);

  return (
    <>
      <button id="upload_widget" onClick={initializeCloudinaryWidget}>
        Upload Images
      </button>
      {uploadProgress && (
        <p>Image Upload Status: {uploadProgress === "successful" ? "successful" : "failed"}</p>
      )}
    </>
  );
}

CloudinaryUploadWidget.propTypes = {
  uwConfig: PropTypes.object.isRequired,
  docSnap: PropTypes.object,
};

export default CloudinaryUploadWidget;
Enter fullscreen mode Exit fullscreen mode

7) Event Page (Upload)

Create src/Event.jsx:

import "./Event.css";
import { useEffect, useState } from "react";
import CloudinaryUploadWidget from "./CloudinaryUploadWidget";
import { getEventIdFromUrl } from "./helpers/urlHelpers";
import { getEventData } from "./helpers/firebase";

function Event() {
  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" },
  };

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

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

  return (
    <>
      {docSnap && (
        <div className="event">
          <h2>{docSnap?.eventTitle}</h2>
          <h3>{docSnap?.eventHashtag}</h3>
          <CloudinaryUploadWidget uwConfig={uwConfig} docSnap={docSnap} />
          <button onClick={() => (window.location.href = `/galleries/${getEventIdFromUrl(urlPath)}`)}>
            View Pictures
          </button>
          <p className="footer">
            Created with 💜 by <a href="https://eventographyapp.com/">Eventography</a>
          </p>
        </div>
      )}
    </>
  );
}
export default Event;
Enter fullscreen mode Exit fullscreen mode

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


8) Gallery Page (Thumbnails + Cloudinary React SDK)

Create src/Gallery.jsx:

import { useEffect, useState } from "react";
import "./Gallery.css";
import { getEventIdFromUrl } from "./helpers/urlHelpers";
import { getEventData } from "./helpers/firebase";
import { AdvancedImage } from "@cloudinary/react";
import { Cloudinary } from "@cloudinary/url-gen";

const Gallery = () => {
  const [showFullImage, setShowFullImage] = useState(false);
  const [selectedImage, setSelectedImage] = useState(null);
  const [docSnap, setDocSnap] = useState(null);
  const [loadingStates, setLoadingStates] = useState([]);

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

  const eventId = getEventIdFromUrl(window.location.pathname);

  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]);

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

  const handleCloseFullImage = () => {
    setShowFullImage(false);
    setSelectedImage(null);
  };

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

  return (
    <div className="gallery">
      <button className="gallery-upload-btn" onClick={() => (window.location.href = `/events/${eventId}`)}>
        Upload Pics
      </button>

      <div className="gallery-container">
        {showFullImage && selectedImage && (
          <div className="full-image-container">
            <button className="close-btn" onClick={handleCloseFullImage}>Close</button>
            <AdvancedImage
              cldImg={cld.image(selectedImage).delivery("q_auto").format("auto")}
              className="full-image"
            />
          </div>
        )}

        {!showFullImage && docSnap?.thumbnails?.length > 0 ? (
          docSnap.thumbnails.map((imgUrl, index) => (
            <div className="image-item" key={index} onClick={() => handleThumbnailClick(imgUrl)}>
              <img src={imgUrl} alt={`Image ${index}`} onLoad={() => handleImageLoad(index)} />
              {loadingStates[index] && <p>Loading...</p>}
            </div>
          ))
        ) : (
          <p>Loading...</p>
        )}
      </div>
    </div>
  );
};

export default Gallery;
Enter fullscreen mode Exit fullscreen mode

9) Profile Page, QR Code, and Routing

  • Profile.jsx: reads/writes eventTitle and eventHashtag to Firestore; includes copy‑link and QR share (via a QRCodeGenerator component).
  • QRCodeGenerator.jsx: generate a QR from the event link.
  • Navbar.jsx, App.jsx, Main.jsx, and CSS files: wire up routes and basic styling.

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!