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.

Previous
Previous

SHOPPING WISH LISTS WITH FIREBASE (PART 2 - Redundant)

Next
Next

Accounts and user collections with “firebase” (Redundant)