Advanced Image Gallery

This example demonstrates a sophisticated image gallery with drag-and-drop upload, file management, and S3 integration using the BuzzyFrameAPI.

Key Features

  • Drag-and-drop file upload with visual feedback

  • Multiple file selection and batch upload

  • Progress tracking during upload operations

  • Image preview with hover actions

  • File deletion with confirmation

  • Automatic URL refresh for expired S3 links

  • Real-time updates when images are added or removed

  • Responsive grid layout using Bulma CSS framework

Use Cases

Perfect for applications requiring:

  • Photo galleries and portfolios

  • Document management systems

  • Product image management

  • User-generated content platforms

  • Media libraries and archives

Implementation Overview

This widget uses the Child Table Data pattern, where images are stored as child records linked to a parent row through a file/image field. The implementation showcases:

  • Modern React hooks (useState, useEffect, useCallback)

  • Proper BuzzyFrameAPI initialization outside React components

  • Real-time data synchronization with microapp listeners

  • S3 file operations with presigned URLs

  • Error handling and user feedback

Code Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Advanced Image Gallery</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;
      }
      .image-actions {
        position: absolute;
        top: 10px;
        right: 10px;
        opacity: 0;
        transition: opacity 0.2s;
      }
      .image-card:hover .image-actions {
        opacity: 1;
      }
      .image-card img {
        width: 100%;
        height: 150px;
        object-fit: cover;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel" data-presets="env,react">
      const buzzyFrameAPI = new BuzzyFrameAPI();
      let appInitData = null;
      let appImages = [];

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

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

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

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

          appImages = children;

          if (window.updateImages) {
            window.updateImages(appImages);
          }
        } catch (error) {
          console.error('Error fetching images:', error);
          if (window.updateImages) {
            window.updateImages([]);
          }
        }
      }

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

        useEffect(() => {
          window.updateImages = newImages => {
            setImages(newImages);
            setLoading(false);
          };

          if (appImages.length > 0) {
            setImages(appImages);
            setLoading(false);
          }

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

        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 handleDeleteImage = async childID => {
          if (!confirm('Are you sure you want to delete this image?')) return;

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

        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 images here or click to browse
              </p>
              <input
                type="file"
                accept="image/*"
                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="columns is-multiline">
              {images.map(image => (
                <div key={image._id} className="column is-3">
                  <div className="card image-card">
                    <div className="card-image">
                      <figure className="image">
                        <img
                          src={image.content.url}
                          alt={image.content.filename}
                        />
                      </figure>
                    </div>
                    <div className="card-content">
                      <p className="is-size-7 has-text-grey">
                        {image.content.filename}
                      </p>
                    </div>
                    <div className="image-actions">
                      <div className="buttons has-addons">
                        <button
                          className="button is-danger is-small"
                          onClick={() => handleDeleteImage(image._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

BuzzyFrameAPI Integration

  • Initialization Pattern: API is initialized outside React components for proper async handling

  • Child Data Management: Uses getChildItemsByField() to retrieve file attachments

  • Real-time Updates: addMicroappListener() ensures immediate UI updates when data changes

  • S3 Operations: Handles presigned URLs for secure file upload and access

File Management Features

  • Drag-and-Drop: Native HTML5 drag-and-drop with visual feedback

  • Progress Tracking: Real-time upload progress with onProgress callbacks

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

  • Error Handling: Comprehensive error handling with user feedback

React Best Practices

  • Hooks Usage: Modern React patterns with useState, useEffect, and useCallback

  • Memory Management: Proper cleanup of event listeners and global functions

  • State Management: Efficient state updates with minimal re-renders

This example demonstrates how code widgets can create professional-grade file management interfaces that rival dedicated file management applications.

Last updated