How to create a save/favorite function in Webflow with Memberstack that requires login and persists user selections? Answered

Post author
Michael Ola

Hey team.

I have a memberstack powered membership webflow site. I want to have a Save/Favorite function where by when a user who is not logged in click on Save/Favorite button, the user is required to signup/login and their chosen template is on a Save/Favorite page.

Is this possible with Memberstack?

Comments

9 comments

  • Comment author
    A J

    Hey Michael Ola, off the top of my head, I believe the simplest approach would be to show a set of save / favorite buttons for the logged out users (which is hidden for logged in users) and have a custom attribute enabled to be able to redirect the users back to the same page they were trying to save an item from, and the users could then just save it (via the buttons that show up for logged in users) once they login / signup.

    0
  • Comment author
    Duncan from Memberstack

    šŸ‘ to what A J said! And here's a tutorial on how to enable the like/save functionality. https://www.memberstack.com/scripts/save-unsave-items-to-your-collection

    0
  • Comment author
    Michael Ola

    Thank you so much

    Duncan Hamra I find the above tutorial to be very helpful.

    I noticed when a user is not logged in, it doesn't regard their saved item. Is it possible for it to be regarded and have them signup/login when they try to save when not logged in??

    0
  • Comment author
    A J

    Michael Ola, this feature only works for logged in users as of now since Memberstack needs the member data to store their favorites for them uniquely. So, as a workaround you could copy paste those unsave buttons and have them visible for logged out users alone and link them to the signup / login page as suggested earlier. That way, users will signup / login and come back to the same page where they were trying to add the favorites and can proceed by saving the items of their choice. This is the simplest approach I have seen other users setup previously.

    If you don't mind custom code, you could further customize and come up with a code to possibly save the item slug in local storage and see if you can push them to the member JSON, once they have logged in etc. I have not explored this use-case, so not sure how far it is feasible, so feel free to further research on what approach would work if you don't prefer the simpler approach shared earlier.

    0
  • Comment author
    Michael Ola

    Duncan Hamra This video (https://www.memberstack.com/scripts/save-unsave-items-to-your-collection) has been very helpful. There's another function required which is not stated and I need help with.

    On the my collection page (the page showing saved items) I want the user to be able to click and link back to the CMS template page. Kindly assist.

    0
  • Comment author
    Chukwudi

    I’ve updated both theĀ save logicĀ and theĀ collection rendering logicĀ so saved items link back to their CMS page.When usersĀ save items, the script stores the CMS page link (ms-code-link), and on the collection page,Ā images are wrapped inĀ <a>Ā tagsĀ that link back to the CMS template page. If no link was saved, it defaults toĀ "#".

    <!-- šŸ’™ MEMBERSCRIPT #150 v0.2 šŸ’™ - SAVE, UNSAVE, AND LINK ITEMS TO CMS PAGES -->
    <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 link = btn.getAttribute('ms-code-link') || '#';
        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, link });
          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 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));
      };
    
      await fetchSavedItems();
      updateButtons();
      attachListeners();
    });
    </script>
    
    <!-- šŸ’™ COLLECTIONS DISPLAY WITH CMS LINK SUPPORT -->
    <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 in 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 = '';
    
      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.length})`;
    
        const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
        const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
        if (imageTemplate) imageTemplate.style.display = 'none';
    
        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 = '100%';
          imgClone.style.height = 'auto';
          imgClone.style.maxWidth = '100%';
    
          const linkEl = document.createElement('a');
          linkEl.href = item.link || '#';
          linkEl.style.display = 'block';
          linkEl.style.textDecoration = 'none';
    
          linkEl.appendChild(imgClone);
          imageContainer.appendChild(linkEl);
        });
    
        wrapper.appendChild(folderClone);
      });
    });
    </script>
    

    That said, an example Save button would look like this:

    <button 
      ms-code-add-button 
      ms-code-save="item-123" 
      ms-code-category="Posters" 
      ms-code-link="/posters/item-123"
    >
      Save
    </button>
    
    0
  • Comment author
    Michael Ola

    <button
    ms-code-add-button
    ms-code-save="item-123"
    ms-code-category="Posters"
    ms-code-link="/posters/item-123"
    >
    Save
    </button>

    Do I need to add the attributes above anywhere?Also which item is made Link block or it doesn't matter be it div block or link block.

    0
  • Comment author
    A J

    HeyĀ Michael Ola, just made a minor change in the code shared by Chukwudi. So let me share the steps here:

    1. In theĀ saved items page, replace the memberscript with the following (same as provided above):
    <!-- šŸ’™ COLLECTIONS DISPLAY WITH CMS LINK SUPPORT -->
    <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 in 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 = '';
    
      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.length})`;
    
        const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
        const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
        if (imageTemplate) imageTemplate.style.display = 'none';
    
        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 = '100%';
          imgClone.style.height = 'auto';
          imgClone.style.maxWidth = '100%';
    
          const linkEl = document.createElement('a');
          linkEl.href = item.link || '#';
          linkEl.style.display = 'block';
          linkEl.style.textDecoration = 'none';
    
          linkEl.appendChild(imgClone);
          imageContainer.appendChild(linkEl);
        });
    
        wrapper.appendChild(folderClone);
      });
    });
    </script>
    

    2. In theĀ main pageĀ where all items are listed, replace the memberscript you have with the following:

    <!-- šŸ’™ MEMBERSCRIPT #150 v0.2 šŸ’™ - SAVE, UNSAVE, AND LINK ITEMS TO CMS PAGES -->
    <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 prefix = 'COLLECTION-SLUG/'; 
            const link = id ? (prefix + id) : '#';
        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, link });
          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 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));
      };
    
      await fetchSavedItems();
      updateButtons();
      attachListeners();
    });
    </script> 
    

    In this memberscript, replace 'COLLECTION-SLUG/' with your actual collection slug. For example, say your collection item's url is '_some-site.webflow.io/images/cat-8'. _So in the above code you will replace 'COLLECTION-SLUG/' with 'images/'

    That's it. Nothing to change on the buttons or no need to add any link blocks. The layout can be as it is and the code should help you achieve the use-case.

    Let me know if you face any blockers.

    0
  • Comment author
    Michael Ola

    I have implemented it as described here but it is not working.

    But it’s been sorted by A J

    Thank you Duncan Hamra

    0

Please sign in to leave a comment.