#211 - Vercel Blob File Uploads with Data Tables

Upload, view, and delete files stored in Vercel Blob. Tracked in Memberstack Data Tables.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

309 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #211 v0.1 💙 VERCEL BLOB 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 + '="blob-manager"]');
  if (!container) return;

  var memberstack = window.$memberstackDom;
  if (!memberstack) { console.error('MemberScript #number211: 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') || '';
  if (API_URL && API_URL.charAt(API_URL.length - 1) === '/') API_URL = API_URL.slice(0, -1);
  var MAX_SIZE = parseInt(container.getAttribute(CONFIG.code + '-max-size'), 10) || CONFIG.maxFileSize;

  if (!API_URL) {
    console.error('MemberScript #number211: data-ms-code-api attribute 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 Vercel Blob via API --
  async function uploadToBlob(file) {
    var ext = file.name.split('.').pop();
    var newName = generateId() + '.' + ext;

    var response = await fetch(API_URL + '/api/upload?filename=' + encodeURIComponent(newName), {
      method: 'PUT',
      body: file,
      headers: { 'Content-Type': file.type }
    });

    if (!response.ok) throw new Error('Upload failed: status ' + response.status);
    var blob = await response.json();

    return {
      originalName: file.name,
      storedName: newName,
      fileUrl: blob.url,
      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 Vercel Blob and Data Table --
  async function deleteFile(recordId, fileUrl) {
    try {
      var blobDeleted = false;
      if (fileUrl) {
        try {
          var res = await fetch(API_URL + '/api/delete?url=' + encodeURIComponent(fileUrl), { method: 'DELETE' });
          blobDeleted = res.ok;
        } catch (e) { console.warn('MemberScript #number211: Blob delete failed', e.message); }
      }
      await memberstack.deleteDataRecord({ recordId: recordId });
      showStatus(container, blobDeleted ? 'File deleted' : 'Record funcremoved(blob may persist)', blobDeleted ? 'success' : 'info');
      renderFiles();
    } catch (e) {
      console.error('MemberScript #number211: 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 uploadToBlob(files[i]);
        await saveFileRecord(member, fileData);
        uploaded++;
      } catch (e) {
        console.error('MemberScript #number211: 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.ms211 = { refresh: renderFiles, upload: handleUpload, deleteFile: deleteFile };

  // -- Initial file list load --
  renderFiles();
  console.log('MemberScript #number211: Vercel Blob 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