Files
medusa-store/packages/medusa-react/src/contexts/session-cart.tsx
Zakaria El Asri 2e384842d5 feat: medusa-react admin hooks (#978)
* add: medusa admin hooks + tests

* fix: remove unneeded props

* fix: deps

* fix: deps

* fix: deps

* fix: failing tests

* fix: failing tests

* fix: query key

* add: yarn workspaces

* fix: linting medusa-react

* fix: add prepare script

* fix: buildOptions

* fix: useAdminShippingOptions query

* fix: use qs instead for query params (#1019)

* fix: formatting

* debug: ci pipeline

* debug: log node_modules structure

* debug: use lerna bootstrap

* debug: update node version

* debug: print pkgs in workspace

* debug: print pkgs in workspace

* debug: print pkgs in workspace

* debug: print pkgs in workspace

* debug: add explicit build step

* fix: jsdoc

* debug: run build step

* debug: fix build errors

* debug: add build step to integration tests

* fix: failing test

* cleanup

Co-authored-by: Sebastian Rindom <seb@medusajs.com>
Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
2022-02-02 17:10:56 +01:00

290 lines
6.8 KiB
TypeScript

import React, { useContext, useEffect } from "react"
import { useLocalStorage } from "../hooks/utils"
import { RegionInfo, ProductVariant } from "../types"
import { getVariantPrice } from "../helpers"
import { isArray, isEmpty, isObject } from "lodash"
interface Item {
variant: ProductVariant
quantity: number
readonly total?: number
}
export interface SessionCartState {
region: RegionInfo
items: Item[]
totalItems: number
total: number
}
interface SessionCartContextState extends SessionCartState {
setRegion: (region: RegionInfo) => void
addItem: (item: Item) => void
removeItem: (id: string) => void
updateItem: (id: string, item: Partial<Item>) => void
setItems: (items: Item[]) => void
updateItemQuantity: (id: string, quantity: number) => void
incrementItemQuantity: (id: string) => void
decrementItemQuantity: (id: string) => void
getItem: (id: string) => Item | undefined
clearItems: () => void
}
const SessionCartContext = React.createContext<SessionCartContextState | null>(
null
)
enum ACTION_TYPES {
INIT,
ADD_ITEM,
SET_ITEMS,
REMOVE_ITEM,
UPDATE_ITEM,
CLEAR_ITEMS,
SET_REGION,
}
type Action =
| { type: ACTION_TYPES.SET_REGION; payload: RegionInfo }
| { type: ACTION_TYPES.INIT; payload: object }
| { type: ACTION_TYPES.ADD_ITEM; payload: Item }
| {
type: ACTION_TYPES.UPDATE_ITEM
payload: { id: string; item: Partial<Item> }
}
| { type: ACTION_TYPES.REMOVE_ITEM; payload: { id: string } }
| { type: ACTION_TYPES.SET_ITEMS; payload: Item[] }
| { type: ACTION_TYPES.CLEAR_ITEMS }
const reducer = (state: SessionCartState, action: Action) => {
switch (action.type) {
case ACTION_TYPES.INIT: {
return state
}
case ACTION_TYPES.SET_REGION: {
return generateCartState(
{
...state,
region: action.payload,
},
state.items
)
}
case ACTION_TYPES.ADD_ITEM: {
const duplicateVariantIndex = state.items.findIndex(
item => item.variant.id === action.payload?.variant?.id
)
if (duplicateVariantIndex !== -1) {
state.items.splice(duplicateVariantIndex, 1)
}
const items = [...state.items, action.payload]
return generateCartState(state, items)
}
case ACTION_TYPES.UPDATE_ITEM: {
const items = state.items.map(item =>
item.variant.id === action.payload.id
? { ...item, ...action.payload.item }
: item
)
return generateCartState(state, items)
}
case ACTION_TYPES.REMOVE_ITEM: {
const items = state.items.filter(
item => item.variant.id !== action.payload.id
)
return generateCartState(state, items)
}
case ACTION_TYPES.SET_ITEMS: {
return generateCartState(state, action.payload)
}
case ACTION_TYPES.CLEAR_ITEMS: {
return {
...state,
items: [],
total: 0,
totalItems: 0,
}
}
default:
return state
}
}
export const generateCartState = (state: SessionCartState, items: Item[]) => {
const newItems = generateItems(state.region, items)
return {
...state,
items: newItems,
totalItems: items.reduce((sum, item) => sum + item.quantity, 0),
total: calculateSessionCartTotal(newItems),
}
}
const generateItems = (region: RegionInfo, items: Item[]) => {
return items.map(item => ({
...item,
total: getVariantPrice(item.variant, region),
}))
}
const calculateSessionCartTotal = (items: Item[]) => {
return items.reduce(
(total, item) => total + item.quantity * (item.total || 0),
0
)
}
interface SessionCartProviderProps {
children: React.ReactNode
initialState?: SessionCartState
}
const defaultInitialState: SessionCartState = {
region: {} as RegionInfo,
items: [],
total: 0,
totalItems: 0,
}
export const SessionCartProvider = ({
initialState = defaultInitialState,
children,
}: SessionCartProviderProps) => {
const [saved, save] = useLocalStorage(
"medusa-session-cart",
JSON.stringify(initialState)
)
const [state, dispatch] = React.useReducer(reducer, JSON.parse(saved))
useEffect(() => {
save(JSON.stringify(state))
}, [state, save])
const setRegion = (region: RegionInfo) => {
if (!isObject(region) || isEmpty(region)) {
throw new Error("region must be a non-empty object")
}
dispatch({ type: ACTION_TYPES.SET_REGION, payload: region })
}
const getItem = (id: string) => {
return state.items.find(item => item.variant.id === id)
}
const setItems = (items: Item[]) => {
if (!isArray(items)) {
throw new Error("items must be an array of items")
}
dispatch({ type: ACTION_TYPES.SET_ITEMS, payload: items })
}
const addItem = (item: Item) => {
if (!isObject(item) || isEmpty(item)) {
throw new Error("item must be a non-empty object")
}
dispatch({ type: ACTION_TYPES.ADD_ITEM, payload: item })
}
const updateItem = (id: string, item: Partial<Item>) => {
dispatch({ type: ACTION_TYPES.UPDATE_ITEM, payload: { id, item } })
}
const updateItemQuantity = (id: string, quantity: number) => {
const item = getItem(id)
if (!item) return
quantity = quantity <= 0 ? 1 : quantity
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: {
...item,
quantity: Math.min(item.variant.inventory_quantity, quantity),
},
},
})
}
const incrementItemQuantity = (id: string) => {
const item = getItem(id)
if (!item) return
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: {
...item,
quantity: Math.min(
item.variant.inventory_quantity,
item.quantity + 1
),
},
},
})
}
const decrementItemQuantity = (id: string) => {
const item = getItem(id)
if (!item) return
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: { ...item, quantity: Math.max(0, item.quantity - 1) },
},
})
}
const removeItem = (id: string) => {
dispatch({
type: ACTION_TYPES.REMOVE_ITEM,
payload: { id },
})
}
const clearItems = () => {
dispatch({
type: ACTION_TYPES.CLEAR_ITEMS,
})
}
return (
<SessionCartContext.Provider
value={{
...state,
setRegion,
addItem,
updateItem,
updateItemQuantity,
incrementItemQuantity,
decrementItemQuantity,
removeItem,
getItem,
setItems,
clearItems,
}}
>
{children}
</SessionCartContext.Provider>
)
}
export const useSessionCart = () => {
const context = useContext(SessionCartContext)
if (!context) {
throw new Error(
"useSessionCart should be used as a child of SessionCartProvider"
)
}
return context
}