# 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-03-18T07:41:08.688Z" 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. {{%link href="/en/serverless/" %}}{{%bold%}}Catalyst Serverless{{%/bold%}}{{%/link%}}: - {{%link href="/en/serverless/help/appsail/introduction/" %}}{{%bold%}}AppSail{{%/bold%}}{{%/link%}}: You will be bundling the front end and client code together and deploying it on *AppSail*, to host the entire application. 2. {{%link href="/en/cloud-scale/" %}}{{%bold%}}Catalyst CloudScale{{%/bold%}}{{%/link%}}: - {{%link href="/en/cloud-scale/help/authentication/introduction/" %}}{{%bold%}}Authentication{{%/bold%}}{{%/link%}}: To add end users to the application. You will be implementing the {{%link href="/en/cloud-scale/help/authentication/native-catalyst-authentication/hosted-authentication-type/introduction/" %}}Hosted Authentication{{%/link%}} type to handle the authentication requirement. - {{%link href="/en/cloud-scale/help/data-store/introduction/" %}}{{%bold%}}Data Store{{%/bold%}}{{%/link%}}: To store the various details of the images that are present in the application. - {{%link href="/en/cloud-scale/help/zcql/introduction/" %}}{{%bold%}}ZCQL{{%/bold%}}{{%/link%}}: To post and fetch data from the *Data Store*. - {{%link href="/en/cloud-scale/help/stratus/introduction/" %}}{{%bold%}}Stratus{{%/bold%}}{{%/link%}}: To store the images that are being uploaded through the application. 3. {{%link href="/en/pipelines/help/pipelines/introduction/" %}}{{%bold%}}Catalyst Pipelines{{%/bold%}}{{%/link%}}: To maintain the entire project seamlessly in your {{%link href="https://github.com/" %}}GitHub{{%/link%}} repository without ever missing a development lifecycle. More importantly, using Pipelines to deploy this Catalyst application allows us to use the {{%link href="https://www.npmjs.com/package/sharp" %}}Sharp{{%/link%}}, 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 {{%link href="https://console.catalyst.zoho.com/" %}}Catalyst web console{{%/link%}} and the {{%link href="/en/cli/v1/cli-command-reference/" %}}Catalyst Command Line Interface{{%/link%}} (CLI) to build this application. {{%note%}}{{%bold%}}Note:{{%/bold%}} 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.{{%/note%}} ### 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 {{%link href="/en/cloud-scale/help/stratus/objects/introduction/#object-url" %}}Object URL{{%/link%}}. * 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-03-18T07:41:08.697Z" 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. {{%bold%}}Catalyst CLI{{%/bold%}}: 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 {{%link href="/en/getting-started/installing-catalyst-cli/" %}}{{%bold%}}Install Catalyst CLI help page{{%/bold%}}{{%/link%}} 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 {{%link href="/en/cli/v1/login/login-from-cli/" %}}{{%bold%}}CLI Login help page{{%/bold%}}{{%/link%}} for the steps to log in from Catalyst CLI and the various options available for it. 2. {{%bold%}}Any IDE tool for Node.js and client code development{{%/bold%}}: 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. {{%bold%}}A {{%link href="https://github.com/" %}}GitHub{{%/link%}} account{{%/bold%}}: To store and manage your application files in your personal repository. {{%info image="/images/tutorials/todo-list/vscode.png"%}}If you are a Visual Studio Code IDE user, you can install the {{%bold%}}Catalyst Tools{{%/bold%}} extension, and use your IDE itself in place of the CLI. You can find more details about the Catalyst VS Code extension from this {{%link href="/en/catalyst-extensions/vs-code-extension/introduction/" %}}help section{{%/link%}}.{{%/info%}} -------------------------------------------------------------------------------- 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-03-18T07:41:08.697Z" 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 {{%link href="/en/getting-started/catalyst-projects/#creating-a-catalyst-project" %}}create a Catalyst project{{%/link%}} in the Catalyst console. 1. Log in to the {{%link href="https://console.catalyst.zoho.com/baas/index" %}}Catalyst console{{%/link%}} 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-03-18T07:41:08.697Z" 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 {{%link href="/en/cloud-scale/help/data-store/introduction/" %}}Data Store{{%/link%}}. 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>{{%badge%}}{{%bold%}}UserName{{%/bold%}}{{%/badge%}}</td> <td>{{%badge%}}Var Char{{%/badge%}}</td> <td>To store the details of the user accessing the application.</td> </tr> <tr> <td>{{%badge%}}{{%bold%}}BucketPath{{%/bold%}}{{%/badge%}}</td> <td>{{%badge%}}Var Char{{%/badge%}}</td> <td>To store the path of the required image (object).</td> </tr> <tr> <td>{{%badge%}}{{%bold%}}UserZuid{{%/bold%}}{{%/badge%}}</td> <td>{{%badge%}}Var Char{{%/badge%}}</td> <td>To store the unique user ID of the end user signed up to your application.</td> </tr> <tr> <td>{{%badge%}}{{%bold%}}IsUpdate{{%/bold%}}{{%/badge%}}</td> <td>{{%badge%}}Boolean{{%/badge%}}</td> <td>To store shared image access type as a Boolean: True for Edit, False for View.</td> </tr> <tr> <td>{{%badge%}}{{%bold%}}SharedBy{{%/bold%}}{{%/badge%}}</td> <td>{{%badge%}}Var Char{{%/badge%}}</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%}}{{%bold%}}Note:{{%/bold%}} Ensure that you enter the name exactly as instructed because the application's code contains the same name.{{%/note%}} The table will be created. <br /> Now, let’s create the four required columns. {{%info%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cloud-scale/help/data-store/introduction/" %}}Learn more about the various supported data types and properties of a column{{%/link%}}.{{%/info%}} 4. Click **New Column** in the *Schema View* section. <br /> 5. Enter the column's name as "{{%badge%}}UserName{{%/badge%}}". Select **Var Char** as its datatype, enter **Max Length** as {{%badge%}}100{{%/badge%}}, 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 "{{%badge%}}BucketPath{{%/badge%}}". Select **Var Char** as the data type and enter {{%badge%}}255{{%/badge%}} as the **Max Length**. Click **Create**. <br /> 7. Click the **New Column** button again to create the third column. Name the column "{{%badge%}}UserZuid{{%/badge%}}". Select **Var Char** as the data type and enter {{%badge%}}50{{%/badge%}} as the **Max Length**. Click **Create**. <br /> 8. Click the **New Column** button again to create the fourth column. Name the column "{{%badge%}}SharedBy{{%/badge%}}". Select **Var Char** as the data type and enter {{%badge%}}100{{%/badge%}} 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-03-18T07:41:08.698Z" 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 {{%link href="/en/cloud-scale/help/authentication/native-catalyst-authentication/introduction/" %}}Native Catalyst Authentication{{%/link%}} option. <br /> 2. Select **Hosted Authentication** and click **Next**. <br /> {{%info%}}{{%bold%}}Info:{{%/bold%}}<br /> * {{%link href="/en/cloud-scale/help/authentication/native-catalyst-authentication/hosted-authentication-type/introduction/" %}}Learn more about Hosted Authentication type{{%/link%}}. * {{%link href="/en/cloud-scale/help/authentication/native-catalyst-authentication/embedded-authentication/introduction/" %}}Learn more about Embedded Authentication type{{%/link%}}. * {{%link href="/en/cloud-scale/help/authentication/third-party-authentication/introduction/" %}}Learn more about Third-party Authentication type{{%/link%}}.{{%/info%}} 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%}}{{%bold%}}Note:{{%/bold%}} You can use the {{%italics%}}Preview{{%/italics%}} section to simultaneously view the style of your {{%bold%}}Sign In{{%/bold%}}, {{%bold%}}Sign Up{{%/bold%}}, {{%bold%}}Password Reset{{%/bold%}}, and {{%bold%}}Confirm Password{{%/bold%}} login elements. However, to view the {{%bold%}}Sign Up{{%/bold%}} page, you need to enable {{%bold%}}Public Signup{{%/bold%}}. The steps involved in enabling {{%bold%}}Public Signup{{%/bold%}} are detailed below.{{%/note%}} 4. Click the toggle button to enable *Public Signup*. <br /> Read the information pop-up and click **Yes, Proceed**. <br /> {{%info%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cloud-scale/help/authentication/public-signup/" %}}Learn more about Public Signup{{%/link%}}.{{%/info%}} 5. You can enable any of the **Social Logins** according to your preferences. <br /> {{%info%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cloud-scale/help/authentication/social-logins/configuring-social-logins/" %}}Learn the steps to generate {{%badge%}}Client ID{{%/badge%}} and {{%badge%}}Client Secret{{%/badge%}} credentials required to configure other Social Logins{{%/link%}}.{{%/info%}} 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%}}{{%bold%}}Info:{{%/bold%}} 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 /> * {{%link href="/en/cloud-scale/help/authentication/authentication-types/" %}}Learn more about Authentication Types{{%/link%}}. * {{%link href="/en/cloud-scale/help/authentication/user-management/introduction/" %}}Learn more about User Management{{%/link%}}. * {{%link href="/en/cloud-scale/help/authentication/whitelisting/introduction/" %}}Learn more about Whitelisting{{%/link%}}. * {{%link href="/en/cloud-scale/help/authentication/email-templates/introduction/" %}}Learn more about Email Templates{{%/link%}}.{{%/info%}} -------------------------------------------------------------------------------- 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-03-18T07:41:08.698Z" 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 {{%link href="/en/cloud-scale/help/stratus/buckets/name-bucket/" %}}Bucket{{%/link%}} in {{%link href="/en/cloud-scale/help/stratus/introduction/" %}}Stratus{{%/link%}}. 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%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cloud-scale/help/stratus/buckets/name-bucket/#ideal-practices-to-name-a-bucket" %}}Learn more about the bucket naming protocols{{%/link%}}.{{%/info%}} The bucket will be created, and Catalyst will have generated a secure {{%link href="/en/cloud-scale/help/stratus/buckets/create-bucket/#bucket-url" %}}Bucket URL{{%/link%}} 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%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cloud-scale/help/stratus/stratus-permissions/" %}}Learn more about the syntax to define permissions{{%/link%}}. <br/><br />{{%bold%}}Make sure to go through this document and the examples illustrated within to ensure you employ the right permissions for the bucket correctly{{%/bold%}}.{{%/info%}} 4. Copy the JSON snippet below and replace it in place of the default permission defined in the console and click **Update**. {{%panel_without_adjustment class="language-json line-numbers" header="Bucket Permission.json" footer="button" scroll="set-scroll" %}}{ "rules": [ { "rule_id": "PublicBucket", "condition": { "user": { "auth_type": "public" } }, "allowed_actions": [ "PutObject", "DeleteObject", "GetObject" ], "paths": [ "YOUR_BUCKET_NAME::/photos/*" ], "effect": "allow" } ], "version": "v1" } {{%/panel_without_adjustment%}} <br /> {{%note%}}{{%bold%}}Notes:{{%/bold%}}<br /> * Ensure you provide your bucket name in line {{%badge%}}{{%bold%}}16{{%/bold%}}{{%/badge%}}. * The {{%badge%}}Photos{{%/badge%}} path will be automatically created when you upload an image through the client.{{%/note%}} 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-03-18T07:41:08.699Z" 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 {{%link href="/en/cli/v1/project-directory-structure/introduction/" %}}Project Directory Structure help page{{%/link%}}. You can learn about initializing a project in detail from the {{%link href="/en/cli/v1/cli-command-reference/" %}}CLI help documentation{{%/link%}}. 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: {{%cli%}}catalyst init{{%/cli%}} 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%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/getting-started/catalyst-organizations/#access-the-multi-org-portal" %}}Learn more about Catalyst’s multi-org portal feature{{%/link%}}{{%/info%}}. 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 {{%link href="/en/cli/v1/project-directory-structure/catalyst-json/" %}}{{%badge%}}{{%bold%}}catalyst.json{{%/bold%}}{{%/badge%}}{{%/link%}} file will be created. <br /> 5. Initialize an AppSail service in your project directory by executing the following CLI command: {{%cli%}}catalyst appsail:add{{%/cli%}} {{%note%}}{{%bold%}}Note:{{%/bold%}} In this tutorial, we are using the {{%badge%}}add{{%/badge%}} command to initialize the AppSail service to showcase alternative {{%link href="/en/cli/v1/cli-command-reference/#commands" %}}Catalyst CLI commands{{%/link%}} to initialize the AppSail service.{{%/note%}} 6. The CLI will prompt you to choose between {{%link href="/en/serverless/help/appsail/catalyst-managed-runtimes/key-concepts/" %}}Catalyst-Managed Runtime{{%/link%}} and {{%link href="/en/serverless/help/appsail/custom-runtimes/container-registry-services/" %}}Docker Image{{%/link%}}. Because you are creating this project from one of the Catalyst-Managed Runtime, select **Catalyst-Managed Runtime** and click **Enter**. <br/> 7. Enter "{{%badge%}}photo-store-app{{%/badge%}}" as the name of the AppSail service. <br /> 8. Enter "{{%badge%}}{{%bold%}}.{{%/bold%}}{{%/badge%}}" 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: {{%cli%}}npx create-react-app photo-store-app{{%/cli%}} <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 {{%badge%}}PhotostoreApp/photo-store-app{{%/badge%}} 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-03-18T07:41:08.699Z" 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 {{%link href="/en/serverless/help/appsail/introduction/" %}}AppSail{{%/link%}} service to the console, you need to add the following files and code to your project directory: - Create a folder named {{%badge%}}{{%bold%}}server{{%/bold%}}{{%/badge%}} in your project root directory, and create the following files in them: - {{%badge%}}{{%bold%}}index.js{{%/bold%}}{{%/badge%}}: This file will act as the proxy server to server your compiled react files. - Create a folder named {{%badge%}}{{%bold%}}scripts{{%/bold%}}{{%/badge%}}, and add the following files to it: - {{%badge%}}{{%bold%}}filesHelper.js{{%/bold%}}{{%/badge%}}: This file will be used to copy the files from the {{%badge%}}server{{%/badge%}} and {{%badge%}}photo-store-app{{%/badge%}} directories to the AppSail build path. - Create a folder named {{%badge%}}{{%bold%}}build{{%/bold%}}{{%/badge%}} 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. - {{%badge%}}{{%bold%}}server{{%/bold%}}{{%/badge%}} - {{%badge%}}{{%bold%}}photo-store-app{{%/bold%}}{{%/badge%}} The application's directory should now appear in the following manner: <br /> Next navigate to the {{%badge%}}PhotoStoreApp/server/{{%/badge%}} directory and perform the following steps: 1. Run the following CLI command from your terminal: {{%cli%}}npm init{{%/cli%}} <br /> 2. Run the following command to install the *Express* package to handle HTTP requests and responses. {{%cli%}}npm install express --save{{%/cli%}} <br /> This will also create a {{%badge%}}node_modules{{%/badge%}} folder in the {{%badge%}}PhotoStoreApp/server{{%/badge%}} directory. 3. Add the following code to your {{%badge%}}index.js{{%/badge%}} file present in the {{%badge%}}PhotoStoreApp/server/{{%/badge%}} directory. {{%panel_with_adjustment class="language-js line-numbers" header="index.js" footer="button" scroll="set-scroll" %}}'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}`); }); {{%/panel_with_adjustment%}} <br /> {{%note%}}{{%bold%}}Note:{{%/bold%}} You need to use the environment variable '{{%badge%}}{{%bold%}}X_ZOHO_CATALYST_LISTEN_PORT{{%/bold%}}{{%/badge%}}' to listen to the port configured by Catalyst.{{%/note%}} 4. Next, navigate to the {{%badge%}}{{%bold%}}scripts{{%/bold%}}{{%/badge%}} folder present in the {{%badge%}}PhotoStoreApp/scripts{{%/badge%}} directory and execute the following command: {{%cli%}}npm init{{%/cli%}} Ensure you enter the defaults, and this command will create a {{%badge%}}package.json{{%/badge%}} file. <br /> 5. Execute the following command in your {{%badge%}}scripts{{%/badge%}} folder to install the required packages: {{%cli%}}npm install path util fs{{%/cli%}} <br /> This command will install the following packages: - {{%badge%}}{{%bold%}}path{{%/bold%}}{{%/badge%}}: This package will be used to handle file and directory paths. - {{%badge%}}{{%bold%}}util{{%/bold%}}{{%/badge%}}: This package is required to use utilities like {{%badge%}}promisify{{%/badge%}}, {{%badge%}}inherits{{%/badge%}}, and so on. - {{%badge%}}{{%bold%}}fs{{%/bold%}}{{%/badge%}}: This package is required to implement file system operations. 6. Copy the following code and paste it in the {{%badge%}}filesHelper.js{{%/badge%}} file present in the {{%badge%}}PhotoStoreApp/scripts{{%/badge%}} directory. {{%panel_with_adjustment class="language-js line-numbers" header="filesHelper.js" footer="button" scroll="set-scroll" %}}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; } } {{%/panel_with_adjustment%}} Now, navigate to the {{%badge%}}{{%bold%}}app-config.json{{%/bold%}}{{%/badge%}} file present in the project directory {{%badge%}}PhotoStoreApp/app-config.json{{%/badge%}} and paste the following code. {{%panel_with_adjustment class="language-json line-numbers" header="app-config.json" footer="button" scroll="set-scroll" %}}{ "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" } {{%/panel_with_adjustment%}} {{%note%}}{{%bold%}}Note{{%/bold%}}: You need to include the the following key value pairs in the {{%badge%}}app-config.json{{%/badge%}} file: * {{%badge%}}catalyst_auth{{%/badge%}} key's value needs to be set as true, to enable {{%link href="/en/cloud-scale/help/authentication/introduction/" %}}Catalyst Authentication{{%/link%}} for the AppSail service. * You need to set the value of {{%badge%}}login_redirect{{%/badge%}} key as {{%badge%}}index.html{{%/badge%}} to ensure proper redirection after authentication process.{{%/note%}} ### 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: {{%cli%}}catalyst serve{{%/cli%}} <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 {{%badge%}}{{%bold%}}localhost:3001{{%/bold%}}{{%/badge%}}. {{%info%}}{{%bold%}}Info:{{%/bold%}} 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.{{%/info%}} Once you ensure the application is running error free, you can {{%link href="/en/cli/v1/deploy-resources/introduction/" %}}deploy the application{{%/link%}} to the Catalyst console using the following Catalyst CLI command: {{%cli%}}catalyst deploy{{%/cli%}} <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%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cloud-scale/help/stratus/stratus-config/bucket-cors/" %}}Learn more about Bucket CORS{{%/link%}}{{%/info%}} 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%}}{{%bold%}}Note:{{%/bold%}} Going ahead, we are going to implement the {{%link href="/en/pipelines/help/pipelines/introduction/" %}}Pipelines{{%/link%}} service to deploy the application and sync the GitHub project with the Catalyst console.{{%/note%}} -------------------------------------------------------------------------------- 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-03-18T07:41:08.701Z" 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 {{%badge%}}PhotoStoreApp/server{{%/badge%}} directory and install the following packages using the command shown below: - **Express**: The {{%link href="https://www.npmjs.com/package/express" %}}{{%badge%}}express{{%/badge%}}{{%/link%}} package is required to handle HTTP requests. - **Sharp**: The {{%link href="https://www.npmjs.com/package/sharp" %}}{{%badge%}}sharp{{%/badge%}}{{%/link%}} package is required to display the uploaded images as in varying sizes including thumbnails in the application. - **Multer**: The {{%link href="https://www.npmjs.com/package/multer" %}}{{%badge%}}multer{{%/badge%}}{{%/link%}} package is required to handle file uploads in Express applications. - {{%link href="/en/sdk/nodejs/v2/overview/" %}}{{%bold%}}Catalyst SDK{{%/bold%}}{{%/link%}}: To use the required SDK methods in the AppSail service. {{%cli%}}npm install express path zcatalyst-sdk-node@3.0.0-beta.3 multer sharp{{%/cli%}} <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 {{%badge%}}PhotoStoreApp/server/{{%/badge%}} directory: - {{%badge%}}{{%bold%}}index.js{{%/bold%}}{{%/badge%}}: 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. - {{%badge%}}{{%bold%}}helperfunction.js{{%/bold%}}{{%/badge%}}: This files contains the required helper functions to complete the processes detailed in the {{%badge%}}index.js{{%/badge%}} 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 {{%badge%}}index.js{{%/badge%}}</th> <th class="w30p">Functions present in the {{%badge%}}helperfunction.js{{%/badge%}}</th> </tr> </thead> <tbody> <tr> <td>To resize and upload images as thumbnails</td> <td>{{%badge%}}/convertToThumbnailAndUpload{{%/badge%}}</td> <td>{{%badge%}}uploadToStratus(){{%/badge%}}</td> </tr> <tr> <td>Get required images from the bucket</td> <td>{{%badge%}}/fetchAllImages{{%/badge%}}</td> <td>{{%badge%}}listMyObjects(){{%/badge%}}</td> </tr> <tr> <td>Get shared images alone</td> <td>{{%badge%}}/getSharedImages{{%/badge%}}</td> <td>{{%badge%}}listSharedObjects(){{%/badge%}}<br/>{{%badge%}}listMyObjects(){{%/badge%}}</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%}}{{%bold%}}Note:{{%/bold%}} Go through the code in this section to make sure you fully understand it.{{%/note%}} {{%panel_with_adjustment class="language-js line-numbers" header="index.js" footer="button" scroll="set-scroll" %}}'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}`); }); {{%/panel_with_adjustment%}} {{%note%}}{{%bold%}}Note:{{%/bold%}} Ensure you provide your bucket name in the lines {{%bold%}}20{{%/bold%}}, {{%bold%}}49{{%/bold%}}, and {{%bold%}}127{{%/bold%}}.{{%/note%}} {{%panel_with_adjustment class="language-js line-numbers" header="helper-function.js" footer="button" scroll="set-scroll" %}}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 }; {{%/panel_with_adjustment%}} -------------------------------------------------------------------------------- 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-03-18T07:41:08.702Z" 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 {{%link href="https://tailwindcss.com/" %}}Tailwind framework{{%/link%}} 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 {{%badge%}}PhotoStoreApp/photo-store-app/{{%/badge%}} directory and execute the following command <!-- {{%cli%}}npm install -D tailwindcss{{%/cli%}} <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. {{%cli%}}npm install -D tailwindcss@3.4.17{{%/cli%}} <br /> 2. Execute the following command to create a {{%badge%}}tailwind.config.js{{%/badge%}} file in your {{%badge%}}PhotoStoreApp/photo-store-app/{{%/badge%}} directory. {{%cli%}}npx tailwindcss init{{%/cli%}} A {{%badge%}}tailwind.config.js{{%/badge%}} 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 {{%badge%}}tailwind.config.js{{%/badge%}} file. {{%panel_without_adjustment class="language-js line-numbers" header="tailwind.config.js" footer="button" scroll="set-scroll" %}}/** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./src/**/*.{js,jsx,ts,tsx}" ], theme: { extend: {}, }, plugins: [], }; {{%/panel_without_adjustment%}} The {{%badge%}}tailwind.config.js{{%/badge%}} 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: * {{%badge%}}{{%bold%}}axios{{%/bold%}}{{%/badge%}}: This package lets you handle HTTP requests. * {{%badge%}}{{%bold%}}hamburger-react{{%/bold%}}{{%/badge%}}: This package lets you create a hamburger menu icon. * {{%badge%}}{{%bold%}}react-toastify{{%/bold%}}{{%/badge%}}: This package lets you display toast notifications. * {{%badge%}}{{%bold%}}react-router-dom{{%/bold%}}{{%/badge%}}: This package lets you handle client-side routing. * {{%badge%}}{{%bold%}}react-icons{{%/bold%}}{{%/badge%}}: This package lets you handle popular icon libraries. Navigate to the {{%badge%}}PhotoStoreApp/photo-store-app/{{%/badge%}} directory and execute the following command in your terminal to install the required packages. {{%cli%}}npm install axios hamburger-react react-toastify react-router-dom react-icons{{%/cli%}} <br /> ### Code Your Frontend We are going to add code in the following files: - In the {{%badge%}}PhotoStoreApp/photo-store-app/src/{{%/badge%}} directory: - {{%badge%}}{{%bold%}}App.js{{%/bold%}}{{%/badge%}}: 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: - {{%badge%}}{{%bold%}}service{{%/bold%}}{{%/badge%}} - {{%badge%}}{{%bold%}}ImageService.js{{%/bold%}}{{%/badge%}}: Contains helper functions which defines logic to handle the following functionalities: - {{%badge%}}{{%bold%}}fetchImages(){{%/bold%}}{{%/badge%}}: Fetches the required image. - {{%badge%}}{{%bold%}}fetchSharedImages(){{%/bold%}}{{%/badge%}}: Fetches the required shared image. - {{%badge%}}{{%bold%}}handleDelete(){{%/bold%}}{{%/badge%}}: Contains the logic to delete the required image from the application and the bucket. - {{%badge%}}{{%bold%}}handleDownload(){{%/bold%}}{{%/badge%}}: Contains the logic to download the required image from the bucket to the local system. - {{%badge%}}{{%bold%}}handleShareAction(){{%/bold%}}{{%/badge%}}: Contains the logic to invoke the {{%badge%}}/shareDetails{{%/badge%}} API to share the required image with another registered user. - {{%badge%}}{{%bold%}}handleUpdateSharedDetails(){{%/bold%}}{{%/badge%}}: Contains the logic to make update share permissions and image details. - {{%badge%}}{{%bold%}}ImageThumbnail.js{{%/bold%}}{{%/badge%}}: Contains the logic to render the uploaded images in thumbnail, full-scale, or list view. - {{%badge%}}{{%bold%}}pages{{%/bold%}}{{%/badge%}} - {{%badge%}}{{%bold%}}Home.js{{%/bold%}}{{%/badge%}}: 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. - {{%badge%}}{{%bold%}}ImageGrid.js{{%/bold%}}{{%/badge%}}: Contains the logic render the a grid of all the uploaded images with options to download, preview, share, and delete. - {{%badge%}}{{%bold%}}ImageList.js{{%/bold%}}{{%/badge%}}: Contains the logic to render a list view of all the uploaded images with options to download, preview, share, and delete. - {{%badge%}}{{%bold%}}Login.js{{%/bold%}}{{%/badge%}}: Contains the logic to direct end-users to the {{%link href="/en/cloud-scale/help/authentication/introduction/" %}}Catalyst Authentication{{%/link%}} login components. - {{%badge%}}{{%bold%}}Logout.js{{%/bold%}}{{%/badge%}}: Contains the logic to sign out end-users from the application. - {{%badge%}}{{%bold%}}SharedDetails.js{{%/bold%}}{{%/badge%}}: 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. - {{%badge%}}{{%bold%}}SharedImageGrid.js{{%/bold%}}{{%/badge%}}: Contains the logic to display the images in a responsive grid with options to download the required image provided the required permissions are granted. - {{%badge%}}{{%bold%}}SharedImageList.js{{%/bold%}}{{%/badge%}}: Contains the same logic as {{%badge%}}SharedImageGrid.js{{%/badge%}} except the images are displayed in list view with images rendered as thumbnails. - {{%badge%}}{{%bold%}}SharedImages.js{{%/bold%}}{{%/badge%}}: 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. - {{%badge%}}{{%bold%}}Upload.js{{%/bold%}}{{%/badge%}}: 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 {{%badge%}}/convertToThumbnailAndUpload{{%/badge%}} API to convert the image to a thumbnail, and complete the upload. - {{%badge%}}{{%bold%}}index.js{{%/bold%}}{{%/badge%}}: Will act as the entry point for the application. - {{%badge%}}{{%bold%}}index.css{{%/bold%}}{{%/badge%}}: Required to use tailwind for our application. - In the {{%badge%}}PhotoStoreApp/photo-store-app/public/{{%/badge%}} directory: - {{%badge%}}{{%bold%}}index.html{{%/bold%}}{{%/badge%}}: 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%}}{{%bold%}}Note:{{%/bold%}} Please go through the code in this section to make sure you fully understand it.{{%/note%}} Copy the code below and paste it into the respective files of your project using an IDE and save the files. {{%panel_with_adjustment class="language-js line-numbers" header="App.js" footer="button" scroll="set-scroll" %}}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; {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="ImageService.js" footer="button" scroll="set-scroll" %}}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" }); } }; {{%/panel_with_adjustment%}} {{%note%}}{{%bold%}}Note:{{%/bold%}} Ensure you provide your bucket name in the lines {{%bold%}}38{{%/bold%}}, and {{%bold%}}73{{%/bold%}}.{{%/note%}} {{%panel_with_adjustment class="language-js line-numbers" header="ImageThumbnail.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%note%}}{{%bold%}}Note:{{%/bold%}} Ensure you provide your bucket's URL in line {{%bold%}}5{{%/bold%}}.{{%/note%}} {{%panel_with_adjustment class="language-js line-numbers" header="Home.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="ImageGrid.js" footer="button" scroll="set-scroll" %}}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; ); }; {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="ImageList.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="Login.js" footer="button" scroll="set-scroll" %}}import { useEffect } from "react"; function Login() { useEffect(() =&gt; { window.location.href = `${window.origin}/__catalyst/auth/login`; }, []); return null; } export default Login; {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="Logout.js" footer="button" scroll="set-scroll" %}}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; } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="SharedDetails.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="SharedImageGrid.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="SharedImageList.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="SharedImages.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-js line-numbers" header="Upload.js" footer="button" scroll="set-scroll" %}}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; ); } {{%/panel_with_adjustment%}} {{%note%}}{{%bold%}}Note:{{%/bold%}} Ensure you provide your bucket's name in line {{%bold%}}19{{%/bold%}}.{{%/note%}} {{%panel_with_adjustment class="language-js line-numbers" header="index.js" footer="button" scroll="set-scroll" %}}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(); {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-css line-numbers" header="index.css" footer="button" scroll="set-scroll" %}}@tailwind base; @tailwind components; @tailwind utilities; {{%/panel_with_adjustment%}} {{%panel_with_adjustment class="language-xml line-numbers" header="index.html" footer="button" scroll="set-scroll" %}}&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; {{%/panel_with_adjustment%}} 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 {{%link href="/en/cloud-scale/help/authentication/native-catalyst-authentication/hosted-authentication-type/introduction/" %}}Hosted Authentication{{%/link%}}. 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 {{%badge%}}/fetchAllImages{{%/badge%}} 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 {{%link href="/en/sdk/web/v4/cloud-scale/stratus/upload-object/" %}}upload web SDK method{{%/link%}}. While the image is being uploaded, the {{%badge%}}/convertToThumbnailAndUpload{{%/badge%}} API endpoint is invoked and the images are converted to thumbnails and rendered in the client, and the uploaded image is stored in the "{{%badge%}}YOUR_BUCKET_NAME/photos/thumbnails/zuid of the logged in end-user{{%/badge%}}" {{%link href="/en/cloud-scale/help/stratus/objects/introduction/#path" %}}path{{%/link%}}. 4. The logic defined to handle the {{%link href="/en/cloud-scale/help/stratus/objects/manage-object/delete-object/" %}}delete{{%/link%}} and {{%link href="/en/cloud-scale/help/stratus/objects/manage-object/download-object/" %}}download{{%/link%}} operations are defined in the {{%badge%}}handleDownload(){{%/badge%}} and {{%badge%}}handleDelete(){{%/badge%}} functions. When the end-user initiates the download operation, the image will be downloaded to their local system using the {{%link href="/en/sdk/nodejs/v2/cloud-scale/stratus/download-object/" %}}download web SDK method{{%/link%}}. If the image is deleted from the application, it will also be deleted from Stratus using the {{%link href="/en/sdk/nodejs/v2/cloud-scale/stratus/delete-objects/" %}}delete web SDK method{{%/link%}}. 5. The logic to handle the share feature is defined in {{%badge%}}handleShareAction(){{%/badge%}}, which will invokes the {{%badge%}}/shareDetails{{%/badge%}} API to share the required image with another registered user. When the end-user initiates the share feature, the {{%badge%}}/getAllUsers{{%/badge%}} 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-03-18T07:41:08.715Z" 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 {{%link href="/en/cli/v1/serve-resources/introduction/" %}}test the application{{%/link%}} 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: {{%cli%}}catalyst serve{{%/cli%}} The application will be ideally served on the default port {{%badge%}}3001{{%/badge%}}. <br /> {{%note%}}{{%bold%}}Note:{{%/bold%}} 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.{{%/note%}} <br /> {{%note%}}{{%bold%}}Note:{{%/bold%}} Ensure that you have whitelisted your localhost URL using the Bucket CORS feature as demonstrated in {{%link href="/en/tutorials/photo-store-app/nodejs/config-appsail/#enable-bucket-cors" %}}this step{{%/link%}}.{{%/bold%}} 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 {{%link href="/en/cloud-scale/help/authentication/user-management/introduction/" %}}{{%bold%}}User Management{{%/bold%}}{{%/link%}} 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-03-18T07:41:08.724Z" 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 {{%link href="/en/pipelines/help/pipelines/introduction/" %}}Catalyst Pipelines{{%/link%}} 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 {{%link href="https://www.npmjs.com/package/sharp" %}}Sharp{{%/link%}}. 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 "{{%badge%}}PhotoStore-pipeline{{%/badge%}}" and select **GitHub** as the integration source. <br /> {{%info%}}{{%bold%}}Info:{{%/bold%}} We are demonstrating the Pipelines service using GitHub. You can also use the GitLab and BitBucket sources if your requirement demands it.{{%/info%}} 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 {{%badge%}}catalyst-pipelines.yaml{{%/badge%}} file. <br /> You can also click the **Builder** tab to configure the {{%badge%}}.yaml{{%/badge%}} file using the UI. <br /> 7. Copy and paste the following snippet to define the sequence of {{%link href="/en/pipelines/help/catalyst-pipelines.yaml/build-the-pipeline/stages/" %}}stages{{%/link%}} and {{%link href="/en/pipelines/help/catalyst-pipelines.yaml/implementation/" %}}configure your Pipeline's flow{{%/link%}}. {{%panel_with_adjustment class="language-xml line-numbers" header="catalyst-pipelines.yaml" footer="button" scroll="set-scroll" %}}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 {{%/panel_with_adjustment%}} 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. * {{%badge%}}{{%bold%}}CATALYST_TOKEN{{%/bold%}}{{%/badge%}}: This variable needs to be added to point your local CLI to the Pipeline. Steps to generate the required token are listed below. {{%note%}}{{%bold%}}Note:{{%/bold%}} This variable should be mandatorily added if you are using the CLI for deploying using the Pipeline service.{{%/note%}} * {{%badge%}}{{%bold%}}CI{{%/bold%}}{{%/badge%}}: You will need to set the value as {{%badge%}}False{{%/badge%}}. This will ensure that the Pipeline will not fail even if there happens to be warnings in your code. {{%info%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/pipelines/help/catalyst-pipelines.yaml/build-the-pipeline/variables/#global-variables" %}}Learn more about Global Variables{{%/link%}}.{{%/info%}} #### 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 {{%cli%}}catalyst token:generate{{%/cli%}} <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%}}{{%bold%}}Info:{{%/bold%}} {{%link href="/en/cli/v1/working-with-tokens/generate-token/" %}}Learn more about generating tokens{{%/link%}}{{%/info%}} Now, let's begin adding the required global variables. To add Global Variables: 1. Click **Global Variables**. <br /> 2. Enter {{%badge%}}CATALYST_TOKEN{{%/badge%}} as key and the generated token as the value. Click the **+ icon** to add the next token. <br /> 3. Enter {{%badge%}}CI{{%/badge%}} as the key and {{%badge%}}False{{%/badge%}} 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 />