How to create user likes/bookmarks with no external tools. Just Webflow and Memberstack.

Post author
Daniel Thomas

Hi All

I wanted to create some documentation to explain a process I've recently worked on, since I didn't find a 'how-to' for this using Memberstack 2.0. This guide might be useful for those who, like me, aren't coding experts.

Here's how my 'like' button system operates, using my classes and attribute names:

Firstly, you need to create the CMS collection list from which you'd like users to be able to 'like' items. Every CMS item should be equipped with a 'like' button.

Assign the buttons a class 'like-button'. This button needs to have a custom attribute (data-cms-item="Unique CMS Item ID"). This "Unique CMS Item ID" must be a unique identifier for each item and should be linked to a Webflow CMS attribute. This can be done by clicking on the little purple button.

When the 'like' button is pressed, the combo class 'is-liked' is appended to the button via the custom code (provided below). Prior to publishing the site, pre-style this class and remove it from the button.

The following JavaScript code reads the user's JSON file stored in Memberstack (assuming the user is logged in). In the JSON file, it either creates or adds to the array 'likes', using the Unique CMS Item ID value. Furthermore, the code checks each 'like' button and will append the class 'is-liked' if it's present in the user's 'likes' array.

Here's the code:

<script>
document.addEventListener('DOMContentLoaded', (event) => {
    const memberstack = window.$memberstackDom;

    // Get all the like buttons
    let likeButtons = document.querySelectorAll('.like-button');

    // Add an event listener to each like button
    likeButtons.forEach(likeButton => {
        let likeButtonLastClickedAt = 0;

        likeButton.addEventListener('click', async function () {
            // If the last click was less than 500ms ago, ignore this one
            const now = Date.now();
            if (now - likeButtonLastClickedAt < 500) {
                return;
            }
            likeButtonLastClickedAt = now;

            // Optimistically update the UI
            const cmsData = this.getAttribute('data-cms-item');
            if (this.classList.contains('is-liked')) {
                this.classList.remove('is-liked');
            } else {
                this.classList.add('is-liked');
            }

            // Get the current member
            let member = await memberstack.getCurrentMember();

            // If the member is not logged in, redirect to /signup
            if (!member || !member.data) {
                window.location.href = '/signup';
                return;
            }

            // Get current member's JSON
            let memberJson = await memberstack.getMemberJSON();

            // Unwrap unnecessary "data" objects
            while (memberJson.data) {
                memberJson = memberJson.data;
            }

            // Create the likes array if it doesn't exist
            if (!memberJson.likes) {
                memberJson.likes = [];
            }

            // Check if cmsData is already in the array, if not, add it
            if (!memberJson.likes.includes(cmsData)) {
                memberJson.likes.push(cmsData);
            } else {
                // If the item is already liked, remove it from the array
                memberJson.likes = memberJson.likes.filter(item => item !== cmsData);
            }

            // Update member's JSON asynchronously
            memberstack.updateMemberJSON({ json: memberJson }).catch((error) => {
                console.error("Failed to update member JSON: ", error);
                // If there was an error updating the JSON, revert the button style
                if (this.classList.contains('is-liked')) {
                    this.classList.remove('is-liked');
                } else {
                    this.classList.add('is-liked');
                }
            });
        });

        // Initialise the button state based on the member's likes
        (async function initButtonState() {
            // Get the current member
            let member = await memberstack.getCurrentMember();

            // If the member is not logged in, do nothing
            if (!member || !member.data) {
                return;
            }

            let memberJson = await memberstack.getMemberJSON();

            // Unwrap unnecessary "data" objects
            while (memberJson.data) {
                memberJson = memberJson.data;
            }

            let cmsData = likeButton.getAttribute('data-cms-item');

            // If the member has already liked the item, style the button as 'is-liked'
            if (memberJson.likes && memberJson.likes.includes(cmsData)) {
                likeButton.classList.add('is-liked');
            }
        })();
    });
});
</script>

Now that we have stored the user's 'likes', we can display these 'likes' on a separate page.

Using JavaScript, we can use conditional visibility on the Webflow CMS list to hide items if they don't match a value from the user's 'likes' array in JSON. To set this up, I duplicated the initial CMS list onto a new page, e.g., "Your Likes". Each item therefore already had the custom attribute (data-cms-item="Unique CMS Item ID"). It's important to note that all CMS items will load on the "Your Likes" page, however, we're merely hiding ones that don't match the value in 'likes'. For the following code to work in its current state, the individual CMS item class should be called 'featured-frequencies-card'. (the item we are hiding)

Here is the code:

<script>
document.addEventListener('DOMContentLoaded', async () => {
    const memberstack = window.$memberstackDom;
    let memberJson = await memberstack.getMemberJSON();
    
    // Unwrap unnecessary "data" objects
    while (memberJson.data) {
        memberJson = memberJson.data;
    }

    // Assuming each CMS item has a data attribute 'data-cms-item' 
    // which matches the names stored in the user's 'likes' array
    let cmsItems = document.querySelectorAll('.featured-frequencies-card');

    cmsItems.forEach(item => {
        let itemName = item.getAttribute('data-cms-item');

        // If the user has not liked this item, hide it
        if (!memberJson.likes.includes(itemName)) {
            item.style.display = 'none';
        }
    });
});
</script>

Also, on the "Your Likes" page, you can append the 'is-liked' class to the buttons. This will cause them to automatically appear as 'liked'

Note: If a user is not logged in and they press the like button, they are redirected to /signup
Notes: Just updated the first code to include some error handling, and to apply the "is-liked" class optimistically, before the computation is done.

Hope this helped!


as I said I'm not a coder.
I'm just a guy with GPT 4 and some time.

Comments

5 comments

  • Comment author
    Duncan Hamra

    Fantastic!! You just saved me (and others) a ton of time. Thanks so much for documenting and sharing this solution 🙏

    0
  • Comment author
    Josh Lopez

    This has been added to the help center! Thank you again Daniel Thomas. 🙏

    https://docs.memberstack.com/hc/en-us/articles/22268128435739

    0
  • Comment author
    Josh Dean

    Hi Josh, just to let you know, the code Daniel sent in this thread works.  But the code you added to the help center doesn't.  Just thought I'd let you know!

    0
  • Comment author
    Josh Dean

    Hi Josh, just to let you know, the code Daniel sent in this thread works.  But the code you added to the help center doesn't.  Just thought I'd let you know!

    0
  • Comment author
    Josh Lopez

    Thank you Josh Dean. It looks like the editor stripped the arrow functions somehow. I just updated the doc.

    0

Please sign in to leave a comment.