Maropost Wishlist
#1. Use Case
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
- [components/Cards/Product/index.tsx] (https://github.com/findify/findify-js/blob/develop/packages/react-components/src/components/Cards/Product/index.tsx)
#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
Updated 3 months ago