Strapi Plugin Development – Abandoned Cart 1/2

Prerequisites

To build this project, you need to:

    1. Download and install Node.js (version 14 or 16 is recommended by Strapi).
    2. Have npm (version 6 only) or yarn to run the CLI installation scripts.
    3. Have a basic knowledge of JavaScript and Vue.js.

We assume you have a Strapi project and you are located inside of it from your terminal. From there, you can simply generate a plugin using the generate CLI command.

The completed project is available in this GitHub repository.

 

Create a plugin

Strapi provides a command line interface (CLI) for creating plugins. To create a plugins

    1. Navigate to the root of a Strapi project.
    2. Run yarn strapi generate or npm run strapi generate in a terminal window to start the interactive CLI.
    3. Choose “plugin” from the list, press Enter, and give the plugin a name in kebab-case (e.g. my-plugin-name)
    4. Choose either JavaScript or TypeScript for the plugin language.
    5. Enable the plugin by adding it to the plugins configurations file:
    6. (TypeScript-specific) Run npm install or yarn in the newly-created plugin directory.
    7. (TypeScript-specific) Run yarn build or npm run build in the plugin directory. This step transpiles the TypeScript files and outputs the JavaScript files to a dist directory that is unique to the plugin.
    8. Run yarn build or npm run build at the project root.
    9. Run yarn develop or npm run develop at the project root.

Plugins created using the preceding directions are located in the plugins directory of the application .

Add features to a plugin

Strapi provides programmatic APIs for plugins to hook into some of Strapi’s features.

Plugins can register with the server and/or the admin panel, by looking for entry point files at the root of the package:

  • strapi-server.js for the Server.You can check Server API for more.
  • strapi-admin.js for the admin panel.You can check Admin Panel API for more.

 

Admin UI


Set a plugin Icon

Create an icon component

Here we are importing an icon component from strapi design system to set our plugin Icon.

JAVASCRIPT


// path: ./strapi-plugin-abandoned-cart/admin/src/components/PluginnIcon/index.js

/**
*
* PluginIcon
*
*/

import React from "react";
import ShoppingCart from "@strapi/icons/ShoppingCart";

const PluginIcon = () => <ShoppingCart />;
export default PluginIcon;

Set plugin icon

JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/index.js

import { prefixPluginTranslations } from "@strapi/helper-plugin";
import pluginPkg from "../../package.json";
import pluginId from "./pluginId";
import Initializer from "./components/Initializer";
import PluginIcon from "./components/PluginIcon";

const name = pluginPkg.strapi.name;
export default {
 register(app) {
   app.addMenuLink({
     to: `/plugins/${pluginId}`,
     icon: PluginIcon,
  ……………………
};

Import   abandonedCart to homePage

Create a new component inside abandonedCartItemModal/index.js then import it in home page

// path: ./strapi-plugin-abandoned-cart/admin/src/pages/HomePage/index.js
/*
*
* HomePage
*
*/

import React, { memo,useState } from "react";
import {
 BaseHeaderLayout,
 ContentLayout,
 Layout,
} from "@strapi/design-system/Layout";
import { Box } from "@strapi/design-system/Box";
import Abandoned from "../../components/abandonedCartItemModal"

const HomePage = () => {
 return (
   <Layout>
     <Box background="neutral100">
       <BaseHeaderLayout
         title="Abandoned Cart"
         as="h2"
       />
     </Box>
     <ContentLayout>
       <Box paddingLeft={8} background="neutral100">
         <Abandoned></Abandoned>
       </Box>
     </ContentLayout>
   </Layout>
 );
};
export default memo(HomePage);

Create an abandoned cart component

We are using strapi design system Storybook for styling and building UI. Check strapi-design-system for more.




JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/abandonedCartItemModal/index.js

import React, { useEffect,useState } from "react";
import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system/Table';
import { Box } from "@strapi/design-system/Box";
import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
import { IconButton } from '@strapi/design-system/IconButton';
import CarretDown from '@strapi/icons/CarretDown';
import { Typography } from '@strapi/design-system/Typography';
import { Avatar } from '@strapi/design-system/Avatar';
import AbandonedCartRequests from "../../api/abandonedCartAPIs"
import {abandonedCartRestructure} from "../../helper/customFunctions";
import style from "../../public/styleSheets/home.module.css"
import { Button } from '@strapi/design-system/Button';
import { NumberInput } from '@strapi/design-system/NumberInput';
import {Illo} from '../../components/Illo/index';
import Settings from "../settings";
import {getSettings} from '../../helper/customFunctions'

const AbandonedCartItemModal = () => {
const [settings,setSettings] = useState([]);
const [cartItems,setCartItems] = useState([]);
const [days, setDays] = useState();
const [show,setShow] = useState(false)
const fetch = async () => {
let apiPAth = settings?.cart_item_table || 'cart-items'
const rawCartItems = await AbandonedCartRequests.getAllCartItems(days,apiPAth)
let restructuredCartItems = await abandonedCartRestructure(rawCartItems.data)
setCartItems(restructuredCartItems)
setShow(true)
}

useEffect(async () => {
let response = await getSettings()
setSettings(response[0])
   }, []);
return (
<div className={style.wrapper}><Box background="neutral100">
<Settings settings={{...settings}} setSettings={setSettings}/>
</Box>
<Box background="neutral100">
<h1 className={style.h1}>Fetch Abandoned Carts</h1>
<div className={style.fetch}>
<NumberInput label="Days" placeholder="Days" aria-label="Content" name="content" hint="To list cart items updated x days ago" error={undefined} onValueChange={value => setDays(value)} value={days} />
<Button disabled={days != null ? false : true} onClick={() => {fetch()}}>Fetch cart items</Button>
</div>
<div className={style.contentWrapper}>
{
show && cartItems.length > 0 ?
<Table>
<Thead>
<Tr>
<Th action={<IconButton label="Sort on ID" icon={<CarretDown />} noBorder />}>
<Typography variant="sigma">ID</Typography>
</Th>
<Th>
<Typography variant="sigma">Email</Typography>
</Th>
<Th>
<Typography variant="sigma">Product</Typography>
</Th>
<Th>
<Typography variant="sigma">Title</Typography>
</Th>
<Th>
<Typography variant="sigma">Quantity</Typography>
</Th>
<Th>
<Typography variant="sigma">Last Updated on</Typography>
</Th>
</Tr>
</Thead>
<Tbody>
{cartItems.map(entry => <Tr key={entry.id}>
<Td>
<Typography textColor="neutral800">{entry.id}</Typography>
</Td>
<Td>
<Typography textColor="neutral800">{entry[settings.user_email_field || 'email']}</Typography>
</Td>
<Td>
<Avatar src={entry?.product[settings.product_image_field || 'image']?.data?.attributes?.url} alt={entry.contact} />
</Td>
<Td>
<Typography textColor="neutral800">{entry?.product[settings.product_title_field || 'title']}</Typography>
</Td>
<Td>
<Typography textColor="neutral800">{entry[settings.product_quantity_field || 'quantity']}</Typography>
</Td>
<Td>
<Typography textColor="neutral800">{new Date(entry?.lastUpdated).toLocaleDateString()
}</Typography>
</Td>
</Tr>)}
</Tbody>
</Table> :
<EmptyStateLayout
icon={<Illo />}
content="Don't have any abandoned carts,Please try with some other date."
/>
}
</div>
</Box>
</div>
         );
 }
 export default AbandonedCartItemModal;

We have imported components and functions from abandonedCartAPIs ,customFunctions,Illo and Settings in the above code snippet , So in the  next step, we have to declare all these functions.

 

Abandoned Cart APIs

  1. abandonedCartAPIs.js

We are using filters in some of our APIs, the Entity Service API offers the ability to filter results found with its findMany() method.

Results are filtered with the filters parameter that accepts logical operators and attribute operators. Every operator should be prefixed with $.Check Entity Service API: Filtering for more

JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/api/abandonedCartAPIs.js

import { request } from "@strapi/helper-plugin"
import qs from "qs";
import {getSettings} from '../helper/customFunctions'
const AbandonedCartRequests =
{
getAllCartItems: async (days,apiPath) => {
var CurrentDate=new Date();
CurrentDate.setHours(0, 0, 0, 0);
var pastDate=new Date(CurrentDate);
pastDate.setDate(pastDate.getDate() - days)
const query = qs.stringify({
filters: {
updatedAt: {
$lt: pastDate,
       },
     },
populate: ['user', 'product', 'product.image'],
   }, {
encodeValuesOnly: true,
   });
let response = await getSettings()
let token = response[0]?.api_token;
return await request(`/api/${apiPath}?${query}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
           }
   })
}
}
export default AbandonedCartRequests

Custom Functions

  1. customFunctions.js

In this file we are declaring some other custom functions too, all these functions were used by our plugin.

abandonedCartRestructure: It will convert raw data to an easy to iterate structure

getSettings: Send a GET request to get the settings saved in db and return the response.

updateSettings: Send a PUT request to update the settings and return the response.

 

JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/helper/customFunctions.js
import { request } from "@strapi/helper-plugin"
import { auth } from "@strapi/helper-plugin";

export const abandonedCartRestructure = (cartItems) => {
let restructuredCartItems = []
cartItems?.map((cart_item) => {
let temp = {}
let product = cart_item?.attributes?.product?.data?.attributes
let user = cart_item?.attributes?.user
temp['userEmail'] = user?.data?.attributes?.email
temp['product'] = product
temp['id'] = cart_item?.id
temp['quantity'] = cart_item?.attributes?.quantity
temp['lastUpdated'] = cart_item?.attributes?.updatedAt
restructuredCartItems.push(temp)
       })
return restructuredCartItems
}

export const getSettings = async(payload) => {
let userId = auth.getUserInfo().id
let res = await request(`/strapi-plugin-abandoned-cart/settings`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
           },
   })
return res
}

export const updateSettings = async(payload) => {
try {
let userId = auth.getUserInfo().id
let res = await request(`/strapi-plugin-abandoned-cart/settings/update`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
               },
body: payload
       })
if (res?.updatedAt) {
alert('Settings Updated')
       }
return res
   } catch  {
alert('Something went wrong')
   }
}

Settings view

 

JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/components/settings/index.js


import React, { useEffect,useState } from "react";
import style from "../../public/styleSheets/home.module.css"
import { Button } from '@strapi/design-system/Button';
import { TextInput } from '@strapi/design-system/TextInput'; import Cog from '@strapi/icons/Cog';
import {updateSettings} from '../../helper/customFunctions'
import { Switch } from '@strapi/design-system/Switch';
import { NumberInput } from '@strapi/design-system/NumberInput';


const Settings = (props) => {
const settings = props.settings
const setSettings = props.setSettings
const [cartItemTableName, setCartItemTableName] = useState();
const [cartItemAPIName, setCartItemAPIName] = useState();
const [userEmailFieldName, setUserEmailFieldName] = useState();
const [productImageFieldName, setProductImageFieldName] = useState();
const [productTitle, setProductTitle] = useState();
const [productQuantity, setProductQuantity] = useState();
const [duration, setDuration] = useState();
const [apiToken, setApiToken] = useState();
const [cron, setCron] = useState(false);
useEffect( () => {
setCartItemTableName(settings?.cart_item_table || '')
setCartItemAPIName(settings?.cart_item_api || 'cart-items')
setUserEmailFieldName(settings?.user_email_field || 'email');
setProductImageFieldName(settings?.product_image_field || 'image');
setProductTitle(settings?.product_title_field || 'title');
setProductQuantity(settings?.product_quantity_field || 'quantity');
setDuration(settings?.duration);
setApiToken(settings?.api_token);
setCron(settings?.cron || false);
}, [settings]);

const update = async () => {
let payload = { 
cart_item_table: cartItemTableName,
cart_item_api: cartItemAPIName,
user_email_field: userEmailFieldName,
product_image_field: productImageFieldName,
product_title_field: productTitle,
product_quantity_field: productQuantity,
duration: duration,
api_token: apiToken,
cron: cron
}
let response = await updateSettings(payload)
setSettings(response)
}

return (
<div className={style.settingsWrapper}>
<h1 className={style.h1}><Cog />Settings</h1>
<h2 className={style.h2}>Basic</h2>
<div className={style.settings}>
<TextInput placeholder="API Token" name="token" label="API Token" hint="settings > API Token" onChange={e => setApiToken(e.target.value)} value={apiToken} />
<TextInput placeholder="Content Type name" name="cartItem" label="Content Type name for Cart Items" hint="cart-item by default" onChange={e => setCartItemTableName(e.target.value)} value={cartItemTableName} />
<TextInput placeholder="API name" name="cartItem" label="Name in API for Cart Items" hint="cart-items by default" onChange={e => setCartItemAPIName(e.target.value)} value={cartItemAPIName} />
<TextInput placeholder="Field name" name="userEmail" label="Content Type name for User Email" hint="email by default" onChange={e => setUserEmailFieldName(e.target.value)} value={userEmailFieldName} />
<TextInput placeholder="Field name" name="ProductImage" label="Content Type name for Product Image" hint="images by default" onChange={e => setProductImageFieldName(e.target.value)} value={productImageFieldName} />
<TextInput placeholder="Field name" name="title" label="Content Type name for Title" hint="title by default" onChange={e => setProductTitle(e.target.value)} value={productTitle} />
<TextInput placeholder="Field name" name="quantity" label="Content Type name for Quantity" hint="quantity by default" onChange={e => setProductQuantity(e.target.value)} value={productQuantity} />
</div>
<h2 className={style.h2}>Activate Cron</h2>
<span className={style.hint}>You can set the cron in .env file like <italic>SEND_EMAIL_CRON=* * * * * *</italic></span>
<div className={style.settings}>
<NumberInput placeholder="Days" label="Days" name="content" hint="by default it is 5.cart 5 days ago." error={undefined} onChange={e => setDuration(e.target.value)} value={duration} /> 
<Switch label="Activate the Cron" name="cron" selected={cron || false} onChange={() => setCron(s => !s)} visibleLabels={true} />
</div>
<div className={style.settingsUpdate}>
<Button onClick={() => {update()}}>Update</Button>
</div>
</div>
);
}
export default Settings;

Styling

// path: ./strapi-plugin-abandoned-cart/admin/src/public/styleSheets/home.module.css
.fetch {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20px;
padding: 10px0;
}

.wrapper {
display: flex;
font-family: Arial;
text-align: center;
justify-content: flex-start;
flex-direction: column;
gap: 50px;
}

.contentWrapper {
width: 100%;
}

.h1 {
display: flex;
font-size: 1.7rem;
padding: 10px0;
font-weight: 700;
text-align: left;
padding-bottom: 20px;
}

.h2 {
display: flex;
font-size: 1rem;
padding: 5px0;
font-weight: 700;
text-align: left;
}

.settingsWrapper {
display: flex;
flex-direction: column;
background: antiquewhite;
padding: 10px20px;
}

.settings {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20px;
padding: 10px0;
padding-bottom: 20px;
}

.settings p {
text-align: left;
}

.hint {
text-align: left;
font-size: .7rem;
color: rgb(66, 61, 61);
font-style: italic;
}

Abandoned cart items listing

It will show a illo icon when the list is empty.So we need to create the illo icon.

Illo Icon

 

JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/components/Illo/index.js

import React from "react";

export const Illo = () => (
<svg
width="159"
height="88"
viewBox="0 0 159 88"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>

<path
fillRule="evenodd"
clipRule="evenodd"
d="M134.933 17.417C137.768 17.417 140.067 19.7153 140.067 22.5503C140.067 25.3854 137.768 27.6837 134.933 27.6837H105.6C108.435 27.6837 110.733 29.9819 110.733 32.817C110.733 35.6521 108.435 37.9503 105.6 37.9503H121.733C124.568 37.9503 126.867 40.2486 126.867 43.0837C126.867 45.9187 124.568 48.217 121.733 48.217H114.272C110.698 48.217 107.8 50.5153 107.8 53.3503C107.8 55.2404 109.267 56.9515 112.2 58.4837C115.035 58.4837 117.333 60.7819 117.333 63.617C117.333 66.4521 115.035 68.7503 112.2 68.7503H51.3333C48.4982 68.7503 46.2 66.4521 46.2 63.617C46.2 60.7819 48.4982 58.4837 51.3333 58.4837H22.7333C19.8982 58.4837 17.6 56.1854 17.6 53.3503C17.6 50.5153 19.8982 48.217 22.7333 48.217H52.0666C54.9017 48.217 57.2 45.9187 57.2 43.0837C57.2 40.2486 54.9017 37.9503 52.0666 37.9503H33.7333C30.8982 37.9503 28.6 35.6521 28.6 32.817C28.6 29.9819 30.8982 27.6837 33.7333 27.6837H63.0666C60.2316 27.6837 57.9333 25.3854 57.9333 22.5503C57.9333 19.7153 60.2316 17.417 63.0666 17.417H134.933ZM134.933 37.9503C137.768 37.9503 140.067 40.2486 140.067 43.0837C140.067 45.9187 137.768 48.217 134.933 48.217C132.098 48.217 129.8 45.9187 129.8 43.0837C129.8 40.2486 132.098 37.9503 134.933 37.9503Z"
fill="#DBDBFA"
/>

<path
fillRule="evenodd"
clipRule="evenodd"
d="M95.826 16.6834L102.647 66.4348L103.26 71.4261C103.458 73.034 102.314 74.4976 100.706 74.695L57.7621 79.9679C56.1542 80.1653 54.6906 79.0219 54.4932 77.4139L47.8816 23.5671C47.7829 22.7631 48.3546 22.0313 49.1586 21.9326C49.1637 21.932 49.1688 21.9313 49.1739 21.9307L52.7367 21.5311L95.826 16.6834ZM55.6176 21.208L58.9814 20.8306Z"
fill="white"
/>

<path
d="M55.6176 21.208L58.9814 20.8306M95.826 16.6834L102.647 66.4348L103.26 71.4261C103.458 73.034 102.314 74.4976 100.706 74.695L57.7621 79.9679C56.1542 80.1653 54.6906 79.0219 54.4932 77.4139L47.8816 23.5671C47.7829 22.7631 48.3546 22.0313 49.1586 21.9326C49.1637 21.932 49.1688 21.9313 49.1739 21.9307L52.7367 21.5311L95.826 16.6834Z"
stroke="#7E7BF6"
strokeWidth="2.5"
/>

<path
fillRule="evenodd"
clipRule="evenodd"
d="M93.9695 19.8144L100.144 64.9025L100.699 69.4258C100.878 70.8831 99.8559 72.2077 98.416 72.3845L59.9585 77.1065C58.5185 77.2833 57.2062 76.2453 57.0272 74.7881L51.0506 26.112C50.9519 25.308 51.5236 24.5762 52.3276 24.4775L57.0851 23.8934"
fill="#F0F0FF"
/>

<path
fillRule="evenodd"
clipRule="evenodd"
d="M97.701 7.33301H64.2927C63.7358 7.33301 63.2316 7.55873 62.8667 7.92368C62.5017 8.28862 62.276 8.79279 62.276 9.34967V65.083C62.276 65.6399 62.5017 66.1441 62.8667 66.509C63.2316 66.874 63.7358 67.0997 64.2927 67.0997H107.559C108.116 67.0997 108.62 66.874 108.985 66.509C109.35 66.1441 109.576 65.6399 109.576 65.083V19.202C109.576 18.6669 109.363 18.1537 108.985 17.7755L99.1265 7.92324C98.7484 7.54531 98.2356 7.33301 97.701 7.33301Z"
fill="white"
stroke="#7F7CFA"
strokeWidth="2.5"
/>


<path
d="M98.026 8.17871V16.6833C98.026 17.8983 99.011 18.8833 100.226 18.8833H106.044"
stroke="#807EFA"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>

<path
d="M70.1594 56.2838H89.2261M70.1594 18.8838H89.2261H70.1594ZM70.1594 27.6838H101.693H70.1594ZM70.1594 37.2171H101.693H70.1594ZM70.1594 46.7505H101.693H70.1594Z"
stroke="#817FFA"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);


 

Item Details

JAVASCRIPT

// path: ./strapi-plugin-abandoned-cart/admin/src/components/ItemDetail/index.js

import React, { memo,useState } from "react";
import { ModalLayout, ModalBody, ModalHeader, ModalFooter } from '@strapi/design-system/ModalLayout';
import { Typography } from '@strapi/design-system/Typography';
import { DatePicker } from '@strapi/design-system/DatePicker';
import { Box } from '@strapi/design-system/Box';
import { Button } from '@strapi/design-system/Button';

const ItemDetail = ({setShowModal}) => {
const [isVisible, setIsVisible] = useState(true);
const [date, setDate] = useState();
return <>
{isVisible && <ModalLayout onClose={() => {setIsVisible(prev => !prev);
setShowModal(false)
       }
} labelledBy="title">
<ModalHeader>
<Typography fontWeight="bold" textColor="neutral800" as="h2" id="title">
                   Title
</Typography>
</ModalHeader>
<ModalBody>
<DatePicker onChange={setDate} selectedDate={date} label="Date picker" name="datepicker" clearLabel={'Clear the datepicker'} onClear={() => setDate(undefined)} selectedDateLabel={formattedDate => `Date picker, current is ${formattedDate}`} />
{Array(50).fill(null).map((_, index) => <Box key={`box-${index}`} padding={8} background="neutral100">
                       Hello world

</Box>)}
</ModalBody>
<ModalFooter startActions={<Button onClick={() => {setIsVisible(prev => !prev);setShowModal(false)}} variant="tertiary">
                     Cancel
</Button>} endActions={<>
<Button variant="secondary">Add new stuff</Button>
<Button onClick={() => setIsVisible(prev => !prev)}>Finish</Button>
</>} />

</ModalLayout>}
</>;
 }
 export default ItemDetail;

Strapi-abandoned-cart Plugin development – 2/2


 



Leave a Reply