Configure the Frontend of Your Application
Now, we are going to configure the front-end of our application.
Install and Configure the Tailwind CSS Framework
Before, you start adding code, you need to ensure the Tailwind framework will allow you to implement pre-built CSS classes that can scan the HTML files and JS components, and generate the required static CSS files.
To install the required framework:
- Navigate to the PhotoStoreApp/photo-store-app/ directory and execute the following command
Next, execute the following command to complete the installation of the required Tailwind CSS framework.
- Next, you need to execute the following command to create a tailwind.config.js file in your PhotoStoreApp/photo-store-app/ directory.
A tailwind.config.js file will be created. The project directory should now look like the image shown below:
- Now, copy the code given below and paste it in the tailwind.config.js file.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}"
],
theme: {
extend: {},
},
plugins: [],
};
The tailwind.config.js file has been configured for your application.
Install Required Packages
To ensure all front-end dependencies are met, you need to install the following packages:
- axios: This package lets you handle HTTP requests.
- hamburger-react: This package lets you create a hamburger menu icon.
- react-toastify: This package lets you display toast notifications.
- react-router-dom: This package lets you handle client-side routing.
- react-icons: This package lets you handle popular icon libraries.
Navigate to the PhotoStoreApp/photo-store-app/ directory and execute the following command in your terminal to install the required packages.
Code Your Frontend
We are going to add code in the following files:
- In the PhotoStoreApp/photo-store-app/src/ directory:
- App.js: This file will contain the logic to render login components, all upload related operations, and share functionalities. The code also contains logic for responsive navigation bar, toast messages, and hamburger menu.
- Create the following folders, and their respective files:
- service
- ImageService.js: Contains helper functions which defines logic to handle the following functionalities:
- fetchImages(): Fetches the required image.
- fetchSharedImages(): Fetches the required shared image.
- handleDelete(): Contains the logic to delete the required image from the application and the bucket.
- handleDownload(): Contains the logic to download the required image from the bucket to the local system.
- handleShareAction(): Contains the logic to invoke the /shareDetails API to share the required image with another registered user.
- handleUpdateSharedDetails(): Contains the logic to make update share permissions and image details.
- ImageThumbnail.js: Contains the logic to render the uploaded images in thumbnail, full-scale, or list view.
- ImageService.js: Contains helper functions which defines logic to handle the following functionalities:
- pages
- Home.js: Contains the logic to render the grid/list view for images. Permits CRUD operations on the images, and supports pagination view to display all the images uploaded to the bucket.
- ImageGrid.js: Contains the logic render the a grid of all the uploaded images with options to download, preview, share, and delete.
- ImageList.js: Contains the logic to render a list view of all the uploaded images with options to download, preview, share, and delete.
- Login.js: Contains the logic to direct end-users to the Catalyst Authentication login components.
- Logout.js: Contains the logic to sign out end-users from the application.
- SharedDetails.js: Contains the logic to display the images that are shared with other users in a paginated manner. It also defines the logic to revoke and update access permissions for an image when required.
- SharedImageGrid.js: Contains the logic to display the images in a responsive grid with options to download the required image provided the required permissions are granted.
- SharedImageList.js: Contains the same logic as SharedImageGrid.js except the images are displayed in list view with images rendered as thumbnails.
- SharedImages.js: Contains the logic to display shared images either in a grid view or list view through pagination. The logic of this code file also allows registered and permitted end-users to perform CRUD operations on the shared images.
- Upload.js: Contains the logic to upload an image. The logic in this code is defined to check if another image exists to prevent duplicates. While uploading the image call is made to the /convertToThumbnailAndUpload API to convert the image to a thumbnail, and complete the upload.
- service
- index.js: Will act as the entry point for the application.
- index.css: Required to use tailwind for our application.
- In the PhotoStoreApp/photo-store-app/public/ directory:
- index.html: This file is the main HTML template for the entire application.
Once all the files have been created for your application, your project directory will look like this:
Let’s begin adding code to your files.
Copy the code below and paste it into the respective files of your project using an IDE and save the files.
import { useEffect, useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom";
import Upload from "./pages/Upload";
import "./App.css";
import Login from "./pages/Login";
import Logout from "./pages/Logout";
import { ToastContainer } from "react-toastify";
import { Turn as Hamburger } from "hamburger-react";
import SharedDetails from "./pages/SharedDetails";
import Home from "./pages/Home";
import SharedImages from "./pages/SharedImages";
function HeaderClick() {
const navigate = useNavigate();
return (
<h1
className="text-2xl font-bold px-4 cursor-pointer hover:text-gray-300 transition duration-300"
onClick={() => navigate("/")}>
Photo Store App
</h1>
);
}
function App() {
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
const [isFetching, setIsFetching] = useState(true);
const [userId, setUserId] = useState(null);
const [isOpen, setOpen] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const authenticateUser = async () => {
try {
const result = await window.catalyst.auth.isUserAuthenticated();
setUserId(result.content.zuid);
setIsUserAuthenticated(true);
} catch (err) {
console.log("UNAUTHENTICATED");
} finally {
setIsFetching(false);
}
};
authenticateUser();
}, []);
return (
<div className="bg-black text-white min-h-screen flex flex-col overflow-hidden">
<nav className="flex justify-between items-center px-6 py-4 shadow-lg relative">
<div className="flex items-center relative">
<Hamburger toggled={isOpen} toggle={setOpen} />
{isOpen && (
<div className="absolute left-0 top-full mt-2 bg-gray-800 text-white w-64 h-screen shadow-lg p-4 z-50 overflow-y-auto flex flex-col justify-between text-lg">
<ul className="flex flex-col space-y-4">
<li
className="py-2 px-4 cursor-pointer hover:bg-blue-700"
onClick={() => {
setOpen(false);
navigate("/upload");
}}
>
Upload Image
</li>
<li
className="py-2 px-4 cursor-pointer hover:bg-blue-700"
onClick={() => {
setOpen(false);
navigate("/");
}}
>
Your Gallery
</li>
<li
className="py-2 px-4 cursor-pointer hover:bg-blue-700"
onClick={() => {
setOpen(false);
navigate("/sharedImages");
}}
>
Shared Gallery
</li>
<li
className="py-2 px-4 cursor-pointer hover:bg-blue-700"
onClick={() => {
setOpen(false);
navigate("/sharedDetails");
}}
>
Manage Shared Details
</li>
</ul>
<ul className="pb-16 mt-auto">
<li
className="py-2 px-4 cursor-pointer hover:bg-red-700"
onClick={() => {
setOpen(false);
navigate("/logout");
}}
>
LogOut
</li>
</ul>
</div>
)}
<HeaderClick />
</div>
<ul className="flex space-x-32">
<li>
<button
onClick={() => navigate("/upload")}
className="bg-blue-800 text-white px-4 py-2 rounded-md hover:bg-blue-900 transition duration-300"
>
Upload
</button>
</li>
</ul>
</nav>
<div className="flex-grow flex justify-center items-center overflow-hidden">
{isFetching ? (
<div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
) : isUserAuthenticated ? (
<Routes>
<Route path="/upload" element=<Upload userId={userId} /> />
<Route path="/sharedDetails" element=<SharedDetails userId={userId} /> />
<Route path="/logout" element=<Logout /> />
<Route path="/" element=<Home userId={userId} /> />
<Route path="/sharedImages" element=<SharedImages userId={userId} /> />
<Route path="*" element=<Home userId={userId} /> />
</Routes>
) : (
<Login />
)}
</div>
<ToastContainer />
</div>
);
}
export default App;
import { toast } from 'react-toastify';
export const fetchImages = async (userId, setImageDetails, setLoading) => {
try {
const response = await fetch(`/fetchAllImages?id=${userId}`);
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const data = await response.json();
data.map((image) => {
image.object_url = image.object_url + "?responseCacheControl=max-age=3600";
});
setTimeout(() => {
setLoading(false);
}, 1000);
setImageDetails(data || []);
} catch (err) {
console.error(err);
toast.error("Error Occurred", {
theme: "colored",
});
}
};
export const fetchUsers = async (setUsers) => {
try {
const response = await fetch("/getAllUsers");
const data = await response.json();
setUsers(data);
} catch (error) {
console.error("Error fetching users:", error);
toast.error("Error Occurred", {
theme: "colored",
});
}
};
export const handleDelete = async (imageKey, setImageDetails, setOpenMenuIndex) => {
try {
const stratus = window.catalyst.stratus;
const bucket = stratus.bucket("YOUR_BUCKET_NAME");
await bucket.deleteObject(imageKey);
const pathParts = imageKey.split("/");
const fileName = pathParts.pop();
const folderName = pathParts.pop();
const newFileName = fileName.replace(/\.[^/.]+$/, ".jpeg");
const thumbnailPath = ["photos", "thumbnails", folderName, newFileName].join("/");
await bucket.deleteObject(thumbnailPath);
let zcql = window.catalyst.ZCatalystQL;
let query = `DELETE FROM ImageShareDetails WHERE BucketPath = '${imageKey}'`;
console.log("QUERY: " + query);
await zcql.executeQuery(query)
.then((response) => {
console.log("ZCQL Response: " + JSON.stringify(response.content));
})
.catch((err) => {
console.log("ZCQL Error: " + err);
});
setImageDetails(prev => prev.filter(image => image.key !== imageKey));
if (setOpenMenuIndex != null) {
setOpenMenuIndex(null);
}
toast.success("Image Deleted Successfully", {
theme: "colored",
});
} catch (error) {
console.error("Error deleting image:", error);
toast.error("Error deleting the file. Please try again.", {
theme: "colored",
});
}
};
export const handleDownload = async (imageKey, setOpenMenuIndex) => {
try {
const stratus = window.catalyst.stratus;
const bucket = stratus.bucket("YOUR_BUCKET_NAME");
const getObject = await bucket.getObject(imageKey);
const getObjectStart = getObject.start();
if (setOpenMenuIndex != null) {
setOpenMenuIndex(null);
}
await getObjectStart.then((blob) => {
const url = URL.createObjectURL(blob.content);
const a = document.createElement("a");
a.href = url;
a.download = imageKey;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}).catch((error) => {
console.error("Error downloading file:", error);
});
setTimeout(() => {
toast.success("File Downloaded Successfully", {
theme: "colored",
});
}, 2000);
} catch (error) {
console.error("Error downloading image:", error);
toast.error("Error in downloading the file", {
theme: "colored",
});
}
};
export const handleShareAction = async (name, id, path, action, userId) => {
try {
const response = await fetch(`/shareDetails`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userName: name, imagePath: path, zuid: id, isUpdate: action, sharedBy: userId }),
});
const data = await response.json();
if (data.message === "Access Provided") {
toast.success("Image Shared Successfully", {
theme: "colored",
});
} else {
toast.error(data.message, {
theme: "colored",
});
}
console.log("API Response:", data);
} catch (error) {
console.error("Error calling API:", error);
}
};
export const fetchSharedImages = async (userId, setImageDetails, setLoading) => {
try {
const response = await fetch(`/getSharedImages?id=${userId}`);
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const data = await response.json();
const flattenedImages = data.flatMap(item => item || []);
setTimeout(() => {
setLoading(false);
}, 1000);
setImageDetails(flattenedImages || []);
} catch (err) {
console.error(err);
toast.error("Error Occurred", {
theme: "colored",
});
}
};
export const fetchSharedDetails = async (userId, setDetails, setLoading) => {
try {
const response = await fetch(/getSharedDetails?id=${userId}
);
const data = await response.json();
setDetails(data);
setLoading(false);
} catch (error) {
console.error(“Error fetching details:”, error);
setLoading(false);
}
};
export const handleUpdateSharedDetails = async (updatedItem, navigate) => {
try {
const response = await fetch("/updateSharedDetails", {
method: “PATCH”,
headers: { “Content-Type”: “application/json” },
body: JSON.stringify(updatedItem),
});
await response.json();
toast.success(“Details Updated Successfully”, { theme: “colored” });
navigate(0);
} catch (error) {
console.error(“Error updating details:”, error);
toast.error(“Error updating details”, { theme: “colored” });
}
};
import { useState, useEffect } from "react";
export default function ImageThumbnail({ imageUrl, listStyle }) {
console.log("Image URL: " + imageUrl);
const [thumbnail, setThumbnail] = useState(null);
const bucketUrl = "YOUR_BUCKET_URL";
const pathParts = imageUrl.split("/");
const fileName = pathParts.pop();
const folderName = pathParts.pop();
const newFileName = fileName.replace(/\.[^/.]+$/, ".jpeg");
const newFilePath = ["photos", "thumbnails", folderName, newFileName].join("/");
console.log(newFilePath);
const thumbnailUrl = bucketUrl + newFilePath;
console.log("ThumbNail URL: " + thumbnailUrl);
useEffect(() => {
setThumbnail(newFilePath);
}, [imageUrl]);
return (
<>
{thumbnail ? (
listStyle ? (
<img
src={thumbnailUrl}
alt="Thumbnail"
style={{ width: "1rem", height: "1rem" }}
/>
) : (
<img
src={thumbnailUrl}
alt="Thumbnail image"
className="w-40 h-40 object-cover rounded-lg transition-all duration-500 ease-in-out"
/>
)
) : (
<p>Loading...</p>
)}
</>
);
}
import { useState, useEffect } from 'react';
import { FiX, FiList, FiGrid } from "react-icons/fi";
import { fetchImages, handleDelete, handleDownload, fetchUsers, handleShareAction } from "../service/ImageService";
import ImageGrid from './ImageGrid';
import ImageList from './ImageList';
export default function Home({ userId }) {
const [imageDetails, setImageDetails] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [openMenuIndex, setOpenMenuIndex] = useState(null);
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
const [selectedUserIndex, setSelectedUserIndex] = useState(null);
const [openShareIndex, setOpenShareIndex] = useState(null);
const [selectedImage, setSelectedImage] = useState(null);
const [viewMode, setViewMode] = useState("grid");
const imagesPerPage = 9;
useEffect(() => {
fetchImages(userId, setImageDetails, setLoading);
}, [userId]);
useEffect(() => {
const handleClickOutside = (event) => {
if (openShareIndex !== null || selectedUserIndex !== null) {
setOpenShareIndex(null);
setSelectedUserIndex(null);
}
if (openMenuIndex !== null) {
setOpenMenuIndex(null);
}
};
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, [openShareIndex, selectedUserIndex, openMenuIndex]);
const handleShareClick = async (index, e) => {
e.preventDefault();
e.stopPropagation();
if (openShareIndex === index) {
setOpenShareIndex(null);
setSelectedUserIndex(null);
return;
}
await fetchUsers(setUsers);
setOpenShareIndex(index);
};
const toggleUserOptions = (index, e) => {
e.preventDefault();
e.stopPropagation();
setSelectedUserIndex(selectedUserIndex === index ? null : index);
};
const openPreview = (imageUrl) => setSelectedImage(imageUrl);
const closePreview = () => setSelectedImage(null);
const indexOfLastImage = currentPage * imagesPerPage;
const indexOfFirstImage = indexOfLastImage - imagesPerPage;
const currentImages = imageDetails.slice(indexOfFirstImage, indexOfLastImage);
const nextPage = () => {
if (indexOfLastImage < imageDetails.length) setCurrentPage(currentPage + 1);
};
const prevPage = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};
const toggleMenu = (index, e) => {
e.preventDefault();
e.stopPropagation();
setOpenMenuIndex(openMenuIndex === index ? null : index);
};
return (
{loading ? (
) : (
<>
{imageDetails.length === 0 ? (
No files found. Click the Upload button to upload your photos.
) : (
<>
{viewMode === "grid" ? (
) : (
)}
{imageDetails.length > imagesPerPage && (
Page {currentPage}
)}
>
)}
>
)}
{selectedImage && (
)}
);
}
import { FiShare2, FiChevronRight, FiMoreVertical } from "react-icons/fi";
import ImageThumbnail from "../service/ImageThumbnail";
export default function ImageGrid({
currentImages,
openShareIndex,
handleShareClick,
users,
selectedUserIndex,
toggleUserOptions,
handleShareAction,
openPreview,
openMenuIndex,
toggleMenu,
handleDelete,
handleDownload,
setImageDetails,
setOpenMenuIndex,
setOpenShareIndex,
setSelectedUserIndex,
userId
}) {
return (
{currentImages.map((image, index) => (
openPreview(image.object_url)}
>
{/* Share Button */}
{/* Share Dropdown */}
{openShareIndex === index && users.length > 0 && (
{users.map((user, idx) => (
{selectedUserIndex === idx && (
)}
))}
)}
{/* Image Thumbnail */}
{/* Options Menu Button */}
{/* Options Menu Dropdown */}
{openMenuIndex === index && (
)}
{/* Image Name */}
{image.key.split("/").pop()}
))}
);
};
import ImageThumbnail from '../service/ImageThumbnail';
import { FiDownload, FiTrash2, FiEye, FiChevronRight, FiShare2 } from "react-icons/fi";
export default function ImageList({
currentImages,
openShareIndex,
handleShareClick,
users,
selectedUserIndex,
toggleUserOptions,
handleShareAction,
handleDelete,
handleDownload,
setImageDetails,
openPreview,
setOpenShareIndex,
setSelectedUserIndex,
userId
}) {
return (
{currentImages.map((image, index) => (
{image.key.split('/').pop()}
{openShareIndex === index && users.length > 0 && (
{users.map((user, idx) => (
{selectedUserIndex === idx && (
)}
))}
)}
))}
);
}
import { useEffect } from "react";
function Login() {
useEffect(() => {
window.location.href = `${window.origin}/__catalyst/auth/login`;
}, []);
return null;
}
export default Login;
import { useEffect } from "react";
export default function Logout() {
useEffect(() => {
const redirectURL = `${window.origin}/__catalyst/auth/login`;
const auth = window.catalyst.auth;
auth.signOut(redirectURL);
}, []);
return null;
}
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { fetchSharedDetails, handleUpdateSharedDetails } from "../service/ImageService";
export default function SharedDetails({ userId }) {
const [details, setDetails] = useState([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const rowsPerPage = 10;
const navigate = useNavigate();
useEffect(() => {
fetchSharedDetails(userId, setDetails, setLoading);
}, [userId]);
const handleChange = (index, key, value) => {
const updatedData = [...details];
updatedData[index][key] = value;
setDetails(updatedData);
};
const indexOfLastRow = currentPage * rowsPerPage;
const indexOfFirstRow = indexOfLastRow - rowsPerPage;
const currentRows = details.slice(indexOfFirstRow, indexOfLastRow);
const totalPages = Math.ceil(details.length / rowsPerPage);
return (
{loading ? (
) : details.length === 0 ? (
Nothing Shared
) : (
<>
Username
Image Name
Access Type
Revoke Access
Action
{currentRows.map((item, index) => (
{item.UserName}
{item.BucketPath.split("/").pop()}
))}
{details.length > rowsPerPage && (
Page {currentPage} of {totalPages}
)}
>
)}
);
}
import ImageThumbnail from "../service/ImageThumbnail";
import { FiMoreVertical } from "react-icons/fi";
export default function SharedImageGrid({
currentImages,
toggleMenu,
handleDelete,
handleDownload,
setImageDetails,
setOpenMenuIndex,
openMenuIndex,
openPreview
}) {
return (
{currentImages.map((image, index) => (
openPreview(image.object_url)}
>
{openMenuIndex === index && (
{image.isEditAccess && (
)}
)}
{image.key.split("/").pop()}
))}
);
}
import ImageThumbnail from '../service/ImageThumbnail';
import { FiDownload, FiTrash2, FiEye } from "react-icons/fi";
export default function SharedImageList({
currentImages,
handleDelete,
handleDownload,
setImageDetails,
openPreview
}) {
return (
{currentImages.map((image) => (
{image.key.split('/').pop()}
{image.isEditAccess && (
)}
))}
);
}
import { useState, useEffect } from 'react';
import { FiList, FiGrid, FiX } from "react-icons/fi";
import { fetchSharedImages, handleDelete, handleDownload } from "../service/ImageService";
import SharedImageGrid from './SharedImageGrid';
import SharedImageList from './SharedImageList';
export default function SharedImages({ userId }) {
const [imageDetails, setImageDetails] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [openMenuIndex, setOpenMenuIndex] = useState(null);
const [loading, setLoading] = useState(true);
const [selectedImage, setSelectedImage] = useState(null);
const [viewMode, setViewMode] = useState("grid");
const imagesPerPage = 9;
useEffect(() => {
fetchSharedImages(userId, setImageDetails, setLoading);
}, [userId]);
const indexOfLastImage = currentPage * imagesPerPage;
const indexOfFirstImage = indexOfLastImage - imagesPerPage;
const currentImages = imageDetails.slice(indexOfFirstImage, indexOfLastImage);
const nextPage = () => {
if (indexOfLastImage < imageDetails.length) setCurrentPage(currentPage + 1);
};
const prevPage = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};
const toggleMenu = (index, e) => {
e.preventDefault();
e.stopPropagation();
setOpenMenuIndex(openMenuIndex === index ? null : index);
};
const openPreview = (imageUrl) => setSelectedImage(imageUrl);
const closePreview = () => setSelectedImage(null);
return (
{loading ? (
) : (
<>
{imageDetails.length === 0 ? (
No Shared files found.
) : (
<>
{viewMode === "grid" ? (
) : (
)}
{imageDetails.length > imagesPerPage && (
Page {currentPage}
)}
>
)}
>
)}
{selectedImage && (
)}
);
}
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { toast } from 'react-toastify';
export default function Upload({ userId }) {
const [file, setFile] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const navigate = useNavigate();
const zuid = userId;
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
setFile(selectedFile);
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
setIsUploading(true);
const stratus = window.catalyst.stratus;
const bucket = stratus.bucket("YOUR_BUCKET_NAME");
const checkObjectAvailability = await bucket.headObject(`photos/${zuid}/${file.name}`);
console.log("Availability: " + JSON.stringify(checkObjectAvailability));
if (checkObjectAvailability.content === true) {
toast.error(`File named ${file.name} already exists`, { theme: "colored" });
setIsUploading(false);
setFile(null);
return;
}
const putObject = await bucket.putObject(`photos/${zuid}/${file.name}`, file);
const response = await putObject.start();
console.log(JSON.stringify(response));
const formData = new FormData();
formData.append("image", file);
formData.append("id", zuid);
try {
console.log("Thumbnail API Started");
const response = await axios.post("/convertToThumbnailAndUpload", formData);
console.log("Response: " + JSON.stringify(response));
} catch (error) {
console.error("Thumbnail Upload failed", error);
}
toast.success(`File uploaded: ${file.name}`, { theme: "colored" });
navigate("/");
} catch (error) {
console.error("Error during upload:", error);
toast.error("Error uploading the file. Please try again.", { theme: "colored" });
} finally {
setIsUploading(false);
setFile(null);
}
};
return (
<div className="flex items-center justify-center min-h-screen bg-black">
<div className="bg-white p-8 rounded-lg shadow-lg w-96 text-center flex flex-col items-center text-black">
<h1 className="text-2xl font-semibold mb-4 text-black">Upload Image</h1>
<p className="text-sm text-black mb-4">
NOTE: Only .png, .jpg, and .jpeg files are allowed.
</p>
<form onSubmit={handleSubmit} className="flex flex-col items-center space-y-4 w-full">
<div className="flex justify-center w-full">
<input
type="file"
onChange={handleFileChange}
className="border p-2 rounded w-full text-center"
accept="image/png, image/jpg, image/jpeg"
/>
</div>
<button
type="submit"
disabled={isUploading}
className={`px-4 py-2 text-white rounded w-full ${
isUploading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600"
}`}
>
{isUploading ? "Uploading..." : "Upload"}
</button>
</form>
</div>
</div>
);
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { HashRouter as Router } from "react-router-dom"; // Move Router here
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
@tailwind base;
@tailwind components;
@tailwind utilities;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PhotoStore App</title>
<script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.0/catalystWebSDK.js"></script>
<script src="/__catalyst/sdk/init.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
The frontend component for the application is now configured.
Let us quickly go through the working of the function and client components of the application:
-
When the application URL is triggered, we direct the user to the Login page. The authentication requirement and all login functionalities are handled by Catalyst’s Hosted Authentication. If the end-user is successfully authenticated, then they are redirected to the main page of the application.
-
Once the end-user is directed to the main page of the application, the /fetchAllImages API endpoint will be invoked. This API will return all the images that are stored in the bucket and display them as thumbnails in the client.
-
When the end-user tried to upload an image to the application, the image upload action will be completed using the upload web SDK method. While the image is being uploaded, the /convertToThumbnailAndUpload API endpoint is invoked and the images are converted to thumbnails and rendered in the client, and the uploaded image is stored in the “YOUR_BUCKET_NAME/photos/thumbnails/zuid of the logged in end-user” path.
-
The logic defined to handle the delete and download operations are defined in the handleDownload() and handleDelete() functions. When the end-user initiates the download operation, the image will be downloaded to their local system using the download web SDK method. If the image is deleted from the application, it will also be deleted from Stratus using the delete web SDK method.
-
The logic to handle the share feature is defined in handleShareAction(), which will invokes the /shareDetails API to share the required image with another registered user. When the end-user initiates the share feature, the /getAllUsers API endpoint is invoked, which will return the list of all registered end-users of the application. With the list of registered users, the end-user can choose to share the required image with View or Edit access. The View access only allows the shared member to download the image, and the Edit access allows them to update, download or delete the image.
Last Updated 2025-10-09 17:47:14 +0530 IST