Image optimization paired with Firebase’s powerful database enhances user experience by accelerating loading times and reducing bandwidth costs. This synergy allows developers to deliver fast, visually engaging experiences, driving higher engagement and conversions.
Eventography is a centralized platform for guests to easily upload their best shots to a secure server powered by Cloudinary. Our content moderation feature, powered by Amazon Rekognition, ensures that looking through your event’s memorable photos will be a safe and enjoyable experience.
This tutorial requires some basic knowledge of React. I’m using Vite to generate my React v18 application and NodeJS v20.
- GitHub repository: Cloudinary Event Gallery
- Live demo: https://eventographyapp.com/
To begin, log in to your Firebase account or create a free account. Then, click Create a project to configure your Firebase project.
Give a name to your Firebase project. I named mine Eventography. Check the boxes and click Continue.
Enabling Google Analytics on your project is optional. I disabled it. Then click Create project.
Inside the Firebase console, on the left-hand panel, click Hosting.
Now, inside the hosting page, click Get Started.
Time to set Firebase CLI in your machine. Run the following command in your terminal:
npm install -g firebase-tools
Then click Next to initialize your Firebase project.
Initialize your Firebase project by logging into your Firebase account in your terminal. This is the same email you used to create your Firebase website.
In your terminal, run:
firebase login
Run the following command from your app’s root directory, then click Next.
firebase init
To deploy your app to Firebase Hosting, you can use the following command. We aren’t ready to deploy our app, but we will later on. Then click Continue to console.
firebase deploy
On the left navigation, click All products, then click Authentication.
On the Authentication page, click Get Started. Go to Sign-in method and select Google. You can add different sign-in methods, but for this app, we’ll only do Google.
Then you’ll enable Google, add a support email for your project, and finish the configuration by clicking Save.
On the left-hand panel, click All products, then Cloud Firestore.
On the Cloud Firestore page, click Create database.
Set the name and location of your project. I left everything as is. Then click Next.
Click Start in test mode (make sure to come back and set the production mode after 30 days and update your security settings), then Create.
To begin, log in to your Cloudinary account or create a free account. If prompted with the question, “What’s your main interest?”, select Coding with APIs and SDKs or Skip.
In your account, select Settings > Product Environments. Here you’ll see the cloud that you created. Let’s click the three-dot menu to edit your Cloudinary Cloud Name.
Edit the product environment and name your cloud, then click Save Changes.
It’s important to keep the same cloud name across different tools to be consistent.
Cloudinary Presets is one of my favorite Cloudinary features. Upload Presets allow you to centrally establish a collection of asset upload configurations rather than configuring them individually with each upload request. You can create multiple upload presets and employ distinct presets for various upload scenarios.
To create your first preset, go to Settings > Upload, then click Add Upload preset.
In your preset, make sure you’re using Unsigned as the signing mode. Lastly, you’ll turn on the option of Use filename or externally defined Public ID and Unique filename, then click Save.
Cloudinary also provides the Signed as the signing mode. This mode allows you to enhance security around your uploads.
AWS Rekognition is a machine learning technology that analyzes image and video, and while it has different features, the one we’re using with Cloudinary is the content moderation integration. This integration can help us detect potentially unsafe, inappropriate, or unwanted content.
On the left-hand panel, click Settings > Upload > Your newly created preset > Edit.
In your preset, click Upload Control, and in the Auto moderation section, selectAWS Rekognition. You can adjust the confidence level, but I’ll leave the default settings as 0.5. After configuring your preset, click Save.
In this section, we’ll configure the three client-side Firebase services (SSO, Hosting, Cloud Storage) in our application. In your React application within the SRC folder, create a folder called helpers and a file called firebase.js, and copy and paste the following code.
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getStorage } from "firebase/storage";
import {getAuth} from 'firebase/auth';
import {collection, getFirestore, setDoc} from 'firebase/firestore'
import { doc, getDoc } from "firebase/firestore";
const firebaseConfig = {
apiKey: "XXXXXXXXXXXX",
authDomain: "XXXXXXXXXXXX",
projectId: "XXXXXXXXXXXX",
storageBucket: "XXXXXXXXXXXX",
messagingSenderId: "XXXXXXXXXXXX",
appId: "XXXXXXXXXXXX",
measurementId: "XXXXXXXXXXXX"
};
export const getEventData = async (eventId) => {
const docRef = doc(db, "events", eventId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.data();
} else {
return null;
}
};
export const updateEventData = async (eventId, data) => {
try {
await setDoc(doc(collection(db, "events"), eventId), data);
} catch (e) {
console.error("Error adding document: ", e);
}
}
// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const storage = getStorage(app);
export const db = getFirestore(app);
export const auth = getAuth(app);
Code language: JavaScript (javascript)
As you can see I’m initializing the app and creating some functions that will be used in different parts of the application.
The getEventData(eventId)
function pulls the data from a given eventID that lives inside the events located in our Firestorage DB.
The updateEventData(eventId, data)
function updates an event in our Firestore DB using the eventId.
The last step of the Firebase configuration is to add your own Firebase configuration to the firebase.js file.
In your Firebase Console, click Project Overview on the left-hand panel, and select Web.
It’s now time to register your web application with Firebase. Enter the name of your application (remember to be consistent with your application name), then click Register app.
Make sure that your React app has the Firebase npm
package we previously installed. If it doesn’t have it, run npm install firebase
in the root folder of the project.
Replace the firebaseConfig object in the firebase.js file with the firebaseConfiguration provided by Firebase in your console.
In the React application, create a file named AuthContext.jsx in the src folder. This file will contain all the business logic related to your authentication workflow.
/* 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((user) => {
setUser(user);
});
return () => {
unsubscribe();
};
} else {
fetchData();
}
}, [user]);
const signInWithGoogle = async () => {
try {
const result = await signInWithPopup(auth, provider);
const user = result.user;
setUser(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);
Code language: JavaScript (javascript)
Cloudinary’s Upload widget is a complete, interactive user interface that enables your users to upload files from a variety of sources to your website or application.
In your src folder, create a new file called CloudinaryUploadWidget.jsx. This file contains the necessary code to upload your images to your Cloudinary Cloud as well as the code to run the Cloudinary Upload widget in your React app.
CloudinaryUploadWidget.jsx source file
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]);
/**
* This useEffect will be trigger every time the docSnap, images, or thumbnails chage
* and will update the data in our Firestore DB
*/
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(prevImages => [
...prevImages,
img.uploadInfo.url,
]);
setThumbnails(prevThumbnails => [
...prevThumbnails,
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;
Code language: JavaScript (javascript)
In the src
folder, create an Events.jsx file, and copy and paste the following code:
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: "eventography",
uploadPreset: "eventography",
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;
Code language: JavaScript (javascript)
In the code above, we’ll list the Cloudinary Upload Widget Configuration that we want to pass to our CloudinaryUploadWidget
component, and pull the title and the hashtag of the event from Firestore.
Now create an Event.css file. You can file the code of styles in the public repo.
In the src
folder, create a Gallery.jsx file. In this file, we’ll pull the data from Firestore, which has the URLs to our event images. In this file, I decided to take two different approaches to show you how we can render the images in our app:
- Using the URL as the
src
of ourimg
tag to display the thumbnails. The user will see the thumbnails that they can click to enlarge the image. - Using the Cloudinary React SDK. In this case, we’ll create a new instance of Cloudinary to generate a transformation URL and pass that object to the AdvanceImage component to render the image.
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 docSnap = await getEventData(eventId);
setDocSnap(docSnap);
setLoadingStates(new Array(docSnap?.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((prevStates) => {
const newStates = [...prevStates];
newStates[index] = false;
return newStates;
});
};
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;
Code language: JavaScript (javascript)
Now create a Gallery.css file in your src folder. You can find the code to style in the public repo.
Once you start uploading images to your gallery, the page will look like this:
In the src
folder, create a Profile.jsx file. In this file we won’t touch Cloudinary specifics. All we’ll do is pull the data from Firestore containing the name of our event and the event hashtag. If the user wants to change the information, they can easily edit it and click Save. The Save button will call the handleSave()
function, which updates the event’s information on Firestore. In this file, we’ll also add a nice touch to allow the user to copy the event link with one click and share a QR code generated by the QRCodeGenerator component.
Now create a Profile.jsx file in your src
folder. You can find the source code in the public repo.
Next, create a Profile.css file in your src
folder. You can find the styles in the public repo.
Your profile page should look like this:
This component generates a QR code based on a URL. It’s a nice touch to add to your app, so users can share their Eventography event with a QR code.
Now create a QRCodeGenerator.jsx file inside your src
folder. You can find the code in the public repo.
In your src/helper folder, create a new file called urlHelpers.js. In this file, add all the functions related to URL manipulations that can be used across your application. I created the getEventIdFromUrl() function, which takes the current URL of the browser or any URL you pass and grabs the last part of the URL. In this case, it grabs the eventId, which is the last part of the URL.
/**
* Function to extract the EventId from the Apps URL
* @param {*} urlPath
* @returns Event Id
*/
export const getEventIdFromUrl = (urlPath) => {
return urlPath
.split("/")
.filter((segment) => segment !== "")[1];
}
Code language: JavaScript (javascript)
- App.jsx. This is the component where the user lands when they first visit the app. You can find the source code here.
- App.css. This file has the styles of the App.jsx file. You can find the source code here.
- Navbar.jsx. This is the navigation component for your application. The state of the users drives this component behavior. Suppose it’s logged in or not. You can find the source code here.
- Navbar.css. This file has the styles of the Navbar.jsx file. You can find the source code here.
- Main.jsx. This file contains the routes to our application wrapped in the AuthContext, so the entire app has access to the AuthContext. You can find the source code here.
- Index.css. This file has the styles of the shell of the app. You can find the source code here.
- ENV. This file contains the cloud name environment variable. You can find the source code here.
Log in to your app, navigate to the Profile page, and add a title and a hashtag to your event. Next, go to the Event page, click Upload Images, and select the images you want to upload. Once the images are successfully uploaded, you can navigate to the Gallery Page to view them. Lastly, you can also view them within your Cloudinary Cloud.
To see the assets, click Digital Assets Management > Media Library > Assets
Below, you can see a screenshot of my Cloudinary Cloud with all the images I’ve uploaded:
To see the assets by event ID, click Digital Assets Management > Media Library > Folders.
In this image you can see I have multiple folders with unique names. The highlighted name is the same as your event ID. Each folder has the corresponding images to that event.
During the app development process, we gained insights into various features of Cloudinary, including the Cloudinary Upload Widget, image optimization, content moderation, Cloudinary cloud storage, and upload presets. With just a few lines of code, you can accomplish a multitude of tasks using Cloudinary. Explore our website to dive deeper into our products and services.
Live demo: https://eventographyapp.com/
To stay updated with the latest product features, follow Cloudinary on Twitter, explore other sample apps, and join our Discord server and Community forum.