Maropost Wishlist

#1. Use Case

1684

A nifty little functionality. In case you're using wish lists on your store, these can be added to your Findify-powered pages as well. In this guide we'll be showing how you create the basic functionality that can then be further customised.

#2. Requirements for applying

  • Applicable for Maropost, obviously.
  • This guide requires an existing wish list functionality on your store, we don't build it from scratch.

#3. Time Estimates

  • Set up in Platform: n/a hours
  • Integration: 30 mins
  • Styling: 5 mins

#4. Functional Overview

The list of the components that are to be adjusted is as follows:

📘

Components

#5. Integration Steps

There'll be a plenty of coding needed to be done, hopefully you're seated comfortably :)

First of all, we have to create the basic structure of WishList Component and place it in components/Cards/Product/index.tsx. We need to push props we need to WishList (only item is required).

import WishList from 'WishList'
// some code

export default ({
  item,
  theme = styles,
  className,
  config,
  Container = 'div'
}: IProductCardProps) => {
  
  // your code

  return (
    <Container
      ref={container}
      data-element="card"
      className={cx(theme.root, theme[config.get('template')], className)}
    >
        // your code
        
        <WishList
          isMobile={isMobile}
          item={variant}
        />
            
        // your code
    </Container>
  );
};

WishList structure may vary depending on your needs. However, we need to create some states to know if the item is in the WishList, collect some WishListData, and check if the user is logged in.

import React, { useState } from 'react'
import cx from 'classnames'

export default ({ isMobile, item }) => {
  // creating a state to save wishList data
  const [wishListData, setWishListData] = useState(false)
  // creating a state to know if the item is in the wishList
  const [inWishlist, setInwishlist] = useState(false)
  // creating a state to know if user is logged
  const [requireLogin, setRequireLogin] = useState(false)
  const id = item.get('id')

  return (
    <>
      <a className="wishlist_toggle" 
         display-if={!isMobile} // 100%  optinal 
         className={cx("findify-components--cards--product__wishlist", inWishlist === true && "findify-components--cards--product__wishlist__in-wish-list")}>
        
        <span >      
         // here we need to add icon we need            
          <i className={cx("fa", isInWishlist ? "fa-heart" : "fa-heart-o")} aria-hidden="true"> </i>
        </span>
      </a>
    </>
  )
}

The next step is to create a function to check if the item is in WishList. We need to use the standard getWishlistItems function. It will be better to create a separate CTAHandlers.tsx file for this.

Also, we create netoResponseParser to parse the message.

import axios from "axios";

export default () => {
  const getWishlistItems = async (itemId, netoResponseParser, setInwishlist, setRequireLogin) => {
    const message = await axios({
      method: 'POST',
      url: '/ajax/wishlist',
      params: {
        proc: 'AddItem',
        sku: itemId
      },
      headers: {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
      }
    });

  	if (message.data.includes('REQUIRE_LOGIN')) {
      return
    }
    if(message,status == "200"){
      const keys = ['active', 'id', 'name']
      const wishlist = netoResponseParser(message.data, keys)
      const elementsInWishlist = wishlist.filter(item => item.active === 'y')
      setInwishlist(elementsInWishlist[0]?.active ? true : false)
    }

  }

  const transformItems = (arr, keys) => {
    const array = arr.reduce((acc, el, i) => {
      keys.includes(el) && acc.push({ [el]: arr[i + 1] })
      return acc
    }, [])
    const transformedArray = []
    for (let i = 0; i < array.length; i += 3) {
      transformedArray.push(array.slice(i, i + 3))
    }
    return transformedArray.map(item => {
      return item.reduce((acc, el) => { return Object.assign(acc, el) }, {})
    })
  }

  const findIdx = (arr, key, item) => arr.findIndex(i => { return !item ? i === key : i[item] === key })

  const netoResponseParser = (responseBody, keys) => {
    const arr = responseBody.split('|').map(item => {
      item = item.split('')
      if (item.includes('$')) { item.splice(findIdx(item, '$')) }
      if (item.includes('#')) { item.splice(findIdx(item, '#')) }
      return item.join('')
    })
    return transformItems(arr, keys)
  }
  
  return {
    netoResponseParser,
    getWishlistItems,
    findIdx
  }
 }

Then we can use the function in WishList inner useEffect.

import React, { useState, useEffect } from 'react'
import cx from 'classnames'
import CTAHandlers from 'CTAHandlers'

const { netoResponseParser, getWishlistItems } = CTAHandlers()

export default ({ isMobile, item }) => {
  // creating a state to save the list of wishList items 
  const [wishListData, setWishListData] = useState(false)
  // creating a state to know if the item is in the wishList
  const [inWishlist, setInwishlist] = useState(false)
  // creating a state to know if user is logged
  const [requireLogin, setRequireLogin] = useState(false)
  const id = item.get('id')
  
  useEffect(() => {
    getWishlistItems(id, netoResponseParser, setInwishlist, setRequireLogin)
  }, [wishListData])

  return (
    <>
      <a className="wishlist_toggle" 
         display-if={!isMobile} // optional 
         className={cx("findify-components--cards--product__wishlist", inWishlist === true && "findify-components--cards--product__wishlist__in-wish-list")}>
        
        <span >      
         // here we need to add icon we need            
          <i className={cx("fa", isInWishlist ? "fa-heart" : "fa-heart-o")} aria-hidden="true"> </i>
        </span>
      </a>
    </>
  )
}

After that we're adding AddToWishList function and handleAnalytics in CTAHandlers. Don't forget to return the functions.

const addToWishList = async(id, netoResponseParser, handleOverlay, setWishListData) => {
  const message = await axios({
    method: 'POST',
    url: '/ajax/wishlist',
    params: {
      proc: 'AddItem',
      sku: itemId
    },
    headers: {
      'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
    }
  });
  if(message.status == "200"){
    const keys = ['active', 'id', 'name']
    const wishlist = netoResponseParser(message.data, keys)
    const model = document.querySelector(`#${id}`).innerText
    const modal = true
    handleOverlay('inject', 'wishlist--modal-overlay'); // we need this to open the WishlistModal
    setWishListData({ wishlist, model, modal })
  }
};


const handleAnalytics = async (item, cta) => {
  const item_id = item.get('id')
  const variant_item_id = item.get('selected_variant_id');
  await item.analytics.sendEvent('click-item', { item_id, variant_item_id, rid: item.meta.toJS().rid });
  cta && await item.analytics.sendEvent(cta, { item_id, variant_item_id, quantity: 1, rid: item.meta.toJS().rid });
}

//  we don`t need this all time, just in case when we need to create WishlistModal manually
const handleOverlay = (action, className) => {
  const handleOverflow = state => document.body.style.overflow = state
  if (action === 'inject') {
    const el = document.createElement('div');
    handleOverflow('hidden')
    el.classList.add(className);
    document.body.appendChild(el)
  } else {
    const el = document.querySelector(`.${className}`)
    handleOverflow('initial')
    el?.remove()
  }
}

We need to use the function in WishList.

import React, { useState, useMemo } from 'react'
import CTAHandlers from 'CTAHandlers'
import cx from 'classnames'

const { handleOverlay, addToWishList, netoResponseParser, getWishlistItems, handleAnalytics } = CTAHandlers()

export default ({ isMobile, item }) => {
  const [wishListData, setWishListData] = useState(false)
  // your code
  return (
    <>
      <a className="wishlist_toggle" 
         onClick={() => {addToWishList(id, netoResponseParser, handleOverlay, setWishListData); handleAnalytics(item, 'add-to-wishlist')}} 
         display-if={!isMobile} 
         className={cx("findify-components--cards--product__wishlist", inWishlist === true && "findify-components--cards--product__wishlist__in-wish-list")}>
        // your code
      </a>
    </>
  )
}

Sometimes we don't need to create WishlistModal and open it. Firstly, we need to check the structure for WishList on the production store. Then we need to duplicate the basic structure in to our store. And then check if it all works as expected. If the modal doesn't appear after clicking on WishListIcon, then we create a WishlistModal, which we will show using createPortal.

import React, { useState } from 'react'
import CTAHandlers from 'CTAHandlers'

const { handleClickOutsideOfModal, findIdx, handleWishlistRequests, handleOverlay, addToNewList } = CTAHandlers()

export default ({ data, model, setWishListData, setInwishlist, itemId, requireLogin }) => {
  const [inputValue, setInputValue] = useState('')
  const [openedInput, setOpenedInput] = useState(false)
  const authLink = 'https://www.bits4blokes.com.au/_myacct/'
  const closeModal = () => {
    handleOverlay('remove', 'wishlist--modal-overlay')
    setWishListData(prev => ({ ...prev, modal: false }))
  }

  const handleOptions = (wishlistId, active) => {
    const idx = findIdx(data, wishlistId, 'id')
    if (active === 'n') {
      data[idx].active = 'y'
      setInwishlist(true)
      handleWishlistRequests(itemId, wishlistId, 'AddItem', 'Item was successfully added!')
    } else {
      data[idx].active = 'n'
      setInwishlist(false)
      handleWishlistRequests(itemId, wishlistId, 'RemoveItem', 'Item was successfully removed!')
    }
    setWishListData(prev => ({ ...prev, wishlist: data }))

  }

  handleClickOutsideOfModal(closeModal, 'wishlist--modal-overlay')

  return (
    <div className="findify-modal__wrapper">
      <span className="npopup-btn-close" onClick={() => closeModal()}></span>
      {!requireLogin ?
        <>
          <span className="findify-modal__header">Add or Remove {model} From Wishlist</span>
          <div className="findify-modal__body">
            {data.map(item => {
              return (
                <div className="findify-modal__item">
                  <span>{item.name}</span>
                  <input
                    type="checkbox"
                    defaultChecked={item.active === 'y'} /* n - stands for no and y - yes*/
                    value={item.id}
                    onChange={() => handleOptions(item.id, item.active)} />
                </div>
              )
            })}
            <span
              onClick={() => setOpenedInput(prev => !prev)}
              className="findify-wishlist__add-new-list">
              Or Add To A New List
          </span>
            <span display-if={openedInput} className="findify-wishlist__add-new-list-input">
              New List Name: <input onChange={e => setInputValue(e.target.value)} />
            </span>
            <hr />
          </div>
          <button
            className="findify-wishlist__save-changes"
            onClick={
              () => openedInput ?
                addToNewList(itemId, inputValue, closeModal, `Item was added to ${inputValue} wishlist`) :
                closeModal()
            }>
            {!openedInput ? 'Save My Wishlist Changes' : 'Add New Wishlist And Save My Changes'}
          </button>
        </> :
        <span>
          You must first <a href={authLink}>login</a> or <a href={authLink}>create an account</a> to add to a Wishlist
        </span>
      }
    </div>
  )
}

We use handleClickOutsideOfModal and handleWishlistRequests, which we can create in CTAhandlers. Finally, it will look like this:

import axios from "axios";
export default () => {
  /* add or remove 1 item */
  const handleWishlistRequests = async(itemId, wishListId, action, message) => {
    const data = await axios({
      method: 'POST',
      url: '/ajax/wishlist',
      params: {
        proc: action, 
        sku: itemId,
        wishlist: wishListId
      },
      headers: {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
      }
    });
    if(data.status == "200"){
      console.log(data.data, message)
    }
  }
  /* initialize wishlist api */
  const getWishlistItems = async (itemId, netoResponseParser, setInwishlist, setRequireLogin) => {
    const message = await axios({
      method: 'POST',
      url: '/ajax/wishlist',
      params: {
        proc: 'AddItem',
        sku: itemId
      },
      headers: {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
      }
    });

  	if (message.data.includes('REQUIRE_LOGIN')) {
      return
    }
    if(message,status == "200"){
      const keys = ['active', 'id', 'name']
      const wishlist = netoResponseParser(message.data, keys)
      const elementsInWishlist = wishlist.filter(item => item.active === 'y')
      setInwishlist(elementsInWishlist[0]?.active ? true : false)
    }

  }
  /* add/remove window overlay */
  const handleOverlay = (action, className) => {
    const handleOverflow = state => document.body.style.overflow = state
    if (action === 'inject') {
      const el = document.createElement('div');
      handleOverflow('hidden')
      el.classList.add(className);
      document.body.appendChild(el)
    } else {
      const el = document.querySelector(`.${className}`)
      handleOverflow('initial')
      el?.remove()
    }
  }
  /* fire close modal func if click was outside of modal window */
  const handleClickOutsideOfModal = (closeModal, className) => {
    const modal = document.querySelector(`.${className}`)
    if (modal !== null) {
      modal.addEventListener('click', e => {
        if (modal?.contains(e.target)) {
          closeModal()
        }
      })
    }
  }
  /* initialize adding of new item and open modal */
  const addToWishList = async(id, netoResponseParser, handleOverlay, setWishListData) => {
    const message = await axios({
      method: 'POST',
      url: '/ajax/wishlist',
      params: {
        proc: 'AddItem',
        sku: itemId
      },
      headers: {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
      }
    });
    if(message.status == "200"){
      const keys = ['active', 'id', 'name']
      const wishlist = netoResponseParser(message.data, keys)
      const model = document.querySelector(`#${id}`).innerText
      const modal = true
      handleOverlay('inject', 'wishlist--modal-overlay'); // we need this to open the WishlistModal
      setWishListData({ wishlist, model, modal })
    }
  };
  /* create new wishlsit and add item to it*/
  const addToNewList = async(itemId, newListName, callback, message) => {
    const message = await axios({
      method: 'POST',
      url: '/ajax/wishlist',
      params: {
        proc: 'AddItem',
        sku: itemId,
        name: newListName,
        wishlist: -1
      },
      headers: {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
      }
    });
    if(message.status == "200"){
      callback()
    }
  }
  /*generate array of items in wishlist with the following values {active: y or n (yes or no, e.i in wishlist or not in it), id: wishlistId, name: wishListName}*/
  const transformItems = (arr, keys) => {
    const array = arr.reduce((acc, el, i) => {
      keys.includes(el) && acc.push({ [el]: arr[i + 1] })
      return acc
    }, [])
    const transformedArray = []
    for (let i = 0; i < array.length; i += 3) {
      transformedArray.push(array.slice(i, i + 3))
    }
    return transformedArray.map(item => {
      return item.reduce((acc, el) => { return Object.assign(acc, el) }, {})
    })
  }

  const findIdx = (arr, key, item) => arr.findIndex(i => { return !item ? i === key : i[item] === key })
  /* parse response from post request to Neto, remove unnecessary symbols and words and make new array based on required values in transformItems func. */
  const netoResponseParser = (responseBody, keys) => {
    const arr = responseBody.split('|').map(item => {
      item = item.split('')
      if (item.includes('$')) { item.splice(findIdx(item, '$')) }
      if (item.includes('#')) { item.splice(findIdx(item, '#')) }
      return item.join('')
    })
    return transformItems(arr, keys)
  }
  /* handle CTA events */
  const handleAnalytics = async (item, cta) => {
    const item_id = item.get('id')
    const variant_item_id = item.get('selected_variant_id');
    await item.analytics.sendEvent('click-item', { item_id, variant_item_id, rid: item.meta.toJS().rid });
    cta && await item.analytics.sendEvent(cta, { item_id, variant_item_id, quantity: 1, rid: item.meta.toJS().rid });
  }

  return {
    handleOverlay,
    handleWishlistRequests,
    addToWishList,
    addToNewList,
    netoResponseParser,
    getWishlistItems,
    handleClickOutsideOfModal,
    findIdx,
    handleAnalytics
  }
}

and WishList:

import React, { useState, useMemo } from 'react'
import { createPortal } from 'react-dom'
import CTAHandlers from 'CTAHandlers'
import WishlistModal from 'WishlistModal'
import cx from 'classnames'

const { handleOverlay, addToWishList, netoResponseParser, getWishlistItems, handleAnalytics } = CTAHandlers()

export default ({ isMobile, item }) => {
  const [wishListData, setWishListData] = useState(false)
  const [inWishlist, setInwishlist] = useState(false)
  const [requireLogin, setRequireLogin] = useState(false)
  const id = item.get('id')

	useEffect(() => {
    getWishlistItems(id, netoResponseParser, setInwishlist, setRequireLogin)
  }, [wishListData])
  return (
    <>
      {
        wishListData.modal === true && createPortal(
         <WishlistModal
          data={wishListData.wishlist}
          model={wishListData.model}
          modal={wishListData.modal}
          setWishListData={setWishListData}
          setInwishlist={setInwishlist}
          requireLogin={requireLogin}
          itemId={id}
        />, document.querySelector('body')
        )
      }
      <a className="wishlist_toggle" 
         onClick={() => {addToWishList(id, netoResponseParser, handleOverlay, setWishListData); handleAnalytics(item, 'add-to-wishlist')}} 
         display-if={!isMobile} 
         className={cx("findify-components--cards--product__wishlist", inWishlist === true && "findify-components--cards--product__wishlist__in-wish-list")}>
        <span >
         <i className={cx("fa", isInWishlist ? "fa-heart" : "fa-heart-o")} aria-hidden="true"> </i>
        </span>
      </a>
    </>
  )
}

#5. MJS Version

This module has been optimized for MJS version 7.1.42