--- title: "Configure the client directory" url: https://docs.catalyst.zoho.com/en/tutorials/workdrivesync/nodejs/configure-client-directory/ description: "Build a web application that synchronizes file uploads and deletions between two folders in the Catalyst Stratus and Zoho WorkDrive through APIs, using Catalyst functions and Event Listener" last_updated: 2026-08-28T07:59:38Z service: All Services type: tutorial related_links: - name: "Client Directory Structure" url: /en/cli/v1/project-directory-structure/client-directory --- # Configure the Client Directory Let's now configure the client component. The [client directory](https://docs.catalyst.zoho.com/en/cli/v1/project-directory-structure/client-directory) contains: * The `index.html` file that contains the HTML code for the frontend application * The `main.css` file that contains the CSS code * The `main.js` file that contains the JavaScript code * The `client-package.json` configuration file We will be coding `index.html`, `main.js` and `main.css`. > **Note:** Please go through the code in this section to make sure you fully understand it. We will discuss the code at the end of this section. Copy the code below and paste it in the respective files located in the `client/` directory of your project using an IDE and save the files. **index.html** ```xml <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Catalyst File Vault</title> <script src="https://cdn.tailwindcss.com"> </script> <link rel="stylesheet" href="main.css"> </head> <body class="min-h-screen bg-gradient-to-br from-[#DCE7FF] to-blue-100 text-gray-800 flex items-center justify-center p-4"> <main class="w-full max-w-6xl"> <div class="bg-white p-6 rounded-lg shadow-2xl border border-blue-100 animate-card-fade-in-scale"> <div class="flex flex-col sm:flex-row items-center justify-between mb-6 pb-4 border-b border-gray-200"> <button id="logout-button" class="bg-transparent border-2 border-red-500 text-red-600 hover:bg-red-100 hover:text-red-700 font-semibold py-2 px-5 rounded-full shadow hover:shadow-lg transition-all duration-300 flex items-center justify-center text-sm order-2 sm:order-1 mt-4 sm:mt-0"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"> </path> <polyline points="17 17 22 12 17 7"> </polyline> <line x1="22" y1="12" x2="12" y2="12"> </line> </svg> LOGOUT </button> <h1 class="text-4xl sm:text-5xl font-extrabold text-blue-700 text-center flex-grow order-1 sm:order-2">Catalyst File Vault</h1> <button id="upload-button" class="bg-transparent border-2 border-green-500 text-green-600 hover:bg-green-100 hover:text-green-700 font-semibold py-2 px-5 rounded-full shadow hover:shadow-lg transition-all duration-300 flex items-center justify-center text-sm order-3 sm:order-3 mt-4 sm:mt-0"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"> </path> <polyline points="17 8 12 3 7 8"> </polyline> <line x1="12" y1="3" x2="12" y2="15"> </line> </svg> UPLOAD FILE </button> <input type="file" id="file-input" class="hidden"> </div> <div class="overflow-x-auto"> <table class="min-w-full bg-white border border-gray-200 rounded-lg overflow-hidden"> <thead> <tr class="table-header-bg text-sm uppercase tracking-wider"> <th class="py-3 px-4 text-left font-bold">File Name</th> <th class="py-3 px-4 text-left font-bold">Uploaded Time</th> <th class="py-3 px-4 text-left font-bold">File Size</th> <th class="py-3 px-4 text-left font-bold">Stratus Upload</th> <th class="py-3 px-4 text-left font-bold">WorkDrive Sync</th> <th class="py-3 px-4 text-center font-bold">Download File</th> <th class="py-3 px-4 text-center font-bold">Delete File</th> </tr> </thead> <tbody id="file-table-body" class="divide-y divide-gray-100"> <tr id="loading-row"> <td colspan="7" class="py-6 text-center"> <div class="flex flex-col items-center justify-center"> <div class="animate-spin-custom rounded-full h-10 w-10 border-t-4 border-b-4 border-[#0059E9] mb-3"> </div> <p class="text-gray-500">Loading files...</p> </div> </td> </tr> <tr id="no-files-row" class="hidden"> <td colspan="7" class="py-16 text-center bg-blue-50/50 rounded-b-lg"> <div class="flex flex-col items-center justify-center text-gray-700"> <svg xmlns="http://www.w3.org/2000/svg" class="h-20 w-20 text-blue-500 mb-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"> </path> <polyline points="14 2 14 8 20 8"> </polyline> <line x1="12" y1="18" x2="12" y2="12"> </line> <line x1="9" y1="15" x2="15" y2="15"> </line> </svg> <p class="text-3xl font-extrabold mb-2 text-blue-800">No files here yet!</p> <p class="text-lg text-gray-600 mt-4">Your vault is empty. Click the 'Upload File' button in the top right to add your first file!</p> </div> </td> </tr> </tbody> </table> </div> </div> </main> <div id="message-overlay" class="message-overlay hidden"> <div class="message-box"> <p id="message-text" class="text-lg font-semibold mb-4"> </p> <button id="message-close-button" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Close</button> </div> </div> <script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.2/catalystWebSDK.js"> </script> <script src="/__catalyst/sdk/init.js"> </script> <script src="main.js" defer> </script> </body> </html> ``` **main.css** ```css @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .animate-spin-custom { animation: spin 1s linear infinite; } body { font-family: 'Inter', sans-serif; } .table-header-bg { background-image: linear-gradient(to right, #DBEAFE, #BFDBFE); color: #1E40AF; } #file-table-body tr:nth-child(even) { background-color: #F9FAFB; } @keyframes cardFadeInScale { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } } .animate-card-fade-in-scale { animation: cardFadeInScale 0.8s ease-out forwards; animation-delay: 0.2s; } .message-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .message-box { background-color: white; padding: 2rem; border-radius: 0.5rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center; max-width: 400px; width: 90%; } ``` **main.js** ```javascript const bucket_name = "file-vault"; // Replace with your actual bucket name const loginURL = "https://p1-807759706.development.catalystserverless.com/__catalyst/auth/login"; // Replace your login URL document.addEventListener('DOMContentLoaded', async () => { const fileTableBody = document.getElementById('file-table-body'); const loadingRow = document.getElementById('loading-row'); const noFilesRow = document.getElementById('no-files-row'); const logoutButton = document.getElementById('logout-button'); const uploadButton = document.getElementById('upload-button'); const fileInput = document.getElementById('file-input'); const messageOverlay = document.getElementById('message-overlay'); const messageText = document.getElementById('message-text'); const messageCloseButton = document.getElementById('message-close-button'); async function checkSignInStatus() { let userIsActuallySignedIn = false; try { await catalyst.auth.isUserAuthenticated(); userIsActuallySignedIn = true; console.log("User is signed in."); } catch (err) { console.log("User not signed in:", err); userIsActuallySignedIn = false; } try { if (!userIsActuallySignedIn) { fileTableBody.innerHTML = ` Error: Not signed in. Redirecting to login... `; window.location.href = loginURL; } } catch (error) { console.error("Failed to redirect:", error); } } function showMessage(text, isError = false) { messageText.textContent = text; if (isError) { messageText.classList.add('text-red-600'); messageText.classList.remove('text-gray-800'); } else { messageText.classList.add('text-gray-800'); messageText.classList.remove('text-red-600'); } messageOverlay.classList.remove('hidden'); } function hideMessage() { messageOverlay.classList.add('hidden'); } messageCloseButton.addEventListener('click', hideMessage); function createFileStatusBadge(status) { let bgColorClass, textColorClass, dotColorClass; switch (status) { case 'Uploaded': bgColorClass = 'bg-green-100'; textColorClass = 'text-green-800'; dotColorClass = 'text-green-400'; break; case 'Deleted': bgColorClass = 'bg-gray-200'; textColorClass = 'text-gray-700'; dotColorClass = 'text-gray-500'; break; case 'Failed': bgColorClass = 'bg-red-100'; textColorClass = 'text-red-800'; dotColorClass = 'text-red-400'; break; default: bgColorClass = 'bg-yellow-100'; textColorClass = 'text-yellow-800'; dotColorClass = 'text-yellow-400'; } return ` ${status} `; } function createFileRow(file) { const row = document.createElement('tr'); row.classList.add('hover:bg-blue-50', 'transition-colors', 'duration-150'); row.dataset.rowId = file.ROWID; row.dataset.fileName = file.FileName; const fileSizeFormatted = file.FileSize ? (file.FileSize / 1024).toFixed(2) + ' KB' : 'N/A'; const uploadedTimeFormatted = file.UploadedTime || 'N/A'; const isActionDisabled = file.StratusUpload === 'Deleted' || file.StratusUpload === 'Failed'; const disabledClass = isActionDisabled ? 'opacity-50 cursor-not-allowed' : ''; const downloadButtonState = isActionDisabled ? 'disabled' : ''; const deleteButtonState = isActionDisabled ? 'disabled' : ''; row.innerHTML = ` ${file.FileName} ${uploadedTimeFormatted} ${file.FileSize ? (file.FileSize / 1024).toFixed(2) + ' KB' : 'N/A'} ${createFileStatusBadge(file.StratusUpload)} ${createFileStatusBadge(file.WorkDriveSync)} `; return row; } logoutButton.addEventListener('click', async () => { console.log('Logging out...'); showMessage('Logging out...', false); try { await catalyst.auth.signOut(loginURL); } catch (error) { console.error('Logout failed:', error); showMessage('Logout failed. Please try again.', true); } }); uploadButton.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; if (file) { showMessage(`Uploading "${file.name}"...`, false); console.log('Selected file for upload:', file.name, file.type, file.size); let uploadSuccess = false; let rowId = null; try { const table = catalyst.table.tableId('FileVault'); const details = [ { "FileName": file.name, "FileSize": file.size, "StratusUpload": "Pending", "WorkDriveSync": "Pending" } ]; const insertResponse = await table.addRow(details); rowId = insertResponse.content[0].ROWID; const bucket = catalyst.stratus.bucket(bucket_name); const putObject = await bucket.putObject(file.name, file); const putObjectResponse = await putObject.start(); if (putObjectResponse.status !== 200) { throw new Error("Something went wrong!"); } uploadSuccess = true; const date = new Date(); const options = { month: "long", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: true, }; const parts = new Intl.DateTimeFormat("en-US", options).formatToParts(date); const lookup = Object.fromEntries(parts.map(p => [p.type, p.value])); const currentTime = `${lookup.month} ${lookup.day}, ${lookup.year} ${lookup.hour}:${lookup.minute} ${lookup.dayPeriod}`; const updateDetails = [ { "UploadedTime": currentTime, "StratusUpload": "Uploaded", "ROWID": rowId } ]; await table.updateRow(updateDetails); } catch (err) { console.error('Error during upload process:', err); if (rowId) { try { const table = catalyst.table.tableId('FileVault'); const row = table.rowId(rowId); await row.delete(); } catch (updateErr) { console.error('Failed to update row status after upload error:', updateErr); } } } if (uploadSuccess) { showMessage(`"${file.name}" uploaded successfully!`, false); fetchFiles(); setTimeout(hideMessage, 1500); } else { showMessage(`Failed to upload "${file.name}". Please check console for details.`, true); setTimeout(hideMessage, 2000); } } event.target.value = null; }); async function fetchFiles() { fileTableBody.innerHTML = ''; loadingRow.classList.remove('hidden'); noFilesRow.classList.add('hidden'); fileTableBody.appendChild(loadingRow); try { const table = catalyst.table.tableId('FileVault'); const response = await table.getPagedRows({}); loadingRow.classList.add('hidden'); if (response.status === 200) { if (response.content && response.content.length > 0) { renderFiles(response.content); noFilesRow.classList.add('hidden'); } else { fileTableBody.innerHTML = ''; noFilesRow.classList.remove('hidden'); fileTableBody.appendChild(noFilesRow); } } else { fileTableBody.innerHTML = ` Error: Failed to load files. ${response.statusText || 'Please check your Catalyst Function.'} `; console.error('Failed to fetch files:', response.statusText); } } catch (error) { loadingRow.classList.add('hidden'); fileTableBody.innerHTML = ` Network Error: Could not connect or retrieve data.

Check console for details.

`; console.error('Error during file fetch:', error); } } function renderFiles(files) { files.forEach(file => { fileTableBody.appendChild(createFileRow(file)); }); } async function downloadFile(fileName) { showMessage(`Preparing to download "${fileName}"...`, false); try { const bucket = catalyst.stratus.bucket(bucket_name); const getObject = await bucket.getObject(fileName); const getObjectResponse = await getObject.start(); const fileBlob = getObjectResponse.content; if (fileBlob) { const blobUrl = URL.createObjectURL(fileBlob); const a = document.createElement('a'); a.href = blobUrl; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); hideMessage(); } else { showMessage(`Failed to retrieve file content for "${fileName}".`, true); setTimeout(hideMessage, 2000); } } catch (error) { console.error('Error during file download:', error); let errorMessage = `Error downloading "${fileName}". Please try again.`; if (error && error.message && error.message.includes("404")) { errorMessage = `File "${fileName}" not found in Stratus.`; } showMessage(errorMessage, true); setTimeout(hideMessage, 3000); } } async function deleteFile(rowId, fileName) { showMessage(`Are you sure you want to delete "${fileName}"? This will remove it from storage and mark it as deleted.`, false); messageCloseButton.textContent = 'Cancel'; const confirmDeleteButton = document.createElement('button'); confirmDeleteButton.textContent = 'Delete'; confirmDeleteButton.className = 'ml-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600'; const originalCloseHandler = messageCloseButton.onclick; messageCloseButton.onclick = () => { hideMessage(); messageCloseButton.textContent = 'Close'; messageCloseButton.onclick = originalCloseHandler; confirmDeleteButton.remove(); }; messageCloseButton.parentNode.appendChild(confirmDeleteButton); confirmDeleteButton.onclick = async () => { hideMessage(); messageCloseButton.textContent = 'Close'; messageCloseButton.onclick = originalCloseHandler; confirmDeleteButton.remove(); showMessage(`Processing deletion for "${fileName}"...`, false); console.log(`Attempting to delete file from Stratus (Name: ${fileName}) and update DB (ROWID: ${rowId})`); let dbUpdateSuccess = false; let stratusDeleteSuccess = false; try { const bucket = catalyst.stratus.bucket(bucket_name); await bucket.deleteObject(fileName); stratusDeleteSuccess = true; } catch (error) { console.error('Error deleting file from Stratus:', error); showMessage(`Failed to remove "${fileName}" from storage.`, true); } try { const table = catalyst.table.tableId('FileVault'); const updateDetails = [ { "ROWID": rowId, "StratusUpload": "Deleted" } ]; await table.updateRow(updateDetails); dbUpdateSuccess = true; } catch (error) { console.error('Error updating Data Store row to "Deleted":', error); if (stratusDeleteSuccess) { showMessage(`File "${fileName}" removed from storage, but failed to mark as deleted in database.`, true); } else { showMessage(`Failed to mark "${fileName}" as deleted in database.`, true); } } if (dbUpdateSuccess && stratusDeleteSuccess) { showMessage(`"${fileName}" removed from storage!`, false); fetchFiles(); setTimeout(hideMessage, 1500); } else { fetchFiles(); setTimeout(hideMessage, 3000); } }; } fileTableBody.addEventListener('click', (event) => { const targetButton = event.target.closest('.download-btn, .delete-btn'); if (!targetButton || targetButton.disabled) return; const row = targetButton.closest('tr'); if (!row) return; const rowId = row.dataset.rowId; const fileName = row.dataset.fileName; if (targetButton.classList.contains('download-btn')) { downloadFile(fileName); } else if (targetButton.classList.contains('delete-btn')) { deleteFile(rowId, fileName); } }); await checkSignInStatus(); fetchFiles(); }); ``` Next, update your `client-package.json` with the below code. **client-package.json** ```json { "name": "WorkDriveSyncApp", "version": "0.0.1", "homepage" : "/__catalyst/auth/login", "login_redirect" : "index.html" } ``` The client directory is now configured. Let us quickly go through the working of the functions and the client of the application: * `index.html` contains the code for the front-end of the client. The CSS is defined in `main.css`. * `main.js`: The JavaScript function in the client component that handles the various actions performed in the client through these functions: * **`checkSignInStatus()`:** Verifies whether the user is authenticated or not, if authenticated it allows user access to the app else displays an error and redirects the user to the login page. * **`fetchFiles()`:** Fetches all file records from the Catalyst Data Store table. * **`uploadFile (triggered via file input)`:** Handles file upload to Catalyst Stratus and updates metadata in the Data Store. * **`downloadFile(fileName)`:** Downloads a file from the Stratus Bucket. * **`deleteFile(rowId, fileName)`:** Deletes a file both from Stratus and the Data Store. * **`logout()`**: Logs the user out of the application * The **Event function** is triggered each time the event listener executes. This function performs the following actions: * Downloads the file that was sent in as the event data by the event listener using a file stream * Uploads the file and its metadata to WorkDrive through an API with the POST method. The OAuth credentials are passed to authorize this action. * If the action is successful, the Work Drive sync status is set to "Completed". The record in the Data Store is updated with the WorkDrive file ID and the sync status.