# NodeJS -------------------------------------------------------------------------------- title: "Introduction" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.137Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/introduction/" service: "All Services" related: - Project Directory Structure (/en/cli/v1/project-directory-structure/introduction/) - Upload Object to Bucket (/en/cloud-scale/help/stratus/objects/upload-object/) - Stratus SDK (/en/sdk/nodejs/v2/cloud-scale/stratus/overview/) - Stratus API (/en/api/code-reference/cloud-scale/stratus/get-all-buckets/#GetAllBuckets) -------------------------------------------------------------------------------- # PhotoStore App ### Introduction This tutorial will help you build a React web application called **PhotoStore App**, which will allow you to upload and download images from your client. It will also feature the following functionalities: * All login functionalities. * You will also be able to view the uploaded images in different views, such as thumbnail, list, and full-size. * You will be able to adjust your dashboard view in a **Grid** or **List** view. * You will be able to share your uploaded images with other users that have signed on to the application. * There will be two dashboards presented to you: - **Your Gallery**: To showcase images uploaded by you. - **Shared Gallery**: To showcase images that were shared to you by other registered users. * You will also have the option to manage users' access to the images you had previously shared with them. The client side of the application will look like this: <br /> The logic of this application is coded employing the following Catalyst services and its respective components: 1. Catalyst Serverless: - AppSail: You will be bundling the front end and client code together and deploying it on *AppSail*, to host the entire application. 2. Catalyst CloudScale: - Authentication: To add end users to the application. You will be implementing the Hosted Authentication type to handle the authentication requirement. - Data Store: To store the various details of the images that are present in the application. - ZCQL: To post and fetch data from the *Data Store*. - Stratus: To store the images that are being uploaded through the application. 3. Catalyst Pipelines: To maintain the entire project seamlessly in your GitHub repository without ever missing a development lifecycle. More importantly, using Pipelines to deploy this Catalyst application allows us to use the Sharp, a platform dependent package, to convert uploaded images to thumbnails. In this manner, Pipelines handles all the dependencies needed to use the package and host it on Catalyst which runs on a Linux machine. You will use the Catalyst web console and the Catalyst Command Line Interface (CLI) to build this application. Note: You will be given the code for the files to be included in the function and client components in this tutorial. You will just need to copy the provided code and paste it into the appropriate files as directed. ### Application Workflow The workflow of the application occurs as described below: * An end-user signs up for the application. * They can either view, download, or upload images in the client. * If they choose to view the images, by default, the images will be displayed in the thumbnail-view. The details populated to provide more context for the images are handled in the *DataStore*. * When a user uploads an image from the client, the image is uploaded to *Stratus*, and the image will be referred using its Object URL. * The newly uploaded image will be displayed in a thumbnail size, along with its name, which can be viewed in full-size when a user clicks on it. * Additionally, any change you make to your application development cycle will be automatically rendered to your application present in your GitHub repository with the help of *Catalyst Pipelines*. #### Deployment Flow -------------------------------------------------------------------------------- title: "Prerequisites" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.137Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/prerequisites/" service: "All Services" related: - Catalyst CLI Documentation (/en/cli/v1/cli-command-reference/) - Catalyst VS Code Extension (/en/catalyst-extensions/vs-code-extension/introduction/) -------------------------------------------------------------------------------- # Prerequisites Before you begin building the application, you must have the following prerequisites installed on your system: 1. Catalyst CLI: Catalyst CLI contains a host of tools that enable you to initialize, develop, test, and deploy the components of your application from your local machine. We will be working with Catalyst CLI in this tutorial. You must perform the following actions: - **Install Catalyst CLI**: Catalyst CLI is installed through NPM. You must have NPM and Node.js installed on your system before you install the CLI. Refer to the Install Catalyst CLI help page for details on the prerequisites and the steps to install it. - **Login Catalyst CLI**: After you install Catalyst CLI, you must authenticate the CLI with your Catalyst account before using it. Refer to the CLI Login help page for the steps to log in from Catalyst CLI and the various options available for it. 2. Any IDE tool for Node.js and client code development: You can use any IDE to work with the function and the client code. Some popular choices include Visual Studio Code, IntelliJ IDEA, Eclipse, and Sublime Text. Download and install an IDE of your choice on your system. 3. A GitHub account: To store and manage your application files in your personal repository. If you are a Visual Studio Code IDE user, you can install the Catalyst Tools extension, and use your IDE itself in place of the CLI. You can find more details about the Catalyst VS Code extension from this help section. -------------------------------------------------------------------------------- title: "Create Project" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.137Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/create-project/" service: "All Services" related: - Catalyst Projects (/en/getting-started/catalyst-projects/) -------------------------------------------------------------------------------- # Create a Project Let's create a Catalyst project in the Catalyst console. 1. Log in to the Catalyst console and click **Create New Project**. <br /> 2. Enter the project’s name as "**PhotoStoreApp**" in the pop-up window. <br /> Your project will be created. You can open the project by clicking **Access Project**. <br /> -------------------------------------------------------------------------------- title: "Create a Table" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.137Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/create-table/" service: "All Services" related: - Catalyst DataStore (/en/cloud-scale/help/data-store/introduction/) - Scopes and Permissions (/en/cloud-scale/help/data-store/scopes-and-permissions/) -------------------------------------------------------------------------------- # Create a Table Let's create a table in the Data Store. This table is used to create the following columns to store the required details: <table class="content-table"> <thead> <tr> <th class="w25p">Column Name</th> <th class="w25p">Data Type</th> <th class="w50p">Purpose</th> </tr> </thead> <tbody> <tr> <td>UserName</td> <td>Var Char</td> <td>To store the details of the user accessing the application.</td> </tr> <tr> <td>BucketPath</td> <td>Var Char</td> <td>To store the path of the required image (object).</td> </tr> <tr> <td>UserZuid</td> <td>Var Char</td> <td>To store the unique user ID of the end user signed up to your application.</td> </tr> <tr> <td>IsUpdate</td> <td>Boolean</td> <td>To store shared image access type as a Boolean: True for Edit, False for View.</td> </tr> <tr> <td>SharedBy</td> <td>Var Char</td> <td>To store details of the registered user that had shared the image.</td> </tr> </tbody> </table> To create a table: 1. Navigate to the *Catalyst Cloud Scale* service section in the console and click **Start Exploring**. <br /> 2. Navigate to the **Data Store** component present in the *Storage* section and click **Create a new Table**. <br /> 3. Enter "**ImageShareDetails**" as the name of the table and click **Create**. <br /> Note: Ensure that you enter the name exactly as instructed because the application's code contains the same name. The table will be created. <br /> Now, let’s create the four required columns. Info: Learn more about the various supported data types and properties of a column. 4. Click **New Column** in the *Schema View* section. <br /> 5. Enter the column's name as "UserName". Select **Var Char** as its datatype, enter **Max Length** as 100, and click **Create**. <br /> The column will be created. <br /> 6. Click the **New Column** button again to create the second column. Name the column as "BucketPath". Select **Var Char** as the data type and enter 255 as the **Max Length**. Click **Create**. <br /> 7. Click the **New Column** button again to create the third column. Name the column "UserZuid". Select **Var Char** as the data type and enter 50 as the **Max Length**. Click **Create**. <br /> 8. Click the **New Column** button again to create the fourth column. Name the column "SharedBy". Select **Var Char** as the data type and enter 100 as the **Max Length**. Click **Create**. <br /> All the required columns have been created. <br /> ### Configure Scopes and Permissions To allow your end users to provide the required data to populate the created columns, you need to enable the required permissions for the *App User* profile. To enable the required permissions: 1. Click the **Scopes & Permissions** tab. <br /> 2. Select the **Update**, **Insert**, and **Delete** permissions for the *App User* by clicking the respective check boxes. Ensure that you do not change any other permission setting. <br /> The Data Store component is now configured for the application. -------------------------------------------------------------------------------- title: "Enable Authentication" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.138Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/enable-auth/" service: "All Services" related: - Catalyst Authentication (/en/cloud-scale/help/authentication/introduction/) - Hosted Authentication (/en/cloud-scale/help/authentication/native-catalyst-authentication/hosted-authentication-type/introduction/) -------------------------------------------------------------------------------- # Enable Authentication For this tutorial, you will be enabling the **Hosted Authentication** type. This native authentication type will provide us with dedicated pages for each login element. To enable the Hosted Authentication type for your application: 1. Navigate to the **Authentication** component under *Security and Identity* and click **Set Up** in the Native Catalyst Authentication option. <br /> 2. Select **Hosted Authentication** and click **Next**. <br /> Info:<br /> * Learn more about Hosted Authentication type. * Learn more about Embedded Authentication type. * Learn more about Third-party Authentication type. 3. Provide a name for your brand, and upload your logo as an image. You can use the color palettes in the console to style your login element. Use the *Preview* section on the right and design the login element to match your preference. <br /> Note: You can use the Preview section to simultaneously view the style of your Sign In, Sign Up, Password Reset, and Confirm Password login elements. However, to view the Sign Up page, you need to enable Public Signup. The steps involved in enabling Public Signup are detailed below. 4. Click the toggle button to enable *Public Signup*. <br /> Read the information pop-up and click **Yes, Proceed**. <br /> Info: Learn more about Public Signup. 5. You can enable any of the **Social Logins** according to your preferences. <br /> Info: Learn the steps to generate Client ID and Client Secret credentials required to configure other Social Logins. 6. We will be skipping the **Additional Settings** section right now. We will revisit the **Authorized Domains** feature present in this section at a later step. Click **Finish**. <br /> Hosted Authentication will be enabled for your application. You will be able to view the style and secure access URLs generated for each of your login elements in the *Authentication* Types section. <br /> Info: The rest of the sections are not required for the functionality of the application. However, you can learn about them through the following links:<br /> * Learn more about Authentication Types. * Learn more about User Management. * Learn more about Whitelisting. * Learn more about Email Templates. -------------------------------------------------------------------------------- title: "Configure Stratus" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.138Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/configure-stratus/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - Bucket URL (/en/cloud-scale/help/stratus/buckets/create-bucket/#bucket-url) - Object URL (/en/cloud-scale/help/stratus/objects/introduction/#object-url) - Ideal Practices to Name a Bucket (/en/cloud-scale/help/stratus/buckets/name-bucket/#ideal-practices-to-name-a-bucket) - Stratus Node.js SDK (/en/sdk/nodejs/v2/cloud-scale/stratus/overview/) - Stratus REST API (/en/api/code-reference/cloud-scale/stratus/get-all-buckets/#GetAllBuckets) -------------------------------------------------------------------------------- # Configure Stratus The **Stratus** component is going to act as our storage solution. We are going to store all the images that are going to be uploaded from your client in a Bucket in Stratus. This will ensure the images are referred to and rendered in the client as seamlessly and quickly as possible without any loss in quality. To configure Stratus: 1. Navigate to the **Stratus** component present in the *Storage* section of the *CloudScale* console. <br /> 2. Provide a unique name for your bucket, select **Public** as the permission template, and click **Create**. <br /> Info: Learn more about the bucket naming protocols. The bucket will be created, and Catalyst will have generated a secure Bucket URL to refer to the bucket when required. <br /> 3. Next, we are going to edit the permission template to ensure you can upload, download, and edit the images uploaded to the bucket in Stratus. Navigate to the **Bucket Permissions** tab and click **Edit Permissions**. <br /> Info: Learn more about the syntax to define permissions. <br/><br />Make sure to go through this document and the examples illustrated within to ensure you employ the right permissions for the bucket correctly. 4. Copy the JSON snippet below and replace it in place of the default permission defined in the console and click **Update**. { "rules": [ { "rule_id": "PublicBucket", "condition": { "user": { "auth_type": "public" } }, "allowed_actions": [ "PutObject", "DeleteObject", "GetObject" ], "paths": [ "YOUR_BUCKET_NAME::/photos/*" ], "effect": "allow" } ], "version": "v1" } <br /> Notes:<br /> * Ensure you provide your bucket name in line 16. * The Photos path will be automatically created when you upload an image through the client. The Stratus component has been configured for your application. -------------------------------------------------------------------------------- title: "Initialize the Project" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.138Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/init-app/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - CLI Help Documentation (/en/cli/v1/cli-command-reference/) - Initialize Resources From CLI (/en/cli/v1/initialize-resources/introduction/) - Project Directory Structure (/en/cli/v1/project-directory-structure/introduction/) - Catalyst AppSail (/en/serverless/help/appsail/introduction/) - Add an AppSail Service (/en/cli/v1/add-appsail/) - Catalyst Multi-Orgnization Portal (/en/getting-started/catalyst-organizations/#access-the-multi-org-portal) - Catalyst Tools (/en/catalyst-extensions/vs-code-extension/introduction/) -------------------------------------------------------------------------------- # Initialize the Project You can now begin working on your Catalyst project from the CLI. The first step is to initialize the project in an empty directory. This will be the home directory of your project, and all of the project files will be saved in it. You can learn more about this from the Project Directory Structure help page. You can learn about initializing a project in detail from the CLI help documentation. For this application, we will only be initializing the project. We will not be initializing any function or client components. 1. Create a folder for the project on your local machine and navigate to it from the terminal. 2. Initialize a project by executing the following command from that directory: catalyst init Navigate using the **arrow keys** and select your preferred portal and click **Enter**. If you have no other organizations associated with the account, then the default will be selected automatically. <br /> Info: Learn more about Catalyst’s multi-org portal feature. 3. The CLI will now ask you to associate a Catalyst project with the directory. Associate it with the project that we created earlier from the console. Select **PhotoStoreApp** from the list and click **Enter**. <br /> 4. Press **Enter** without making any selection. <br /> The project will be initialized, and a catalyst.json file will be created. <br /> 5. Initialize an AppSail service in your project directory by executing the following CLI command: catalyst appsail:add Note: In this tutorial, we are using the add command to initialize the AppSail service to showcase alternative Catalyst CLI commands to initialize the AppSail service. 6. The CLI will prompt you to choose between Catalyst-Managed Runtime and Docker Image. Because you are creating this project from one of the Catalyst-Managed Runtime, select **Catalyst-Managed Runtime** and click **Enter**. <br/> 7. Enter "photo-store-app" as the name of the AppSail service. <br /> 8. Enter "." as the build path and select the latest runtime of *Node.js* as the function stack for your AppSail service. <br /> The AppSail service will be initialized for your project. <br /> 9. To initialize the client as a React web app, execute the following command: npx create-react-app photo-store-app <br /> The client will be initialized as a React web app. <br /> The client and backend of the application have been initialized and are ready to be configured according to our requirements. 10. Run the following command in the PhotostoreApp/photo-store-app directory. <br /> This is to ensure the project is getting served in localhost without any errors. <br /> Your current project directory should appear as follows: <br /> The AppSail service has been initialized for your application. -------------------------------------------------------------------------------- title: "Configure AppSail Service" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.139Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/config-appsail/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - Catalyst AppSail (/en/serverless/help/appsail/introduction/) - Catalyst Pipelines (/en/pipelines/help/pipelines/introduction/) - Serve Resources (/en/cli/v1/serve-resources/introduction/) - Deploy Resources (/en/cli/v1/deploy-resources/introduction/) -------------------------------------------------------------------------------- # Configure AppSail Service Before we deploy your AppSail service to the console, you need to add the following files and code to your project directory: - Create a folder named server in your project root directory, and create the following files in them: - index.js: This file will act as the proxy server to server your compiled react files. - Create a folder named scripts, and add the following files to it: - filesHelper.js: This file will be used to copy the files from the server and photo-store-app directories to the AppSail build path. - Create a folder named build in your project's root directory. This folder will contain the compiled react files of your client. You need to point this folder as your build path to start your application. Create the following folders in the build directory. - server - photo-store-app The application's directory should now appear in the following manner: <br /> Next navigate to the PhotoStoreApp/server/ directory and perform the following steps: 1. Run the following CLI command from your terminal: npm init <br /> 2. Run the following command to install the *Express* package to handle HTTP requests and responses. npm install express --save <br /> This will also create a node_modules folder in the PhotoStoreApp/server directory. 3. Add the following code to your index.js file present in the PhotoStoreApp/server/ directory. 'use strict'; const express = require('express'); const path = require('path'); const app = express(); const appDir = path.join(__dirname, '../photo-store-app'); const port = process.env.X_ZOHO_CATALYST_LISTEN_PORT || 9000; app.get('/', function (req, res) { res.sendFile(path.join(appDir)); }); app.use(express.static(appDir)); app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); <br /> Note: You need to use the environment variable 'X_ZOHO_CATALYST_LISTEN_PORT' to listen to the port configured by Catalyst. 4. Next, navigate to the scripts folder present in the PhotoStoreApp/scripts directory and execute the following command: npm init Ensure you enter the defaults, and this command will create a package.json file. <br /> 5. Execute the following command in your scripts folder to install the required packages: npm install path util fs <br /> This command will install the following packages: - path: This package will be used to handle file and directory paths. - util: This package is required to use utilities like promisify, inherits, and so on. - fs: This package is required to implement file system operations. 6. Copy the following code and paste it in the filesHelper.js file present in the PhotoStoreApp/scripts directory. const Path = require('path'); const { promisify } = require('util'); const Fs = require('fs'); const readdir = promisify(Fs.readdir); const stat = promisify(Fs.stat); const copyFile = promisify(Fs.copyFile); const mkdir = promisify(Fs.mkdir); const unlink = promisify(Fs.unlink); const rmdir = promisify(Fs.rmdir); if (process.argv.length &lt; 4) { console.error('Usage: node copyAndDelete.js [-c|-d] &lt;sourcePath&gt; &lt;destinationPath&gt;'); process.exit(1); } const operation = process.argv[2]; if (operation !== '-c' &amp;&amp; operation !== '-d') { console.error('Invalid operation. Use -c for copy or -d for delete.'); process.exit(1); } if (operation === '-c') { const sourcePath = Path.resolve(process.argv[3]); const destinationPath = Path.resolve(process.argv[4]); copyFolders(sourcePath, destinationPath) .then(() =&gt; { console.log('Copy completed successfully.'); }) .catch((err) =&gt; { console.error(`Error: ${err}`); }); } else if (operation === '-d') { const sourcePath = Path.resolve(process.argv[3]); deleteFolder(sourcePath) .then(() =&gt; { console.log('Delete completed successfully.'); }) .catch((err) =&gt; { console.error(`Error: ${err}`); }); } async function copyFolders(source, destination) { try { await mkdir(destination, { recursive: true }); const files = await readdir(source); for (const file of files) { const sourceFilePath = Path.join(source, file); const destFilePath = Path.join(destination, file); const fileStat = await stat(sourceFilePath); if (fileStat.isDirectory()) { await copyFolders(sourceFilePath, destFilePath); } else { await copyFile(sourceFilePath, destFilePath); } } } catch (err) { throw err; } } async function deleteFolder(destinationPath) { try { const files = await readdir(destinationPath); for (const file of files) { const filePath = Path.join(destinationPath, file); const fileStat = await stat(filePath); if (fileStat.isDirectory()) { await deleteFolder(filePath); } else { await unlink(filePath); } } await rmdir(destinationPath); } catch (err) { throw err; } } Now, navigate to the app-config.json file present in the project directory PhotoStoreApp/app-config.json and paste the following code. { "command": "node ./server/index.js", "build_path": "./build", "stack": "node20", "env_variables": {}, "memory": 256, "scripts": { "preserve": "cd photo-store-app && npm run build && cd .. && cd server && npm install && cd .. && cd scripts && npm install && cd .. && node ./scripts/filesHelper.js -c ./server/ ./build/server/ && node ./scripts/filesHelper.js -c ./photo-store-app/build/ ./build/photo-store-app/", "postserve": "node ./scripts/filesHelper.js -d ./build/server && node ./scripts/filesHelper.js -d ./build/photo-store-app", "predeploy": "cd photo-store-app && npm run build && cd .. && node ./scripts/filesHelper.js -c ./server/ ./build/server/ && node ./scripts/filesHelper.js -c ./photo-store-app/build/ ./build/photo-store-app/", "postdeploy": "node ./scripts/filesHelper.js -d ./build/server && node ./scripts/filesHelper.js -d ./build/photo-store-app" }, "raw": {}, "catalyst_auth": true, "login_redirect": "/index.html" } Note: You need to include the the following key value pairs in the app-config.json file: * catalyst_auth key's value needs to be set as true, to enable Catalyst Authentication for the AppSail service. * You need to set the value of login_redirect key as index.html to ensure proper redirection after authentication process. ### Test and Deploy the AppSail Service Next, navigate to your project's root directory. To check if all the steps completed thus far are error free, we are going to serve the application from the CLI using the following Catalyst CLI command: catalyst serve <br /> This command will serve the application on your localhost. The local endpoint URLs of the components are displayed. <br /> If the application is error free, the AppSail service will be successfully compiled and served typically on localhost:3001. Info: Every time you access the home page or any of the sub-pages of your client or the function, the CLI will display a live log of the URL accessed, along with the HTTP request method. Once you ensure the application is running error free, you can deploy the application to the Catalyst console using the following Catalyst CLI command: catalyst deploy <br /> The application will now be deployed to the Catalyst console. You can access the AppSail service in the console by navigating to the **AppSail** component present in the *Catalyst Serverless* section. <br /> You can find more details about the *AppSail* service by clicking on the required service. <br /> The AppSail service has been configured for your application. ### Enable Bucket CORS Once the AppSail service is deployed to the Catalyst console, we will have the application URL. Given that the domains for the bucket and the AppService are different, we need to enable Cross-Origin Resource Sharing (CORS) policy to authenticate and allow seamless interaction between the AppSail service and the Bucket. Info: Learn more about Bucket CORS To enable Bucket CORS: 1. Navigate to your Bucket present in Stratus. <br /> 2. Navigate to the **Bucket CORS** section present in the **Configurations** tab, and click **Add Domain**. <br /> 3. Select all **Request Methods** provided in the drop-down and paste the AppSail service's URL generated in the previous step. <br /> 4. Click **Add**, and CORS will be enabled for the AppSail service, ensuring secure access. <br /> ### Setup GitHub With the application deployed to the Catalyst console, you need to ensure that you have pushed the application files to your GitHub account. <br /> Note: Going ahead, we are going to implement the Pipelines service to deploy the application and sync the GitHub project with the Catalyst console. -------------------------------------------------------------------------------- title: "Configure the Backend of your Application" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.139Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/config-backend/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - Catalyst AppSail (/en/serverless/help/appsail/introduction/) - Stratus Node.js SDK (/en/sdk/nodejs/v2/cloud-scale/stratus/overview/) - Stratus REST API (/en/api/code-reference/cloud-scale/stratus/get-all-buckets/#GetAllBuckets) -------------------------------------------------------------------------------- # Configure the Backend of your Application In this step, you are going to configure the backend logic required for the application. Before you start adding code, you need to install the following packages to ensure all required dependencies are satisfied. Navigate to the server folder present in the PhotoStoreApp/server directory and install the following packages using the command shown below: - **Express**: The express package is required to handle HTTP requests. - **Sharp**: The sharp package is required to display the uploaded images as in varying sizes including thumbnails in the application. - **Multer**: The multer package is required to handle file uploads in Express applications. - Catalyst SDK: To use the required SDK methods in the AppSail service. npm install express path zcatalyst-sdk-node@3.0.0-beta.3 multer sharp <br /> You will now add code to the following files, in order to implement the required backend logic. We will be adding to code to the following files present in the server folder in the PhotoStoreApp/server/ directory: - index.js: This file will contain the required APIs to implement various application functionalities, such as, converting the images to thumbnails, getting all the images, and getting all the shared images. - helperfunction.js: This files contains the required helper functions to complete the processes detailed in the index.js file. The following table illustrates the backend logic of this application. <table class="content-table"> <thead> <tr> <th class="w25p">Application Functionality</th> <th class="w30p">APIs Present in the index.js</th> <th class="w30p">Functions present in the helperfunction.js</th> </tr> </thead> <tbody> <tr> <td>To resize and upload images as thumbnails</td> <td>/convertToThumbnailAndUpload</td> <td>uploadToStratus()</td> </tr> <tr> <td>Get required images from the bucket</td> <td>/fetchAllImages</td> <td>listMyObjects()</td> </tr> <tr> <td>Get shared images alone</td> <td>/getSharedImages</td> <td>listSharedObjects()<br/>listMyObjects()</td> </tr> </tbody> </table> Copy the code below and paste it into the respective files of your project using an IDE and save the files. Note: Go through the code in this section to make sure you fully understand it. 'use strict'; const express = require('express'); const path = require('path'); let catalyst = require('zcatalyst-sdk-node'); const multer = require("multer"); const helperFunctions = require('./helper-functions'); const app = express(); const appDir = path.join(__dirname, '../photo-store-app'); const port = process.env.X_ZOHO_CATALYST_LISTEN_PORT || 9000; app.use(express.json()); app.use(express.static(appDir)); const upload = multer({ dest: "uploads/" }); app.get('/', function (req, res) { res.sendFile(path.join(appDir, 'index.html')); }); app.post("/convertToThumbnailAndUpload", upload.single("image"), async (req, res) => { try { const obj = catalyst.initialize(req, { scope: 'admin' }); const stratus = obj.stratus(); const bucket = stratus.bucket("YOUR_BUCKET_NAME"); const thumbnailName = req.file.originalname.substring(0, req.file.originalname.lastIndexOf(".")); const inputPath = req.file.path; const zuid = req.body.id; console.log("ID: " + zuid); const thumbnailPath = `photos/thumbnails/${zuid}/`; let result; await helperFunctions.uploadToStratus(bucket, inputPath, thumbnailPath, thumbnailName) .then(resp => { console.log("Success"); result = resp; res.json({ message: "Thumbnail created and uploaded successfully" }); }) .catch(error => { console.error("Error: " + JSON.stringify(error.message)); res.status(500).json({ message: "Error Occurred" }); return; }); } catch (error) { console.log("Error in convertToThumbnailAndUpload API: " + error.message); } }); app.get("/fetchAllImages", async (req, res) => { try { const obj = catalyst.initialize(req); const zuid = req.query.id; console.log("ID: " + zuid); const objPath = "photos/" + zuid; const stratus = obj.stratus(); const bucket = stratus.bucket("YOUR_BUCKET_NAME"); let resp = await helperFunctions.listMyObjects(bucket, objPath); res.json(resp); } catch (error) { console.error("Error at fetchAllImages API... ", error.message); res.status(500).send({ error: "An error occurred while fetching images." }); } }); app.get('/getAllUsers', async (req, res) => { try { const app = catalyst.initialize(req, { scope: "user" }); const appAdmin = catalyst.initialize(req, { scope: "admin" }); const userManagementAdmin = appAdmin.userManagement(); const userManagements = app.userManagement(); let allUserPromise = userManagementAdmin.getAllUsers(); let currentUserPromise = userManagements.getCurrentUser(); let details; let currentUser; await allUserPromise.then(allUserDetails => { details = allUserDetails; }).catch(err => { console.log("Error: " + err.message); }); await currentUserPromise.then(details => { currentUser = details.email_id; }).catch(err => { console.log("Error: " + err.message); }); const userDetails = details.map(id => ({ zuid: id.zuid, mailId: id.email_id, name: id.first_name })); const otherUsers = userDetails.filter(user => user.mailId !== currentUser); res.send(otherUsers); } catch (error) { console.error("Error in getAllUsers API: " + JSON.stringify(error.message)); res.status(500).send({ error: "An error occurred while fetching details." }); } }); app.post('/shareDetails', async (req, res) => { try { const app = catalyst.initialize(req); let zcql = app.zcql(); let query = `SELECT COUNT(ImageShareDetails.BucketPath) FROM ImageShareDetails WHERE BucketPath = '${req.body.imagePath}' AND UserZuid = '${req.body.zuid}'`; let result = await zcql.executeZCQLQuery(query); let isPresent = result[0].ImageShareDetails["COUNT(BucketPath)"]; if (isPresent == 0) { let rowData = { UserName: req.body.userName, BucketPath: req.body.imagePath, UserZuid: req.body.zuid, IsUpdate: req.body.isUpdate, SharedBy: req.body.sharedBy }; let datastore = app.datastore(); let table = datastore.table('ImageShareDetails'); let insertPromise = table.insertRow(rowData); insertPromise.then((row) => { console.log("Inserted Row: " + row); }).catch((err) => { console.error("Error: " + err.message); }); res.json({ message: "Access Provided" }); } else { res.json({ message: "Image Already Shared" }); } } catch (error) { console.error("Error in shareDetails API: " + error.message); res.status(500).send({ message: "Error Occurred" }); } }); app.get('/getSharedImages', async (req, res) => { try { const obj = catalyst.initialize(req); const zuid = req.query.id; const objPath = "photos/" + zuid; const stratus = obj.stratus(); const bucket = stratus.bucket("YOUR_BUCKET_NAME"); const zcql = obj.zcql(); let resp = await helperFunctions.listSharedObjects(bucket, objPath, zcql, zuid); res.json(resp); } catch (error) { console.error("Error in getSharedImages API: " + error.message); res.status(500).json({ message: "Error Occurred" }); } }); app.get('/getSharedDetails', async (req, res) => { try { const obj = catalyst.initialize(req); const zuid = req.query.id; let zcql = obj.zcql(); let query = `SELECT * FROM ImageShareDetails WHERE SharedBy = '${zuid}'`; let data = await zcql.executeZCQLQuery(query); const result = data.map(item => ({ UserName: item.ImageShareDetails.UserName, IsUpdate: item.ImageShareDetails.IsUpdate, BucketPath: item.ImageShareDetails.BucketPath, UserId: item.ImageShareDetails.UserZuid })); res.send(result); } catch (error) { console.error("Error in getSharedDetails API: " + error.message); res.status(500).json({ message: "Error Occurred" }); } }); app.patch('/updateSharedDetails', async (req, res) => { try { const obj = catalyst.initialize(req); const isRevoke = req.body.RevokeAccess; const zuid = req.body.UserId; const isUpdate = req.body.IsUpdate; const bucketPath = req.body.BucketPath; let zcql = obj.zcql(); let query; if (isRevoke == "yes") { query = `DELETE FROM ImageShareDetails WHERE UserZuid = '${zuid}' AND BucketPath = '${bucketPath}'`; } else { query = `UPDATE ImageShareDetails SET IsUpdate = ${isUpdate} WHERE UserZuid = '${zuid}' AND BucketPath = '${bucketPath}'`; } let data = await zcql.executeZCQLQuery(query); res.json({ message: "Updated Successfully" }); } catch (error) { console.error("Error in updateSharedDetails API: " + error); res.status(500).json({ message: "Error Occurred" }); } }); app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); Note: Ensure you provide your bucket name in the lines 20, 49, and 127. const sharp = require("sharp"); async function listMyObjects(bucket, prefix, isEdit = true) { try { const objects = await bucket.listPagedObjects({ prefix: prefix }); let result = []; for (let i = 0; i < objects.contents.length; i++) { const objDetails = JSON.parse(objects.contents[i]); const imgInfo = { key: objDetails.key, object_url: objDetails.object_url, isEditAccess: isEdit }; result.push(imgInfo); } return result; } catch (error) { console.error("Error at listMyObjects function: " + error); } } async function uploadToStratus(bucket, inputPath, thumbnailPath, thumbnailName) { try { const streamData = await sharp(inputPath) .resize({ width: 150, height: 150 }) .toFormat("jpeg", { quality: 70 }) .toBuffer(); const result = await bucket.putObject( thumbnailPath + thumbnailName + ".jpeg", streamData ); console.log("uploadToStratus method Completed"); return result; } catch (error) { console.error("Error Occurred..." + JSON.stringify(error)); throw { message: "Error in uploading", code: 500 }; } } async function listSharedObjects(bucket, prefix, zcql, zuid) { try { let query = `SELECT * FROM ImageShareDetails WHERE UserZuid = ${zuid}`; let result = await zcql.executeZCQLQuery(query); const queryData = result.map(item => ({ path: item.ImageShareDetails.BucketPath, isEdit: item.ImageShareDetails.IsUpdate })); let allSharedImages = []; for (const item of queryData) { const result = await listMyObjects(bucket, item.path, item.isEdit); allSharedImages.push(result); } return allSharedImages; } catch (error) { console.error("Error Occurred..." + error.message); } } module.exports = { listMyObjects, uploadToStratus, listSharedObjects }; -------------------------------------------------------------------------------- title: "Configure the Frontend of Your Application" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.140Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/config-frontend/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - Path Functionality (/en/cloud-scale/help/stratus/objects/introduction/#path) - Hosted Authentication (/en/cloud-scale/help/authentication/native-catalyst-authentication/hosted-authentication-type/introduction/) - Stratus Web SDK (/en/sdk/web/v4/cloud-scale/stratus/overview/) - Upload Web SDK (/en/sdk/web/v4/cloud-scale/stratus/upload-object/) - Download Web SDK (/en/sdk/web/v4/cloud-scale/stratus/download-object/) - Delete Web SDK (/en/sdk/web/v4/cloud-scale/stratus/delete-object/) - Stratus REST API (/en/api/code-reference/cloud-scale/stratus/get-all-buckets/#GetAllBuckets) - Client Directory Structure (/en/cli/v1/project-directory-structure/client-directory/) - Web Client Hosting (/en/cloud-scale/help/web-client-hosting/introduction/) -------------------------------------------------------------------------------- # Configure the Frontend of Your Application Next, 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: 1. Navigate to the PhotoStoreApp/photo-store-app/ directory and execute the following command <!-- npm install -D tailwindcss <br /> Next, execute the following command to complete the installation of the required Tailwind CSS framework. --> to complete the installation of the required Tailwind CSS framework. npm install -D tailwindcss@3.4.17 <br /> 2. Execute the following command to create a tailwind.config.js file in your PhotoStoreApp/photo-store-app/ directory. npx tailwindcss init A tailwind.config.js file will be created. The project directory should now look like the image shown below: <br /> 3. 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. npm install axios hamburger-react react-toastify react-router-dom react-icons <br /> ### 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. - 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. - 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: <br /> Let's begin adding code to your files. Note: Please go through the code in this section to make sure you fully understand it. 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 ( &lt;h1 className="text-2xl font-bold px-4 cursor-pointer hover:text-gray-300 transition duration-300" onClick={() =&gt; navigate("/")}&gt; Photo Store App &lt;/h1&gt; ); } 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(() =&gt; { const authenticateUser = async () =&gt; { try { const result = await window.catalyst.auth.isUserAuthenticated(); setUserId(result.content.zuid); setIsUserAuthenticated(true); } catch (err) { console.log("UNAUTHENTICATED"); } finally { setIsFetching(false); } }; authenticateUser(); }, []); return ( &lt;div className="bg-black text-white min-h-screen flex flex-col overflow-hidden"&gt; &lt;nav className="flex justify-between items-center px-6 py-4 shadow-lg relative"&gt; &lt;div className="flex items-center relative"&gt; &lt;Hamburger toggled={isOpen} toggle={setOpen} /&gt; {isOpen &amp;&amp; ( &lt;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"&gt; &lt;ul className="flex flex-col space-y-4"&gt; &lt;li className="py-2 px-4 cursor-pointer hover:bg-blue-700" onClick={() =&gt; { setOpen(false); navigate("/upload"); }} &gt; Upload Image &lt;/li&gt; &lt;li className="py-2 px-4 cursor-pointer hover:bg-blue-700" onClick={() =&gt; { setOpen(false); navigate("/"); }} &gt; Your Gallery &lt;/li&gt; &lt;li className="py-2 px-4 cursor-pointer hover:bg-blue-700" onClick={() =&gt; { setOpen(false); navigate("/sharedImages"); }} &gt; Shared Gallery &lt;/li&gt; &lt;li className="py-2 px-4 cursor-pointer hover:bg-blue-700" onClick={() =&gt; { setOpen(false); navigate("/sharedDetails"); }} &gt; Manage Shared Details &lt;/li&gt; &lt;/ul&gt; &lt;ul className="pb-16 mt-auto"&gt; &lt;li className="py-2 px-4 cursor-pointer hover:bg-red-700" onClick={() =&gt; { setOpen(false); navigate("/logout"); }} &gt; LogOut &lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; )} &lt;HeaderClick /&gt; &lt;/div&gt; &lt;ul className="flex space-x-32"&gt; &lt;li&gt; &lt;button onClick={() =&gt; navigate("/upload")} className="bg-blue-800 text-white px-4 py-2 rounded-md hover:bg-blue-900 transition duration-300" &gt; Upload &lt;/button&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/nav&gt; &lt;div className="flex-grow flex justify-center items-center overflow-hidden"&gt; {isFetching ? ( &lt;div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"&gt;&lt;/div&gt; ) : isUserAuthenticated ? ( &lt;Routes&gt; &lt;Route path="/upload" element=&lt;Upload userId={userId} /&gt; /&gt; &lt;Route path="/sharedDetails" element=&lt;SharedDetails userId={userId} /&gt; /&gt; &lt;Route path="/logout" element=&lt;Logout /&gt; /&gt; &lt;Route path="/" element=&lt;Home userId={userId} /&gt; /&gt; &lt;Route path="/sharedImages" element=&lt;SharedImages userId={userId} /&gt; /&gt; &lt;Route path="*" element=&lt;Home userId={userId} /&gt; /&gt; &lt;/Routes&gt; ) : ( &lt;Login /&gt; )} &lt;/div&gt; &lt;ToastContainer /&gt; &lt;/div&gt; ); } export default App; import { toast } from 'react-toastify'; export const fetchImages = async (userId, setImageDetails, setLoading) =&gt; { 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) =&gt; { image.object_url = image.object_url + "?responseCacheControl=max-age=3600"; }); setTimeout(() =&gt; { setLoading(false); }, 1000); setImageDetails(data || []); } catch (err) { console.error(err); toast.error("Error Occurred", { theme: "colored", }); } }; export const fetchUsers = async (setUsers) =&gt; { 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) =&gt; { 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) =&gt; { console.log("ZCQL Response: " + JSON.stringify(response.content)); }) .catch((err) =&gt; { console.log("ZCQL Error: " + err); }); setImageDetails(prev =&gt; prev.filter(image =&gt; 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) =&gt; { 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) =&gt; { 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) =&gt; { console.error("Error downloading file:", error); }); setTimeout(() =&gt; { 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) =&gt; { 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) =&gt; { 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 =&gt; item || []); setTimeout(() =&gt; { setLoading(false); }, 1000); setImageDetails(flattenedImages || []); } catch (err) { console.error(err); toast.error("Error Occurred", { theme: "colored", }); } }; export const fetchSharedDetails = async (userId, setDetails, setLoading) =&gt; { 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) =&gt; { 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" }); } }; Note: Ensure you provide your bucket name in the lines 38, and 73. 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(() =&gt; { setThumbnail(newFilePath); }, [imageUrl]); return ( &lt;&gt; {thumbnail ? ( listStyle ? ( &lt;img src={thumbnailUrl} alt="Thumbnail" style={{ width: "1rem", height: "1rem" }} /&gt; ) : ( &lt;img src={thumbnailUrl} alt="Thumbnail image" className="w-40 h-40 object-cover rounded-lg transition-all duration-500 ease-in-out" /&gt; ) ) : ( &lt;p&gt;Loading...&lt;/p&gt; )} &lt;/&gt; ); } Note: Ensure you provide your bucket's URL in line 5. 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(() =&gt; { fetchImages(userId, setImageDetails, setLoading); }, [userId]); useEffect(() =&gt; { const handleClickOutside = (event) =&gt; { if (openShareIndex !== null || selectedUserIndex !== null) { setOpenShareIndex(null); setSelectedUserIndex(null); } if (openMenuIndex !== null) { setOpenMenuIndex(null); } }; document.addEventListener("click", handleClickOutside); return () =&gt; document.removeEventListener("click", handleClickOutside); }, [openShareIndex, selectedUserIndex, openMenuIndex]); const handleShareClick = async (index, e) =&gt; { e.preventDefault(); e.stopPropagation(); if (openShareIndex === index) { setOpenShareIndex(null); setSelectedUserIndex(null); return; } await fetchUsers(setUsers); setOpenShareIndex(index); }; const toggleUserOptions = (index, e) =&gt; { e.preventDefault(); e.stopPropagation(); setSelectedUserIndex(selectedUserIndex === index ? null : index); }; const openPreview = (imageUrl) =&gt; setSelectedImage(imageUrl); const closePreview = () =&gt; setSelectedImage(null); const indexOfLastImage = currentPage * imagesPerPage; const indexOfFirstImage = indexOfLastImage - imagesPerPage; const currentImages = imageDetails.slice(indexOfFirstImage, indexOfLastImage); const nextPage = () =&gt; { if (indexOfLastImage &lt; imageDetails.length) setCurrentPage(currentPage + 1); }; const prevPage = () =&gt; { if (currentPage &gt; 1) setCurrentPage(currentPage - 1); }; const toggleMenu = (index, e) =&gt; { e.preventDefault(); e.stopPropagation(); setOpenMenuIndex(openMenuIndex === index ? null : index); }; return ( &lt;div className="min-h-screen bg-black flex flex-col items-center p-6 relative"&gt; {loading ? ( &lt;div className="flex justify-center items-center h-screen"&gt; &lt;div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"&gt;&lt;/div&gt; &lt;/div&gt; ) : ( &lt;&gt; {imageDetails.length === 0 ? ( &lt;div className="flex items-center justify-center h-screen"&gt; &lt;p className="text-white text-lg font-semibold text-center"&gt; No files found. Click the Upload button to upload your photos. &lt;/p&gt; &lt;/div&gt; ) : ( &lt;&gt; &lt;button className="absolute top-4 right-4 bg-blue-700 text-white px-4 py-2 rounded shadow-lg flex items-center space-x-2" onClick={() =&gt; setViewMode(viewMode === "grid" ? "list" : "grid")} &gt; {viewMode === "grid" ? &lt;FiList size={20} /&gt; : &lt;FiGrid size={20} /&gt;} &lt;/button&gt; &lt;div className="max-w-screen-lg w-full mt-12"&gt; {viewMode === "grid" ? ( &lt;ImageGrid currentImages={currentImages} openShareIndex={openShareIndex} handleShareClick={handleShareClick} users={users} selectedUserIndex={selectedUserIndex} toggleUserOptions={toggleUserOptions} handleShareAction={handleShareAction} openPreview={openPreview} openMenuIndex={openMenuIndex} toggleMenu={toggleMenu} handleDelete={handleDelete} handleDownload={handleDownload} setImageDetails={setImageDetails} setOpenMenuIndex={setOpenMenuIndex} setOpenShareIndex={setOpenShareIndex} setSelectedUserIndex={setSelectedUserIndex} userId={userId} /&gt; ) : ( &lt;ImageList currentImages={currentImages} openShareIndex={openShareIndex} handleShareClick={handleShareClick} users={users} selectedUserIndex={selectedUserIndex} toggleUserOptions={toggleUserOptions} handleShareAction={handleShareAction} handleDelete={handleDelete} handleDownload={handleDownload} setImageDetails={setImageDetails} openPreview={openPreview} setOpenShareIndex={setOpenShareIndex} setSelectedUserIndex={setSelectedUserIndex} userId={userId} /&gt; )} &lt;/div&gt; {imageDetails.length &gt; imagesPerPage && ( &lt;div className="flex mt-6 justify-center items-center space-x-4"&gt; &lt;button onClick={prevPage} disabled={currentPage === 1} className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200" &gt; Previous &lt;/button&gt; &lt;span className="text-lg font-semibold text-center w-20"&gt; Page {currentPage} &lt;/span&gt; &lt;button onClick={nextPage} disabled={indexOfLastImage &gt;= imageDetails.length} className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200" &gt; Next &lt;/button&gt; &lt;/div&gt; )} &lt;/&gt; )} &lt;/&gt; )} {selectedImage && ( &lt;div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-80 z-50"&gt; &lt;div className="relative"&gt; &lt;button className="absolute top-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100" onClick={closePreview} &gt; &lt;FiX size={20} className="text-gray-600" /&gt; &lt;/button&gt; &lt;img src={selectedImage} alt="Preview" className="max-w-full max-h-[50vh] rounded-lg shadow-lg" /&gt; &lt;/div&gt; &lt;/div&gt; )} &lt;/div&gt; ); } 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 ( &lt;div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 place-items-center"&gt; {currentImages.map((image, index) =&gt; ( &lt;div key={image.key} className={`flex flex-col items-center w-full max-w-xs relative cursor-pointer ${ openShareIndex === index ? "" : "transition-transform hover:scale-110" }`} onClick={() =&gt; openPreview(image.object_url)} &gt; &lt;div className="max-w-xs h-auto flex items-center justify-center bg-gray-200 rounded-lg shadow-lg relative"&gt; {/* Share Button */} &lt;button className="absolute top-2 left-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100" onClick={(e) =&gt; handleShareClick(index, e)} &gt; &lt;FiShare2 size={15} className="text-gray-600" /&gt; &lt;/button&gt; {/* Share Dropdown */} {openShareIndex === index && users.length &gt; 0 && ( &lt;div className="absolute top-10 left-2 bg-white text-black border rounded shadow-lg w-40 text-sm z-50"&gt; {users.map((user, idx) =&gt; ( &lt;div key={user.id} className="relative"&gt; &lt;button className="flex justify-between items-center w-full text-left text-black px-4 py-2 hover:bg-gray-200" onClick={(e) =&gt; toggleUserOptions(idx, e)} &gt; {user.name} &lt;FiChevronRight className="text-gray-600" /&gt; &lt;/button&gt; {selectedUserIndex === idx && ( &lt;div className="absolute top-0 left-full ml-2 bg-white text-black border rounded shadow-lg w-32 text-sm z-50" style={{ zIndex: 1000, pointerEvents: "auto" }} &gt; &lt;button className="block w-full px-4 py-2 text-left hover:bg-gray-200" onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleShareAction(user.name, user.zuid, image.key, false, userId); setSelectedUserIndex(null); setOpenShareIndex(null); }} &gt; View Access &lt;/button&gt; &lt;button className="block w-full px-4 py-2 text-left hover:bg-gray-200" onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleShareAction(user.name, user.zuid, image.key, true, userId); setSelectedUserIndex(null); setOpenShareIndex(null); }} &gt; Edit Access &lt;/button&gt; &lt;/div&gt; )} &lt;/div&gt; ))} &lt;/div&gt; )} {/* Image Thumbnail */} &lt;ImageThumbnail imageUrl={image.object_url} /&gt; {/* Options Menu Button */} &lt;button className="absolute bottom-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100" onClick={(e) =&gt; toggleMenu(index, e)} &gt; &lt;FiMoreVertical size={15} className="text-gray-600" /&gt; &lt;/button&gt; {/* Options Menu Dropdown */} {openMenuIndex === index && ( &lt;div className="absolute bottom-10 right-2 text-blue-600 bg-white border rounded shadow-lg w-32 text-sm z-50"&gt; &lt;button className="block w-full px-4 py-2 text-left hover:bg-gray-100" onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleDownload(image.key, setOpenMenuIndex); }} &gt; Download &lt;/button&gt; &lt;button className="block w-full px-4 py-2 text-left text-red-600 hover:bg-gray-100" onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleDelete(image.key, setImageDetails, setOpenMenuIndex); }} &gt; Delete &lt;/button&gt; &lt;/div&gt; )} &lt;/div&gt; {/* Image Name */} &lt;div className="w-full max-w-xs mt-3"&gt; &lt;p className="text-xl font-medium text-white text-center break-words"&gt; {image.key.split("/").pop()} &lt;/p&gt; &lt;/div&gt; &lt;/div&gt; ))} &lt;/div&gt; ); }; 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 ( &lt;div className="flex flex-col space-y-2"&gt; {currentImages.map((image, index) =&gt; ( &lt;div key={image.key} className={`flex items-center w-full bg-gray-800 p-4 rounded-md ${ openShareIndex === index ? '' : 'transition-transform hover:scale-110' }`} &gt; &lt;ImageThumbnail imageUrl={image.object_url} listStyle={1} /&gt; &lt;div className="ml-2 flex-1"&gt; &lt;p className="text-white text-sm font-medium break-words"&gt; {image.key.split('/').pop()} &lt;/p&gt; &lt;/div&gt; &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1" onClick={() =&gt; openPreview(image.object_url)} &gt; &lt;FiEye size={16} className="text-gray-600" /&gt; &lt;/button&gt; &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1" onClick={() =&gt; handleDownload(image.key, null)} &gt; &lt;FiDownload size={16} className="text-gray-600" /&gt; &lt;/button&gt; &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-red-100" onClick={() =&gt; handleDelete(image.key, setImageDetails, null)} &gt; &lt;FiTrash2 size={16} className="text-red-600" /&gt; &lt;/button&gt; &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1" onClick={(e) =&gt; handleShareClick(index, e)} &gt; &lt;FiShare2 size={15} className="text-gray-600" /&gt; &lt;/button&gt; {openShareIndex === index && users.length &gt; 0 && ( &lt;div className="block top-10 right-4 bg-white text-black border rounded shadow-lg w-40 text-sm z-50"&gt; {users.map((user, idx) =&gt; ( &lt;div key={user.id} className="relative"&gt; &lt;button className="flex justify-between items-center w-full text-left text-black px-4 py-2 hover:bg-gray-200" onClick={(e) =&gt; toggleUserOptions(idx, e)} &gt; {user.name} &lt;FiChevronRight className="text-gray-600" /&gt; &lt;/button&gt; {selectedUserIndex === idx && ( &lt;div className="absolute top-0 left-full ml-2 bg-white text-black border rounded shadow-lg w-32 text-sm z-50" style={{ zIndex: 1000, pointerEvents: "auto" }} &gt; &lt;button className="block w-full px-4 py-2 text-left hover:bg-gray-200" onMouseEnter={() =&gt; (document.body.style.pointerEvents = "none")} onMouseLeave={() =&gt; (document.body.style.pointerEvents = "auto")} onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleShareAction(user.name, user.zuid, image.key, false, userId); setSelectedUserIndex(null); setOpenShareIndex(null); }} &gt; View Access &lt;/button&gt; &lt;button className="block w-full px-4 py-2 text-left hover:bg-gray-200" onMouseEnter={() =&gt; (document.body.style.pointerEvents = "none")} onMouseLeave={() =&gt; (document.body.style.pointerEvents = "auto")} onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleShareAction(user.name, user.zuid, image.key, true, userId); setSelectedUserIndex(null); setOpenShareIndex(null); }} &gt; Edit Access &lt;/button&gt; &lt;/div&gt; )} &lt;/div&gt; ))} &lt;/div&gt; )} &lt;/div&gt; ))} &lt;/div&gt; ); } import { useEffect } from "react"; function Login() { useEffect(() =&gt; { window.location.href = `${window.origin}/__catalyst/auth/login`; }, []); return null; } export default Login; import { useEffect } from "react"; export default function Logout() { useEffect(() =&gt; { 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(() =&gt; { fetchSharedDetails(userId, setDetails, setLoading); }, [userId]); const handleChange = (index, key, value) =&gt; { 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 ( &lt;div className="p-6"&gt; {loading ? ( &lt;div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"&gt;&lt;/div&gt; ) : details.length === 0 ? ( &lt;p className="text-white text-center text-lg font-bold"&gt;Nothing Shared&lt;/p&gt; ) : ( &lt;&gt; &lt;table className="w-full border-collapse border border-red-400"&gt; &lt;thead&gt; &lt;tr className="bg-purple-700"&gt; &lt;th className="border text-white border-white px-4 py-2"&gt;Username&lt;/th&gt; &lt;th className="border text-white border-white px-4 py-2"&gt;Image Name&lt;/th&gt; &lt;th className="border text-white border-white px-4 py-2"&gt;Access Type&lt;/th&gt; &lt;th className="border text-white border-white px-4 py-2"&gt;Revoke Access&lt;/th&gt; &lt;th className="border text-white border-white px-4 py-2"&gt;Action&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; {currentRows.map((item, index) =&gt; ( &lt;tr key={index} className="text-center"&gt; &lt;td className="border text-white border-white px-4 py-2"&gt;{item.UserName}&lt;/td&gt; &lt;td className="border text-white border-white px-4 py-2"&gt; {item.BucketPath.split("/").pop()} &lt;/td&gt; &lt;td className="border text-white border-white px-4 py-2"&gt; &lt;select value={item.IsUpdate} onChange={(e) =&gt; handleChange(index, "IsUpdate", e.target.value === "true") } className="border bg-green-700 rounded px-2 py-1" &gt; &lt;option value="true"&gt;Edit Access&lt;/option&gt; &lt;option value="false"&gt;View Access&lt;/option&gt; &lt;/select&gt; &lt;/td&gt; &lt;td className="border p-2"&gt; &lt;label className="flex items-center justify-center cursor-pointer"&gt; &lt;input type="checkbox" className="sr-only" checked={item.RevokeAccess === "yes"} onChange={() =&gt; handleChange(index, "RevokeAccess", item.RevokeAccess === "yes" ? "no" : "yes") } /&gt; &lt;div className={`relative w-12 h-6 rounded-full transition ${ item.RevokeAccess === "yes" ? "bg-red-500" : "bg-gray-300" }`} &gt; &lt;div className={`absolute left-1 top-1 w-4 h-4 bg-white rounded-full shadow-md transition-transform ${ item.RevokeAccess === "yes" ? "translate-x-6" : "translate-x-0" }`} &gt;&lt;/div&gt; &lt;/div&gt; &lt;span className="ml-2 text-sm font-medium"&gt; {item.RevokeAccess === "yes" ? "Yes" : "No"} &lt;/span&gt; &lt;/label&gt; &lt;/td&gt; &lt;td className="border border-white px-4 py-2"&gt; &lt;button onClick={() =&gt; handleUpdateSharedDetails(details[index], navigate)} className="bg-blue-700 text-white px-4 py-1 rounded hover:bg-blue-900" &gt; Update &lt;/button&gt; &lt;/td&gt; &lt;/tr&gt; ))} &lt;/tbody&gt; &lt;/table&gt; {details.length &gt; rowsPerPage && ( &lt;div className="flex justify-center mt-4"&gt; &lt;button onClick={() =&gt; setCurrentPage((prev) =&gt; Math.max(prev - 1, 1))} disabled={currentPage === 1} className="bg-gray-600 text-white px-3 py-1 mx-2 rounded disabled:opacity-50" &gt; Previous &lt;/button&gt; &lt;span className="text-white mx-2"&gt; Page {currentPage} of {totalPages} &lt;/span&gt; &lt;button onClick={() =&gt; setCurrentPage((prev) =&gt; Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages} className="bg-gray-600 text-white px-3 py-1 mx-2 rounded disabled:opacity-50" &gt; Next &lt;/button&gt; &lt;/div&gt; )} &lt;/&gt; )} &lt;/div&gt; ); } import ImageThumbnail from "../service/ImageThumbnail"; import { FiMoreVertical } from "react-icons/fi"; export default function SharedImageGrid({ currentImages, toggleMenu, handleDelete, handleDownload, setImageDetails, setOpenMenuIndex, openMenuIndex, openPreview }) { return ( &lt;div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 place-items-center"&gt; {currentImages.map((image, index) =&gt; ( &lt;div key={image.key} className="flex flex-col items-center w-full max-w-xs relative cursor-pointer transition-transform hover:scale-110" onClick={() =&gt; openPreview(image.object_url)} &gt; &lt;div className="max-w-xs h-auto flex items-center justify-center bg-gray-200 rounded-lg shadow-lg relative"&gt; &lt;ImageThumbnail imageUrl={image.object_url} /&gt; &lt;button className="absolute bottom-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100" onClick={(e) =&gt; toggleMenu(index, e)} &gt; &lt;FiMoreVertical size={15} className="text-gray-600" /&gt; &lt;/button&gt; {openMenuIndex === index && ( &lt;div className="absolute bottom-10 right-2 text-blue-600 bg-white border rounded shadow-lg w-32 text-sm z-50"&gt; {image.isEditAccess && ( &lt;button className="block w-full px-4 py-2 text-left text-red-600 hover:bg-gray-100" onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleDelete(image.key, setImageDetails, setOpenMenuIndex); }} &gt; Delete &lt;/button&gt; )} &lt;button className="block w-full px-4 py-2 text-left hover:bg-gray-100" onClick={(e) =&gt; { e.preventDefault(); e.stopPropagation(); handleDownload(image.key, setOpenMenuIndex); }} &gt; Download &lt;/button&gt; &lt;/div&gt; )} &lt;/div&gt; &lt;div className="w-full max-w-xs mt-3"&gt; &lt;p className="text-xl font-medium text-white text-center break-words"&gt; {image.key.split("/").pop()} &lt;/p&gt; &lt;/div&gt; &lt;/div&gt; ))} &lt;/div&gt; ); } import ImageThumbnail from '../service/ImageThumbnail'; import { FiDownload, FiTrash2, FiEye } from "react-icons/fi"; export default function SharedImageList({ currentImages, handleDelete, handleDownload, setImageDetails, openPreview }) { return ( &lt;div className="flex flex-col space-y-2"&gt; {currentImages.map((image) =&gt; ( &lt;div key={image.key} className="flex items-center w-full bg-gray-800 p-4 rounded-md transition-transform hover:scale-110" &gt; &lt;ImageThumbnail imageUrl={image.object_url} listStyle={1} /&gt; &lt;div className="ml-2 flex-1"&gt; &lt;p className="text-white text-sm font-medium break-words"&gt; {image.key.split('/').pop()} &lt;/p&gt; &lt;/div&gt; &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1" onClick={() =&gt; openPreview(image.object_url)} &gt; &lt;FiEye size={16} className="text-gray-600" /&gt; &lt;/button&gt; &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1" onClick={() =&gt; handleDownload(image.key, null)} &gt; &lt;FiDownload size={16} className="text-gray-600" /&gt; &lt;/button&gt; {image.isEditAccess && ( &lt;button className="bg-white p-1 rounded-full shadow-md hover:bg-red-100" onClick={() =&gt; handleDelete(image.key, setImageDetails, null)} &gt; &lt;FiTrash2 size={16} className="text-red-600" /&gt; &lt;/button&gt; )} &lt;/div&gt; ))} &lt;/div&gt; ); } 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(() =&gt; { fetchSharedImages(userId, setImageDetails, setLoading); }, [userId]); const indexOfLastImage = currentPage * imagesPerPage; const indexOfFirstImage = indexOfLastImage - imagesPerPage; const currentImages = imageDetails.slice(indexOfFirstImage, indexOfLastImage); const nextPage = () =&gt; { if (indexOfLastImage &lt; imageDetails.length) setCurrentPage(currentPage + 1); }; const prevPage = () =&gt; { if (currentPage &gt; 1) setCurrentPage(currentPage - 1); }; const toggleMenu = (index, e) =&gt; { e.preventDefault(); e.stopPropagation(); setOpenMenuIndex(openMenuIndex === index ? null : index); }; const openPreview = (imageUrl) =&gt; setSelectedImage(imageUrl); const closePreview = () =&gt; setSelectedImage(null); return ( &lt;div className="min-h-screen bg-black flex flex-col items-center p-6 relative"&gt; {loading ? ( &lt;div className="flex justify-center items-center h-screen"&gt; &lt;div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"&gt;&lt;/div&gt; &lt;/div&gt; ) : ( &lt;&gt; {imageDetails.length === 0 ? ( &lt;div className="flex items-center justify-center h-screen"&gt; &lt;p className="text-white text-lg font-semibold text-center"&gt; No Shared files found. &lt;/p&gt; &lt;/div&gt; ) : ( &lt;&gt; &lt;button className="absolute top-4 right-4 bg-blue-700 text-white px-4 py-2 rounded shadow-lg flex items-center space-x-2" onClick={() =&gt; setViewMode(viewMode === "grid" ? "list" : "grid")} &gt; {viewMode === "grid" ? &lt;FiList size={20} /&gt; : &lt;FiGrid size={20} /&gt;} &lt;/button&gt; &lt;div className="max-w-screen-lg w-full mt-12"&gt; {viewMode === "grid" ? ( &lt;SharedImageGrid currentImages={currentImages} toggleMenu={toggleMenu} handleDelete={handleDelete} handleDownload={handleDownload} setImageDetails={setImageDetails} setOpenMenuIndex={setOpenMenuIndex} openMenuIndex={openMenuIndex} openPreview={openPreview} /&gt; ) : ( &lt;SharedImageList currentImages={currentImages} handleDelete={handleDelete} handleDownload={handleDownload} setImageDetails={setImageDetails} openPreview={openPreview} /&gt; )} &lt;/div&gt; {imageDetails.length &gt; imagesPerPage && ( &lt;div className="flex mt-6 justify-center items-center space-x-4"&gt; &lt;button onClick={prevPage} disabled={currentPage === 1} className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200" &gt; Previous &lt;/button&gt; &lt;span className="text-lg font-semibold text-center w-20"&gt; Page {currentPage} &lt;/span&gt; &lt;button onClick={nextPage} disabled={indexOfLastImage &gt;= imageDetails.length} className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200" &gt; Next &lt;/button&gt; &lt;/div&gt; )} &lt;/&gt; )} &lt;/&gt; )} {selectedImage && ( &lt;div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-80 z-50"&gt; &lt;div className="relative"&gt; &lt;button className="absolute top-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100" onClick={closePreview} &gt; &lt;FiX size={20} className="text-gray-600" /&gt; &lt;/button&gt; &lt;img src={selectedImage} alt="Preview" className="max-w-full max-h-[50vh] rounded-lg shadow-lg" /&gt; &lt;/div&gt; &lt;/div&gt; )} &lt;/div&gt; ); } 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) =&gt; { const selectedFile = e.target.files[0]; setFile(selectedFile); }; const handleSubmit = async (e) =&gt; { 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 ( &lt;div className="flex items-center justify-center min-h-screen bg-black"&gt; &lt;div className="bg-white p-8 rounded-lg shadow-lg w-96 text-center flex flex-col items-center text-black"&gt; &lt;h1 className="text-2xl font-semibold mb-4 text-black"&gt;Upload Image&lt;/h1&gt; &lt;p className="text-sm text-black mb-4"&gt; NOTE: Only .png, .jpg, and .jpeg files are allowed. &lt;/p&gt; &lt;form onSubmit={handleSubmit} className="flex flex-col items-center space-y-4 w-full"&gt; &lt;div className="flex justify-center w-full"&gt; &lt;input type="file" onChange={handleFileChange} className="border p-2 rounded w-full text-center" accept="image/png, image/jpg, image/jpeg" /&gt; &lt;/div&gt; &lt;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" }`} &gt; {isUploading ? "Uploading..." : "Upload"} &lt;/button&gt; &lt;/form&gt; &lt;/div&gt; &lt;/div&gt; ); } Note: Ensure you provide your bucket's name in line 19. 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( &lt;React.StrictMode&gt; &lt;Router&gt; &lt;App /&gt; &lt;/Router&gt; &lt;/React.StrictMode&gt; ); // 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; &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt; &lt;meta charset="utf-8" /&gt; &lt;link rel="icon" href="%PUBLIC_URL%/favicon.ico" /&gt; &lt;meta name="viewport" content="width=device-width, initial-scale=1" /&gt; &lt;meta name="theme-color" content="#000000" /&gt; &lt;meta name="description" content="Web site created using create-react-app" /&gt; &lt;link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /&gt; &lt;!-- 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/ --&gt; &lt;link rel="manifest" href="%PUBLIC_URL%/manifest.json" /&gt; &lt;!-- 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`. --&gt; &lt;title&gt;PhotoStore App&lt;/title&gt; &lt;script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.0/catalystWebSDK.js"&gt;&lt;/script&gt; &lt;script src="/__catalyst/sdk/init.js"&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;noscript&gt;You need to enable JavaScript to run this app.&lt;/noscript&gt; &lt;div id="root"&gt;&lt;/div&gt; &lt;!-- 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 &lt;body&gt; tag. To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --&gt; &lt;/body&gt; &lt;/html&gt; The frontend component for the application is now configured. Let's quickly go through the working of the function and client components of the application: 1. 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. 2. 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. 3. When the end-user tries 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. 4. 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. 5. 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. **View** access only allows the shared member to download the image, and **Edit** access allows them to update, download or delete the image. -------------------------------------------------------------------------------- title: "Test Your Application" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.141Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/test-app/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - CLI Serve Resources (/en/cli/v1/serve-resources/introduction/) - Stratus Web SDK (/en/sdk/web/v4/cloud-scale/stratus/overview/) - Stratus REST API (/en/api/code-reference/cloud-scale/stratus/get-all-buckets/#GetAllBuckets) -------------------------------------------------------------------------------- # Test Your Application Before you deploy the application to the remote console, you can test the application on a local server and check if everything works using the Catalyst CLI. To serve the Catalyst project locally, execute the following command from your project directory: catalyst serve The application will be ideally served on the default port 3001. <br /> Note: Every time you access the home page or any of the sub-pages of your application, the CLI will display a live log of the URL accessed, along with the HTTP request method. <br /> Note: Ensure that you have whitelisted your localhost URL using the Bucket CORS feature as demonstrated in this step. You can now open the application’s localhost URL in a browser to access the PhotoStoreApp application. <br /> ### Test Case 1: Login Functionality The first page of the application will be the Login screen. <br /> Once you complete the login process, you will be added to the application, and be able to access all the applications' functionality. <br /> You can manage end-user sign ups using the User Management feature present in the *Authentication* component. <br /> ### Test Case 2: Upload Functionality You can try uploading an image from your local system to the application by clicking the **Upload** button. <br /> Click **Upload** once you've chosen a file from your local system. <br /> The image will be uploaded to the bucket and rendered in the application. <br /> You can verify the upload in the bucket. <br /> To view the object, you can either click the **Goto Preview** button or you can execute **Object URL** to view the object. <br /> ### Test Case 3: View Functionality You can click the **hamburger menu** icon to view the uploaded images in the list view. <br /> You can use the **hamburger-icon** again to switch the view back to thumbnails. <br /> You can also click the required image to view it in its original size. <br /> ### Test Case 4: Delete and Download Functionality You have the option to download or delete an uploaded image. To delete an image, you can click the **delete icon** in *list view* or click the **ellipsis icon** and click **Delete** in the *thumbnail view*. <br /> The image will be deleted. <br /> Similarly, to download an image, you can click the **download icon** in *list view* or click the **ellipsis icon** and click **Download** in the *thumbnail view*. <br /> The required image will be downloaded to your local system. <br /> ### Test Case 5: Share Functionality You can choose to share images with other registered end-users. Click the **share** icon to share your required image with other registered end-users. <br /> You can also choose to share the image with the registered user in **View** or **Edit** access. <br /> **View Access** will only allow the end-user to download the image. **Edit Access** allows the end-user to update, delete and download the image. Once you've shared an image to the end-user the end-user will be able to view the shared image in their Shared **Gallery**. <br /> You can use the **Manage Shared Details** section to update access level of images. <br /> Use the **Access Type** drop-down to update the access level for the required image and click **Update** to render the changes. <br /> You can also use the **Revoke Access** toggle and click **Update** to remove all access to the required image. <br /> The shared user will no longer be able to view the previously shared image. If the application is working as defined in this page, then you can deploy the application. -------------------------------------------------------------------------------- title: "Deploy Your Application" description: "Build a React gallery app to upload images to a bucket in Stratus, and shre these images with other registered users. Host the app on AppSail and sync with GitHub and deploy the app usiong Pipelines." last_updated: "2026-06-16T09:55:12.142Z" source: "https://docs.catalyst.zoho.com/en/tutorials/photo-store-app/nodejs/deploy-app/" service: "All Services" related: - Catalyst Stratus (/en/cloud-scale/help/stratus/introduction/) - Catalyst Pipelines (/en/pipelines/help/pipelines/introduction/) - Configure Pipelines Flow (/en/pipelines/help/catalyst-pipelines.yaml/implementation/) - CLI Deploy Resources (/en/cli/v1/serve-resources/introduction/) - Stratus Web SDK (/en/sdk/web/v4/cloud-scale/stratus/overview/) - Stratus REST API (/en/api/code-reference/cloud-scale/stratus/get-all-buckets/#GetAllBuckets) -------------------------------------------------------------------------------- # Deploy Your Application We're going to be using the Catalyst Pipelines service to deploy and maintain your application in both the Catalyst console and your required GitHub repository. Using the Pipelines service allows us to use a platform dependent package like Sharp. We require the Sharp package to convert and render the uploaded images as thumbnails in the client. Catalyst runs on a Linux machine, in order to use the sharp package and host it on Catalyst, you could install Linux based package library for sharp package and deploy the application. Alternatively, you could easily deploy the application using the Catalyst Pipelines service and all the required dependencies will be handled automatically. Using the Pipelines service allows you to seamlessly sync your application files between your GitHub repository and the Catalyst console. To deploy the application using Pipelines: 1. Click the **Pipelines icon** present in the Console's sidebar to access the *Pipelines* service. <br /> 2. Click **Create Pipelines** <br /> 3. Name your pipeline as "PhotoStore-pipeline" and select **GitHub** as the integration source. <br /> Info: We are demonstrating the Pipelines service using GitHub. You can also use the GitLab and BitBucket sources if your requirement demands it. 4. Add your GitHub account from the drop-down. If you haven't added it previously, you can click the **+Add Account** button and follow the login steps to add the GitHub account to the *Catalyst Pipelines* service. <br /> 5. Select your required GitHub *Organization*, and the repository you had earlier created to maintain the application files. <br /> 6. You will now be directed to the **code-view** of the catalyst-pipelines.yaml file. <br /> You can also click the **Builder** tab to configure the .yaml file using the UI. <br /> 7. Copy and paste the following snippet to define the sequence of stages and configure your Pipeline's flow. version: 1 jobs: deploy: steps: - cd photo-store-app - npm install - npm run build - cd .. - cd server - npm install - cd .. - cd scripts - npm install - cd .. - node ./scripts/filesHelper.js -c ./server/ ./build/server/ - node ./scripts/filesHelper.js -c ./photo-store-app/build/ ./build/photo-store-app/ - npm install -g zcatalyst-cli@beta - catalyst deploy stages: - name: build jobs: - deploy 8. Click **Commit** to execute your Pipeline. <br /> 9. Provide a commit message and click **Commit**. <br /> You will be directed to the overview section, where the status of the pipeline will be displayed along with other general details. <br /> With this step, the Pipeline has been configured and it will automatically begin its execution. ### Add Global Variables We will be adding two Global Variables for this Pipeline. * CATALYST_TOKEN: This variable needs to be added to point your local CLI to the Pipeline. Steps to generate the required token are listed below. Note: This variable should be mandatorily added if you are using the CLI for deploying using the Pipeline service. * CI: You will need to set the value as False. This will ensure that the Pipeline will not fail even if there happens to be warnings in your code. Info: Learn more about Global Variables. #### Generate Catalyst Token We need to generate a token to allow Catalyst CLI to execute CLI commands from your local system to the Pipeline. To generate a token: 1. Execute the following command in your CLI catalyst token:generate <br /> 2. Navigate to the URL displayed in your CLI, enter the verification code generated in the CLI, and click **Verify**. <br /> Once the CLI is verified, the token will be generated. <br /> Info: Learn more about generating tokens Now, let's begin adding the required global variables. To add Global Variables: 1. Click **Global Variables**. <br /> 2. Enter CATALYST_TOKEN as key and the generated token as the value. Click the **+ icon** to add the next token. <br /> 3. Enter CI as the key and False as its value. Click **Save** to add the variables. <br /> The required variables have been added. You can check the status of the pipeline by clicking on the executed pipeline for more details. <br /> Click the **Advanced** tab for more details on the process. <br /> In a few moments, the pipeline will be successfully executed. <br /> The application will now be in sync in GitHub and the Catalyst console. <br /> You will now be able to access the deployed version of the application using the **App URL** generated in the AppSail component present in the *Catalyst Serverless* service. <br /> You can execute this URL in the browser to access the deployed version of your application. <br />