#210 - S3 File Uploads with Data Tables

Upload, view, and delete files stored in AWS S3. Tracked in Memberstack Data Tables.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

313 lines
Paste this into Webflow
<!-- 💙 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>

Script Info

Versionv0.1
PublishedFeb 16, 2026
Last UpdatedFeb 16, 2026

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Data Tables