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 attachmentsReal-time Updates:
addMicroappListener()
ensures immediate UI updates when data changesS3 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
callbacksURL 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