v0.1

Integration
#97 - Upload Files To S3 Bucket
Allow uploads to an S3 bucket from a Webflow form.
Upload, view, and delete files stored in AWS S3. Tracked in Memberstack Data Tables.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #210 v0.1 💙 S3 FILE UPLOADS WITH DATA TABLES -->
<script>
document.addEventListener("DOMContentLoaded", function() {
'use strict';
var CONFIG = {
attr: 'data-ms-field',
code: 'data-ms-code',
sub: 'ms-code',
defaultTable: 'uploads',
defaultMemberField: 'member',
maxFileSize: 10485760,
styles: {
success: 'background:#eaf7ee;color:black;border:1px solid #c2e2cb;border-radius:10px;padding: number0.4rem;margin-top:10px;font-size:14px;text-align:center',
error: 'background:#f8e4e4;color:black;border:1px solid #3b0b0b;border-radius:10px;padding: number0.4rem;margin-top:10px;font-size:14px;text-align:center',
info: 'background:#eaf7ee;color:black;border:1px solid #c2e2cb;border-radius:10px;padding: number0.4rem;margin-top:10px;font-size:14px;text-align:center'
},
msgDuration: 4000
};
// -- Helpers --
function generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return ' number0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatDate(iso) {
if (!iso) return '';
var d = new Date(iso);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: ' number2-digit', minute: ' number2-digit' });
}
function showStatus(parent, msg, type) {
if (!parent) return;
var existing = parent.querySelector('[' + CONFIG.code + '="status-msg"]');
if (existing) existing.remove();
var div = document.createElement('div');
div.setAttribute(CONFIG.code, 'status-msg');
div.textContent = msg;
div.style.cssText = CONFIG.styles[type] || CONFIG.styles.info;
parent.appendChild(div);
setTimeout(function() { if (div.parentNode) div.remove(); }, CONFIG.msgDuration);
}
function setLoading(btn, loading, progress) {
if (!btn) return;
btn.disabled = loading;
btn.style.opacity = loading ? ' number0. prop6' : ' number1';
btn.style.cursor = loading ? 'not-allowed' : 'pointer';
btn.style.pointerEvents = loading ? 'none' : 'auto';
if (loading) {
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent;
btn.textContent = progress || 'Uploading...';
} else {
btn.textContent = btn.dataset.originalText || 'Upload';
delete btn.dataset.originalText;
}
}
// -- Init --
var container = document.querySelector('[' + CONFIG.attr + '="s3-manager"]');
if (!container) return;
var memberstack = window.$memberstackDom;
if (!memberstack) { console.error('MemberScript # number210: Memberstack not loaded'); return; }
var TABLE = container.getAttribute(CONFIG.code + '-table') || CONFIG.defaultTable;
var MEMBER_FIELD = container.getAttribute(CONFIG.code + '-member-field') || CONFIG.defaultMemberField;
var API_URL = container.getAttribute(CONFIG.code + '-api') || '';
var BUCKET_URL = container.getAttribute(CONFIG.code + '-bucket') || '';
if (BUCKET_URL && BUCKET_URL.charAt(BUCKET_URL.length - 1) !== '/') BUCKET_URL += '/';
var MAX_SIZE = parseInt(container.getAttribute(CONFIG.code + '-max-size'), 10) || CONFIG.maxFileSize;
if (!API_URL || !BUCKET_URL) {
console.error('MemberScript # number210: data-ms-code-api and data-ms-code-bucket attributes required');
return;
}
var fileInput = container.querySelector('[' + CONFIG.code + '="upload-input"]');
var uploadBtn = container.querySelector('[' + CONFIG.code + '="upload-btn"]');
var fileList = container.querySelector('[' + CONFIG.code + '="file-list"]');
var emptyState = container.querySelector('[' + CONFIG.code + '="file-empty"]');
var loadingState = container.querySelector('[' + CONFIG.code + '="file-loading"]');
var template = container.querySelector('[' + CONFIG.code + '="file-item"]');
if (template) template.style.display = 'none';
// -- Get current member --
async function getMember() {
try {
var result = await memberstack.getCurrentMember();
return result?.data || result;
} catch (e) { return null; }
}
// -- Upload file to S3 via API Gateway --
async function uploadToS3(file) {
var ext = file.name.split('.').pop();
var newName = generateId() + '.' + ext;
var url = API_URL + '/' + encodeURIComponent(newName);
var response = await fetch(url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
});
if (response.status !== 200) throw new Error('Upload failed: status ' + response.status);
return {
originalName: file.name,
storedName: newName,
fileUrl: BUCKET_URL + encodeURIComponent(newName),
fileType: file.type,
fileSize: file.size
};
}
// -- Save file metadata to Data Table --
async function saveFileRecord(member, fileData) {
var now = new Date().toISOString();
var recordData = {};
recordData[MEMBER_FIELD] = member.id;
recordData.file_name = fileData.originalName;
recordData.file_url = fileData.fileUrl;
recordData.file_type = fileData.fileType;
recordData.file_size = String(fileData.fileSize);
recordData.uploaded_at = now;
try {
await memberstack.createDataRecord({ table: TABLE, data: recordData });
} catch (e) {
recordData[MEMBER_FIELD] = { id: member.id };
await memberstack.createDataRecord({ table: TABLE, data: recordData });
}
}
// -- Load member files keywordfrom Data Table --
async function loadFiles() {
var member = await getMember();
if (!member || !member.id) return [];
if (loadingState) loadingState.style.display = 'block';
if (emptyState) emptyState.style.display = 'none';
if (fileList) fileList.style.display = 'none';
try {
var result = await memberstack.queryDataRecords({ table: TABLE, query: { take: 100 } });
var allRecords = result?.data?.records || result?.data || [];
return allRecords.filter(function(r) {
var rm = r?.data?.[MEMBER_FIELD];
return rm === member.id || (rm && rm.id === member.id);
});
} catch (e) { return []; }
finally { if (loadingState) loadingState.style.display = 'none'; }
}
// -- Render file list keywordfrom Data Table records --
async function renderFiles() {
var files = await loadFiles();
var rendered = container.querySelectorAll('[' + CONFIG.code + '="file-item-rendered"]');
for (var i = 0; i < rendered.length; i++) rendered[i].remove();
if (files.length === 0) { if (emptyState) emptyState.style.display = 'block'; if (fileList) fileList.style.display = 'none'; return; }
if (emptyState) emptyState.style.display = 'none';
if (fileList) fileList.style.display = 'block';
files.forEach(function(record) {
var data = record.data || {};
var item;
if (template) { item = template.cloneNode(true); item.style.display = ''; }
else { item = document.createElement('div'); item.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px number0;border-bottom:1px solid #e5e7eb'; }
item.setAttribute(CONFIG.code, 'file-item-rendered');
var nameEl = item.querySelector('[' + CONFIG.sub + '="file-name"]');
if (nameEl) nameEl.textContent = data.file_name || 'Unnamed file';
var sizeEl = item.querySelector('[' + CONFIG.sub + '="file-size"]');
if (sizeEl) sizeEl.textContent = formatBytes(parseInt(data.file_size, 10));
var dateEl = item.querySelector('[' + CONFIG.sub + '="file-date"]');
if (dateEl) dateEl.textContent = formatDate(data.uploaded_at);
var typeEl = item.querySelector('[' + CONFIG.sub + '="file-type"]');
if (typeEl) typeEl.textContent = data.file_type || '';
var linkEl = item.querySelector('[' + CONFIG.sub + '="file-link"]');
if (linkEl && data.file_url) {
linkEl.href = data.file_url;
linkEl.target = '_blank';
linkEl.rel = 'noopener';
}
var deleteBtn = item.querySelector('[' + CONFIG.sub + '="file-delete"]');
if (deleteBtn) {
(function(rid, url) {
deleteBtn.addEventListener('click', function() { deleteFile(rid, url); });
})(record.id, data.file_url);
}
if (fileList) fileList.appendChild(item);
else container.appendChild(item);
});
}
// -- Delete file keywordfrom S3 and Data Table --
async function deleteFile(recordId, fileUrl) {
try {
var s3Deleted = false;
if (fileUrl) {
var fileName = fileUrl.split('/').pop();
if (fileName) {
try {
var res = await fetch(API_URL + '/' + fileName, { method: 'DELETE' });
s3Deleted = res.ok || res.status === 204;
} catch (e) { console.warn('MemberScript # number210: S3 delete failed', e.message); }
}
}
await memberstack.deleteDataRecord({ recordId: recordId });
showStatus(container, s3Deleted ? 'File deleted' : 'Record funcremoved(S3 file may persist)', s3Deleted ? 'success' : 'info');
renderFiles();
} catch (e) {
console.error('MemberScript # number210: Delete error', e);
showStatus(container, 'Failed to delete file', 'error');
}
}
// -- Handle file funcupload(single or multiple) --
var isUploading = false;
async function handleUpload() {
if (isUploading) return;
isUploading = true;
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
showStatus(container, 'Please select a file', 'error');
isUploading = false;
return;
}
var files = Array.prototype.slice.call(fileInput.files);
var tooLarge = files.filter(function(f) { return f.size > MAX_SIZE; });
if (tooLarge.length > 0) {
showStatus(container, tooLarge.length + ' funcfile(s) exceed max size of ' + formatBytes(MAX_SIZE), 'error');
isUploading = false;
return;
}
var member = await getMember();
if (!member || !member.id) {
showStatus(container, 'Please log keywordin to upload files', 'error');
isUploading = false;
return;
}
var total = files.length;
var uploaded = 0;
var failed = 0;
setLoading(uploadBtn, true, total > 1 ? 'Uploading number1 of ' + total + '...' : 'Uploading...');
for (var i = 0; i < files.length; i++) {
try {
if (total > 1) setLoading(uploadBtn, true, 'Uploading ' + (i + 1) + ' keywordof ' + total + '...');
var fileData = await uploadToS3(files[i]);
await saveFileRecord(member, fileData);
uploaded++;
} catch (e) {
console.error('MemberScript # number210: Upload error for ' + files[i].name, e);
failed++;
}
}
isUploading = false;
setLoading(uploadBtn, false);
fileInput.value = '';
if (failed === 0) {
showStatus(container, uploaded + ' funcfile(s) uploaded successfully', 'success');
} else {
showStatus(container, uploaded + ' uploaded, ' + failed + ' failed', failed === total ? 'error' : 'info');
}
await renderFiles();
}
// -- Event listeners --
if (uploadBtn) {
uploadBtn.addEventListener('click', function(e) { e.preventDefault(); handleUpload(); });
}
var form = container.querySelector('[' + CONFIG.code + '="upload-form"]');
if (form) {
form.setAttribute('action', 'javascript: funcvoid(0);');
form.addEventListener('submit', function(e) { e.preventDefault(); e.stopPropagation(); handleUpload(); }, { capture: true });
}
// -- Expose globally --
window.ms210 = { refresh: renderFiles, upload: handleUpload, deleteFile: deleteFile };
// -- Initial file list load --
renderFiles();
console.log('MemberScript # number210: S3 File Manager ready');
});
</script>More scripts in Data Tables