* 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>
290 lines
6.8 KiB
TypeScript
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
|
|
}
|