Copy <!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>