Configure the Client Directory
Let’s now configure the client component. The 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. We will also create a new file login.html in this directory and add code in it.
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.htmlcopy<!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.5.0-beta/catalystWebSDK.js"> </script> <script src="/__catalyst/sdk/init.js"> </script> <script src="main.js" defer> </script> </body> </html>
main.csscopy@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.jscopyconst 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 = `
`; 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 = ` Error: Not signed in. Redirecting to login... ${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 = ` `; console.error('Failed to fetch files:', response.statusText); } } catch (error) { loadingRow.classList.add('hidden'); fileTableBody.innerHTML = ` Error: Failed to load files. ${response.statusText || 'Please check your Catalyst Function.'} `; 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(); }); Network Error: Could not connect or retrieve data. Check console for details.
Next, create a new HTML file in the client/ directory and name it login.html. Add the code given below to the file.
client-package.jsoncopy{ "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:
The login.html file in the client component enables Catalyst Authentication login. This does not handle the Zoho sign-in feature, only the Catalyst login.
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:
uploadfile(): Initiates the file upload in the client. The file is uploaded to the File Store, and its metadata is populated. This metadata is inserted into the Data Store table after creating a new row. The WorkDrive sync status is set to “In Progress” with the file upload.
getfiles(): Sends a GET request to the /getFiles path defined in the Advanced I/O function that fetches the table rows from the Data Store. This is triggered automatically when the index page of the client is loaded.
renderTable(): Creates and renders a table in client which is populated with the response data of getfiles()
getRequiredData(): Fetches the metadata of each file from getfiles(), such as the name, size, File Store upload status, WorkDrive sync status to render in the client. This also renders the download and delete icons for each file.
deleteFile(): Sends a DELETE request to the /deleteFile path defined in the Advanced I/O function. The showDeletePopup() triggers a pop-up that confirms the delete action.
Note: You will not be able to delete a file from the client while the WorkDrive sync is still in progress.
downloadfile(): Initiates the file download through a download promise when a download link is clicked
logout(): Logs the user out of the application
The Advanced I/O function defines the following APIs:
/getFiles: Queries the Data Store table using ZCQL to fetch its rows and sends the response to main.js
/deleteFile: This triggers an API to delete the file from WorkDrive through the PATCH method. The OAuth credentials are passed to authorize this action. The file is then deleted from the File Store and the associated record is removed from the Data Store as well. The response is sent to main.js which then renders the updated information in the client.
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.
Last Updated 2025-06-17 17:46:43 +0530 +0530