File Manager

This example demonstrates a comprehensive file management system with upload, download, and organization capabilities using the BuzzyFrameAPI.

Key Features

  • Universal file upload supporting all file types

  • Drag-and-drop interface with visual feedback

  • File metadata display including size, type, and filename

  • Download functionality with direct file access

  • File deletion with confirmation dialogs

  • Progress tracking during upload operations

  • Automatic URL refresh for expired S3 links

  • Responsive design with mobile-friendly layout

Use Cases

Ideal for applications requiring:

  • Document management systems

  • File sharing platforms

  • Project collaboration tools

  • Digital asset management

  • Client portals with file exchange

  • Administrative dashboards

Implementation Overview

This widget follows the Child Table Data pattern, storing files as child records linked to a parent row. Key implementation features:

  • Support for any file type (documents, images, videos, etc.)

  • Efficient file size formatting and display

  • S3 integration with presigned URLs for security

  • Real-time synchronization with backend data

  • Mobile-responsive design using Bulma CSS

Code Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>File Manager</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script
      src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
      crossorigin
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js"
      crossorigin
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"
      crossorigin
    ></script>
    <style>
      .dropzone {
        border: 2px dashed #ccc;
        cursor: pointer;
        transition: all 0.3s ease;
      }
      .dropzone.active {
        border-color: #4caf50;
        background-color: #f8fff8;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel" data-presets="env,react">
      const buzzyFrameAPI = new BuzzyFrameAPI();
      let appInitData = null;
      let appFiles = [];

      async function initApp() {
        appInitData = await buzzyFrameAPI.initialise();
        await fetchAndUpdateFiles();

        buzzyFrameAPI.addMicroappListener({
          microAppID: appInitData.rowJSON._id,
          listener: async () => {
            console.log('Child data updated, refreshing files');
            await fetchAndUpdateFiles();
          },
        });

        setInterval(() => {
          refreshExpiredUrls();
        }, 5 * 60 * 1000);
      }

      async function fetchAndUpdateFiles() {
        try {
          const { rowJSON = {} } = appInitData || {};
          const children = await buzzyFrameAPI.getChildItemsByField({
            appID: rowJSON._id,
            fieldID: rowJSON.fieldID,
          });

          appFiles = children;

          if (window.updateFiles) {
            window.updateFiles(appFiles);
          }
        } catch (error) {
          console.error('Error fetching files:', error);
          if (window.updateFiles) {
            window.updateFiles([]);
          }
        }
      }

      function App() {
        const { useEffect, useState, useCallback } = React;
        const [files, setFiles] = useState([]);
        const [loading, setLoading] = useState(true);
        const [uploading, setUploading] = useState(false);
        const [progress, setProgress] = useState(0);
        const [dragActive, setDragActive] = useState(false);

        useEffect(() => {
          window.updateFiles = newFiles => {
            setFiles(newFiles);
            setLoading(false);
          };

          if (appFiles.length > 0) {
            setFiles(appFiles);
            setLoading(false);
          }

          return () => {
            window.updateFiles = null;
          };
        }, []);

        const refreshExpiredUrls = async () => {
          const updatedFiles = await Promise.all(
            appFiles.map(async file => {
              if (
                file.content.expiredAt &&
                Date.now() > file.content.expiredAt
              ) {
                try {
                  const { presignedReadUrl } =
                    await buzzyFrameAPI.createPresignedPutAndReadUrls(
                      appInitData.rowJSON._id,
                      file.content.filename,
                    );

                  await buzzyFrameAPI.updateMicroappChild({
                    childID: file._id,
                    content: {
                      ...file.content,
                      url: presignedReadUrl,
                      expiredAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
                    },
                  });

                  return {
                    ...file,
                    content: {
                      ...file.content,
                      url: presignedReadUrl,
                    },
                  };
                } catch (error) {
                  console.error('Error refreshing URL:', error);
                  return file;
                }
              }
              return file;
            }),
          );
          appFiles = updatedFiles;
          if (window.updateFiles) {
            window.updateFiles(appFiles);
          }
        };

        const handleDrop = useCallback(async e => {
          e.preventDefault();
          setDragActive(false);

          const files = e.dataTransfer?.files;
          if (!files?.length) return;

          setUploading(true);
          setProgress(0);

          try {
            const { rowJSON } = appInitData;

            for (let i = 0; i < files.length; i++) {
              const file = files[i];
              setProgress((i / files.length) * 100);

              const { presignedPutUrl, presignedReadUrl } =
                await buzzyFrameAPI.createPresignedPutAndReadUrls(
                  rowJSON._id,
                  file.name,
                );

              await buzzyFrameAPI.uploadFileToS3({
                presignedPutUrl,
                file,
                onProgress: percent =>
                  setProgress(((i + percent / 100) / files.length) * 100),
              });

              await buzzyFrameAPI.createMicroappChild({
                microAppResourceID: rowJSON.parentResourceID,
                appID: rowJSON._id,
                fieldID: rowJSON.fieldID,
                content: {
                  url: presignedReadUrl,
                  filename: file.name,
                  size: file.size,
                  type: file.type,
                  expiredAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
                },
              });
            }
          } catch (error) {
            console.error('Error uploading files:', error);
          } finally {
            setUploading(false);
            setProgress(0);
          }
        }, []);

        const handleDeleteFile = async childID => {
          if (!confirm('Are you sure you want to delete this file?')) return;

          try {
            await buzzyFrameAPI.removeMicroappChild({ childID });
          } catch (error) {
            console.error('Error deleting file:', error);
          }
        };

        const formatFileSize = bytes => {
          const units = ['B', 'KB', 'MB', 'GB'];
          let size = bytes;
          let unitIndex = 0;

          while (size >= 1024 && unitIndex < units.length - 1) {
            size /= 1024;
            unitIndex++;
          }

          return size.toFixed(1) + ' ' + units[unitIndex];
        };

        if (loading)
          return <div className="has-text-centered p-4">Loading...</div>;

        return (
          <div className="container">
            <div
              className={`dropzone box has-text-centered p-5 mb-4 ${
                dragActive ? 'active' : ''
              }`}
              onDragOver={e => {
                e.preventDefault();
                setDragActive(true);
              }}
              onDragLeave={() => setDragActive(false)}
              onDrop={handleDrop}>
              <p className="mb-3">
                Drag and drop files here or click to browse
              </p>
              <input
                type="file"
                multiple
                onChange={e =>
                  handleDrop({ dataTransfer: { files: e.target.files } })
                }
                style={{ display: 'none' }}
                id="file-input"
              />
              <button
                className="button is-primary"
                onClick={() => document.getElementById('file-input').click()}>
                Browse Files
              </button>
            </div>

            {uploading && (
              <div className="mb-4">
                <progress
                  className="progress is-primary"
                  value={progress}
                  max="100">
                  {progress}%
                </progress>
              </div>
            )}

            <div className="content">
              {files.map(file => (
                <div key={file._id} className="box">
                  <div className="columns is-mobile is-vcentered">
                    <div className="column">
                      <div>
                        <strong>{file.content.filename}</strong>
                      </div>
                      <div className="has-text-grey is-size-7">
                        {formatFileSize(file.content.size)} •{' '}
                        {file.content.type}
                      </div>
                    </div>
                    <div className="column is-narrow">
                      <div className="buttons has-addons">
                        <a
                          href={file.content.url}
                          target="_blank"
                          rel="noopener noreferrer"
                          className="button is-primary is-small">
                          <span className="icon is-small">
                            <i className="fas fa-download"></i>
                          </span>
                          <span>Download</span>
                        </a>
                        <button
                          className="button is-danger is-small"
                          onClick={() => handleDeleteFile(file._id)}>
                          <span className="icon is-small">
                            <i className="fas fa-trash"></i>
                          </span>
                          <span>Delete</span>
                        </button>
                      </div>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        );
      }

      initApp().then(() => {
        ReactDOM.render(<App />, document.getElementById('root'));
      });
    </script>
  </body>
</html>

Key Concepts

Universal File Support

  • All File Types: Supports documents, images, videos, archives, and any other file format

  • File Metadata: Displays filename, size, and MIME type for easy identification

  • Size Formatting: Human-readable file size display (B, KB, MB, GB)

File Operations

  • Upload: Drag-and-drop or click-to-browse file selection

  • Download: Direct file access through presigned URLs

  • Delete: Secure file removal with confirmation

  • Progress Tracking: Real-time upload progress feedback

S3 Integration

  • Presigned URLs: Secure file upload and access without exposing credentials

  • URL Expiration: Automatic refresh of expired URLs to maintain access

  • Storage Management: Efficient file storage with proper metadata tracking

User Experience

  • Responsive Design: Works seamlessly on desktop and mobile devices

  • Visual Feedback: Clear indicators for drag-and-drop states and upload progress

  • Error Handling: Graceful error handling with user-friendly messages

This file manager demonstrates how code widgets can provide enterprise-grade file management capabilities within Buzzy applications, suitable for any use case requiring file storage and organization.

Last updated