feat: add medusa-react (#913)

This commit is contained in:
Zakaria El Asri
2021-12-14 19:09:36 +01:00
committed by GitHub
parent 40f6e88875
commit d0d8dd7bf6
97 changed files with 22822 additions and 7595 deletions

View File

@@ -0,0 +1,98 @@
import React, { useState } from "react"
import {
useAddShippingMethodToCart,
useCompleteCart,
useCreateCart,
useSetPaymentSession,
useUpdateCart,
useCreatePaymentSession,
} from "../hooks/carts"
import { Cart } from "../types"
interface CartState {
cart?: Cart
}
interface CartContext extends CartState {
setCart: (cart: Cart) => void
pay: ReturnType<typeof useSetPaymentSession>
createCart: ReturnType<typeof useCreateCart>
startCheckout: ReturnType<typeof useCreatePaymentSession>
completeCheckout: ReturnType<typeof useCompleteCart>
updateCart: ReturnType<typeof useUpdateCart>
addShippingMethod: ReturnType<typeof useAddShippingMethodToCart>
totalItems: number
}
const CartContext = React.createContext<CartContext | null>(null)
export const useCart = () => {
const context = React.useContext(CartContext)
if (!context) {
throw new Error("useCart must be used within a CartProvider")
}
return context
}
interface CartProps {
children: React.ReactNode
initialState?: Cart
}
const defaultInitialState = {
id: "",
items: [] as any,
} as Cart
export const CartProvider = ({
children,
initialState = defaultInitialState,
}: CartProps) => {
const [cart, setCart] = useState<Cart>(initialState)
const createCart = useCreateCart({
onSuccess: ({ cart }) => setCart(cart),
})
const updateCart = useUpdateCart(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const addShippingMethod = useAddShippingMethodToCart(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const startCheckout = useCreatePaymentSession(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const pay = useSetPaymentSession(cart?.id, {
onSuccess: ({ cart }) => {
setCart(cart)
},
})
const completeCheckout = useCompleteCart(cart?.id)
const totalItems = cart?.items
.map((i) => i.quantity)
.reduce((acc, curr) => acc + curr, 0)
return (
<CartContext.Provider
value={{
cart,
setCart,
createCart,
pay,
startCheckout,
completeCheckout,
updateCart,
addShippingMethod,
totalItems: totalItems || 0,
}}
>
{children}
</CartContext.Provider>
)
}

View File

@@ -0,0 +1,3 @@
export * from "./medusa"
export * from "./session-cart"
export * from "./cart"

View File

@@ -0,0 +1,38 @@
import React from "react"
import { QueryClientProvider, QueryClientProviderProps } from "react-query"
import Medusa from "@medusajs/medusa-js"
interface MedusaContextState {
client: Medusa
}
const MedusaContext = React.createContext<MedusaContextState | null>(null)
export const useMedusa = () => {
const context = React.useContext(MedusaContext)
if (!context) {
throw new Error("useMedusa must be used within a MedusaProvider")
}
return context
}
interface MedusaProviderProps {
baseUrl: string
queryClientProviderProps: QueryClientProviderProps
children: React.ReactNode
}
export const MedusaProvider = ({
queryClientProviderProps,
baseUrl,
children,
}: MedusaProviderProps) => {
const medusaClient = new Medusa({ baseUrl, maxRetries: 0 })
return (
<QueryClientProvider {...queryClientProviderProps}>
<MedusaContext.Provider value={{ client: medusaClient }}>
{children}
</MedusaContext.Provider>
</QueryClientProvider>
)
}

View File

@@ -0,0 +1,289 @@
import React, { useContext, useEffect } from "react"
import { useLocalStorage } from "../hooks/utils"
import { RegionInfo, ProductVariant } from "../types"
import { getVariantPrice } from "../utils"
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
}