SHOPPING WISH LISTS WITH FIREBASE (PART 1)
I have implemented wish list functionality into my store. Wish list functionality allows users to save and manage a list of desired items or products they may want to purchase in the future. By clicking the heart icon in the category view you can add products to, or remove them from the wish list individually, and by clicking the button on the cart page, all items in cart are added to the wish list. I use Firebase for this, which I discussed in the previous post. Below is the code for adding the buttons and handling the functionality. As these buttons are created dynamically, like many of my elements, I use a mutation observer to set their status if the user is logged in.
HTML:
JAVASCRIPT: <script type="module"> // init firebase import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.5/firebase-app.js'; import { getAuth, onAuthStateChanged } from 'https://www.gstatic.com/firebasejs/10.12.5/firebase-auth.js'; import { getFirestore, collection, setDoc, writeBatch, getDocs, getDoc, doc, deleteDoc } from 'https://www.gstatic.com/firebasejs/10.12.5/firebase-firestore.js'; const firebaseConfig = { apiKey: "-----", authDomain: "----", projectId: "----", storageBucket: "----", messagingSenderId: "------", appId: "------", measurementId: "------" }; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // the educated wish document.addEventListener('DOMContentLoaded', function() { const subString = 'cart'; const subStringTwo = 'shop'; const subStringThree = 'shop/p/'; let getURL = window.location.href; let itemId = '0'; var inWishlist = false; let wishlist = []; let productName = "Product"; //Pop-Over init let hideTimeout; var popover = document.getElementById('custom-popover'); var popoverMessage = document.getElementById('popover-message'); function showPopover(message) { popoverMessage.textContent = message; popover.classList.add('show'); popover.style.pointerEvents = 'auto'; clearTimeout(hideTimeout); hideTimeout = setTimeout(function() { popover.classList.remove('show'); popover.style.pointerEvents = 'none'; }, 3000); } //Fetch wishlist async function getWishlist() { const user = auth.currentUser; if (!user) { console.error('User not authenticated when fetching wishlist'); return; } try { const wishlistRef = collection(db, 'users', user.uid, 'wishlist'); const querySnapshot = await getDocs(wishlistRef); wishlist = querySnapshot.docs.map(doc => doc.id); // Get item IDs return wishlist; } catch (error) { console.error('Error fetching wishlist items:', error); return []; // Return an empty array in case of an error } } // add or delete itemID in wishlist on firebase async function addToWishlist(itemId) { const user = auth.currentUser; if (!user) { console.error('User not authenticated'); window.location.href = '/login'; return; } try { const wishlistRef = collection(db, 'users', user.uid, 'wishlist'); const itemDocRef = doc(wishlistRef, itemId); const docSnap = await getDoc(itemDocRef); if (docSnap.exists() && !getURL.includes(subString)) { await deleteDoc(itemDocRef); $('#popoverMessage').off('click'); popoverMessage.addEventListener('click', function() { window.location.href = '/wishlist'; }); popoverMessage.style.color = "#ea4b1a"; showPopover(productName + ' Undesired'); } else { await setDoc(itemDocRef, { itemId: itemId }); $('#popoverMessage').off('click'); popoverMessage.addEventListener('click', function() { window.location.href = '/wishlist'; }); popoverMessage.style.color = "#ffc700"; showPopover(productName + ' Desired'); } } catch (error) { console.error('Error adding item to wishlist:', error.message); } } // Fetch item ID from JSON and add to wishlist async function fetchItemIdAndAddToWishlist(jsonURL) { try { const response = await fetch(jsonURL); if (!response.ok) throw new Error('Failed to fetch JSON data'); const data = await response.json(); const itemId = data.item.id; if (itemId) { productName = "Acquisitions"; await addToWishlist(itemId); } else { console.error('Item ID not found in JSON data'); } } catch (error) { console.error('Error fetching or processing JSON data:', error.message); } } // get cart items info (json link) function getCartContents() { const cartContainer = document.querySelector('.cart-container'); const cartContents = cartContainer.querySelectorAll('.cart-row'); cartContents.forEach(cartRow => { // construct the json url of each product const productDesc = cartRow.querySelector('.cart-row-desc'); const productLinkElement = productDesc.querySelector('a'); const productLink = productLinkElement.getAttribute('href'); const baseURL = 'https://www.polivantage.com'; const jsonURL = `${baseURL}${productLink}?format=json-pretty`; fetchItemIdAndAddToWishlist(jsonURL); }); } if (getURL.includes(subString)) { // Ensure the button is always in the correct place function observeCartContainer() { const observer = new MutationObserver((mutationsList) => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { const addedNodes = Array.from(mutation.addedNodes); addedNodes.forEach(node => { if (node.nodeType === 1 && node.classList.contains('cart-container')) { const existingWishBtns = document.querySelectorAll('.wishlistButton'); if (existingWishBtns.length > 0) { existingWishBtns.forEach(element => { element.remove(); }); } // Create the save to wishlist from cart button const wishlistButton = document.createElement('button'); wishlistButton.id = "wishlistButton"; wishlistButton.classList.add('wishlistButton', 'sqs-block-button-element--medium', 'sqs-block-button-container', 'sqs-button-element--primary', 'sqs-block-button-element'); wishlistButton.type = 'button'; wishlistButton.textContent = 'Add to Wish List'; const lastChild = document.querySelector('.cart-container > :last-child'); if (lastChild) { lastChild.insertAdjacentElement('beforebegin', wishlistButton); } wishlistButton.addEventListener('click', function() { getCartContents(); }); } }); // check for the button to be in right place if (mutation.removedNodes.length > 0) { mutation.removedNodes.forEach(node => { if (node.nodeType === 3) { // Node type 3 is a text node const lastChild = document.querySelector('.cart-container > :last-child'); if (lastChild) { lastChild.insertAdjacentElement('beforebegin', wishlistButton); } } }); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } observeCartContainer(); } else if (getURL.includes(subStringTwo)) { //set intial like button status onAuthStateChanged(auth, async (user) => { if (user) { try { await getWishlist(); function handleHeartDivs() { let allHearts = document.querySelectorAll('.heartDiv'); allHearts.forEach(heart => { let itemId; if (!getURL.includes(subStringThree)) { const gridItem = heart.closest('.grid-item'); itemId = gridItem ? gridItem.getAttribute('data-item-id') : null; } else { const productItem = heart.closest('.ProductItem'); itemId = productItem ? productItem.getAttribute('data-item-id') : null; } if (itemId && wishlist.includes(itemId)) { heart.classList.add('wished'); } else { heart.classList.remove('wished'); } }); } handleHeartDivs(); const observer = new MutationObserver((mutationsList) => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('.heartDiv')) { handleHeartDivs(); } else if (node.querySelectorAll) { node.querySelectorAll('.heartDiv').forEach(heart => { handleHeartDivs(); }); } }); } } }); observer.observe(document.body, { childList: true, subtree: true }); } catch (error) { console.error('Error getting wishlist on auth', error); } } }); // create the quick wishlist icons let allButtonWrappers = document.querySelectorAll('.plp-grid-add-to-cart'); allButtonWrappers.forEach(function(wrapper) { const heartDiv = document.createElement('div'); heartDiv.id = "heartDiv"; heartDiv.classList.add('like-icon', 'heartDiv'); wrapper.insertBefore(heartDiv, wrapper.firstChild); // add to wishlist through quick icon heartDiv.addEventListener('click', function() { // if in category view if (!getURL.includes(subStringThree)) { const gridItem = heartDiv.closest('.grid-item'); const gridItemLink = gridItem.querySelector('a'); productName = gridItemLink.textContent.replace(/€\d+(\.\d+)?/g, '').replace(/[★☆]/g, ''); itemId = gridItem.getAttribute('data-item-id'); } else { // if in product view const productItem = heartDiv.closest('.ProductItem'); const gridItemLink = productItem.querySelector('a'); productName = gridItemLink.textContent.replace(/€\d+(\.\d+)?/g, '').replace(/[★☆]/g, ''); itemId = productItem.getAttribute('data-item-id'); } heartDiv.classList.toggle('wished'); addToWishlist(itemId); }); }); //end create buttons } }); </script>
CSS: .heartDiv.wished { background-image: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/81708e25-5ea7-489e-b938-457024b47773/hearticon1.png'); background-repeat: no-repeat; background-position: center; background-size: contain; } .heartDiv { background-image: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/805dad31-be5b-40f9-9aa9-309c712f7510/hearticonEmpty1.png'); border: 0px !important; width: 25px !important; height: 25px !important; bottom: 85px; right: 5px; position: absolute; padding-left: 10px; pointer-events: auto; cursor: pointer; background-repeat: no-repeat; background-position: center; background-size: contain; transition: all 0.6s ease; scale: 1; z-index: 9999; } .heartDiv:hover { transition: all 0.3s ease; scale: 1.3; } //cart wishlist button .wishlistButton { -webkit-tap-highlight-color: transparent; -moz-tap-highlight-color: transparent; display: block; cursor: pointer; padding-right: 15px !important; padding-top: 10px !important; padding-bottom: 40px !important; margin-top: 30px; min-width: 335px; height: 50px; color: #000000; letter-spacing: 2.5px !important; font-size: 20px; text-align: right !important; position: relative; @media only screen and (min-width:790px) { transform: translateX(0%) translateY(0%) !important; } @media only screen and (max-width:790px) { width: 100%; padding-bottom: 45px !important; padding-left: 44% !important; margin-bottom: -5px; } }
This post has been updated to reflect a more complete overview of the recently added functionality and other improvements.
COMMENTS:
Leave a comment: