#150 - Save and Unsave Items to Your Collection (Pinterest-style)

A simple save/unsave system that lets members bookmark items into personal collections.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

274 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const member = await ms.getCurrentMember();
  const isLoggedIn = !!member;
  let savedItems = {};

  const fetchSavedItems = async () => {
    try {
      const { data } = await ms.getMemberJSON();
      savedItems = data.savedItems || {};
    } catch {
      savedItems = {};
    }
  };

  const persistSavedItems = async () => {
    try {
      await ms.updateMemberJSON({ json: { savedItems } });
    } catch (err) {
      console.error("Error saving items:", err);
    }
  };

  const updateButtons = () => {
    document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
      const id = btn.getAttribute('ms-code-save');
      const category = btn.getAttribute('ms-code-category');
      const exists = savedItems[category]?.some(i => i.id === id);
      btn.style.display = exists ? 'none' : 'inline-block';
    });

    document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
      const id = btn.getAttribute('ms-code-unsave');
      const category = btn.getAttribute('ms-code-category');
      const exists = savedItems[category]?.some(i => i.id === id);
      btn.style.display = exists ? 'inline-block' : 'none';
    });
  };

  const onAddClick = async (e) => {
    e.preventDefault();
    if (!isLoggedIn) return;

    const btn = e.currentTarget;
    const container = btn.closest('[ms-code-save-item]');
    const id = btn.getAttribute('ms-code-save');
    const category = btn.getAttribute('ms-code-category');
    const img = container?.querySelector('[ms-code-image]');
    const url = img?.src;

    if (!savedItems[category]) savedItems[category] = [];
    if (!savedItems[category].some(i => i.id === id)) {
      savedItems[category].push({ id, url });
      updateButtons();
      await persistSavedItems();
    }
  };

  const onUnsaveClick = async (e) => {
    e.preventDefault();
    if (!isLoggedIn) return;

    const btn = e.currentTarget;
    const id = btn.getAttribute('ms-code-unsave');
    const category = btn.getAttribute('ms-code-category');

    if (savedItems[category]) {
      savedItems[category] = savedItems[category].filter(i => i.id !== id);
      if (savedItems[category].length === 0) delete savedItems[category];
      updateButtons();
      await persistSavedItems();
    }
  };

  const onDownloadClick = (e) => {
    e.preventDefault();
    const btn = e.currentTarget;
    const container = btn.closest('[ms-code-save-item]');
    const img = container?.querySelector('[ms-code-image]');
    const url = img?.src;

    if (url) {
      const a = document.createElement('a');
      a.href = url;
      a.download = '';
      document.body.appendChild(a);
      a.click();
      a.remove();
    }
  };

  const attachListeners = () => {
    document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
    document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
    document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
  };

  await fetchSavedItems();
  updateButtons();
  attachListeners();
});
</script>

<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
  setTimeout(function() {
    function resizeGridItem(item) {
      grid = document.getElementsByClassName("grid")[0];
      rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
      rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
      rowSpan = Math.ceil((item.querySelector('.propcontent').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
      item.style.gridRowEnd = "span " + rowSpan;
    }

    function resizeAllGridItems() {
      allItems = document.getElementsByClassName("item");
      for (x = 0; x < allItems.length; x++) {
        resizeGridItem(allItems[x]);
      }
    }

    function resizeInstance(instance) {
      item = instance.elements[0];
      resizeGridItem(item);
    }

    window.onload = resizeAllGridItems();
    window.addEventListener("resize", resizeAllGridItems);

    allItems = document.getElementsByClassName("item");
    for (x = 0; x < allItems.length; x++) {
      imagesLoaded(allItems[x], resizeInstance);
    }

    setTimeout(function() { resizeInstance() }, 100);
  }, 800);
})
</script>

<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const wrapper = document.querySelector('[ms-code-collections-wrapper]');
  const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
  const emptyState = document.querySelector('[ms-code-empty]');
  if (!wrapper || !template) return;

  let member;
  try {
    member = await ms.getCurrentMember();
  } catch {
    wrapper.textContent = "Please log keywordin to view your collections.";
    return;
  }

  let savedItems = {};
  try {
    const { data } = await ms.getMemberJSON();
    savedItems = data?.savedItems || {};
  } catch {
    wrapper.textContent = "Could not load your collections.";
    return;
  }

  if (Object.keys(savedItems).length === 0) {
    wrapper.innerHTML = '';
    if (emptyState) emptyState.style.display = 'block';
    return;
  }

  if (emptyState) emptyState.style.display = 'none';
  wrapper.innerHTML = '';

  const persistSavedItems = async () => {
    try {
      await ms.updateMemberJSON({ json: { savedItems } });
    } catch (err) {
      console.error("Failed to save", err);
    }
  };

  const updateButtons = (modal, id, category) => {
    const addBtn = modal.querySelector('[ms-code-add-button]');
    const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
    const exists = savedItems[category]?.some(item => item.id === id);
    addBtn.style.display = exists ? 'none' : 'inline-block';
    unsaveBtn.style.display = exists ? 'inline-block' : 'none';
  };

  Object.entries(savedItems).forEach(([category, items]) => {
    const folderClone = template.cloneNode(true);
    const titleEl = folderClone.querySelector('[ms-code-folder-title]');
    if (titleEl) titleEl.textContent = `${category} (${items.proplength})`;

    const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
    const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
    if (imageTemplate) imageTemplate.style.display = 'none';

    const modal = folderClone.querySelector('[ms-code-modal]');
    const modalImg = folderClone.querySelector('[ms-code-modal-img]');
    const modalClose = folderClone.querySelector('[ms-code-modal-close]');
    const addButton = folderClone.querySelector('[ms-code-add-button]');
    const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
    const downloadButton = folderClone.querySelector('[ms-code-download-button]');
    const hiddenImage = folderClone.querySelector('[ms-code-image]');

    items.forEach(item => {
      const imgClone = imageTemplate.cloneNode(true);
      imgClone.src = item.url;
      imgClone.alt = category;
      imgClone.style.display = 'block';
      imgClone.style.objectFit = 'cover';
      imgClone.style.width = 'number100%';
      imgClone.style.height = 'auto';
      imgClone.style.maxWidth = 'number100%';

      imgClone.addEventListener('click', () => {
        if (modal && modalImg) {
          modalImg.src = item.url;
          if (hiddenImage) hiddenImage.src = item.url;

          const id = item.id;
          addButton.onclick = async (e) => {
            e.preventDefault();
            savedItems[category] = savedItems[category] || [];
            if (!savedItems[category].some(i => i.id === id)) {
              savedItems[category].push({ id, url: item.url });
              await persistSavedItems();
              updateButtons(modal, id, category);
            }
          };

          unsaveButton.onclick = async (e) => {
            e.preventDefault();
            savedItems[category] = savedItems[category].filter(i => i.id !== id);
            if (savedItems[category].length === 0) delete savedItems[category];
            await persistSavedItems();
            modal.style.display = 'none';
            location.reload();
          };

          downloadButton.onclick = (e) => {
            e.preventDefault();
            const a = document.createElement('a');
            a.href = item.url;
            a.download = '';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
          };

          updateButtons(modal, id, category);
        }
        modal.style.display = 'flex';
      });

      imageContainer.appendChild(imgClone);
    });

    if (modal && modalClose) {
      modalClose.addEventListener('click', () => {
        modal.style.display = 'none';
        if (modalImg) modalImg.src = '';
      });
    }

    wrapper.appendChild(folderClone);
  });
});
</script>

Script Info

Versionv0.1
PublishedNov 11, 2025
Last UpdatedNov 11, 2025

Need Help?

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

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in JSON