forked from crowetic/commerce
MOving stuff around and adding temporal new files
This commit is contained in:
parent
1898f094bc
commit
2c9b8b100d
@ -1,11 +1,16 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { Fetcher } from '@commerce/utils/types'
|
||||||
import {
|
import {
|
||||||
CommerceConfig,
|
CommerceConfig,
|
||||||
CommerceProvider as CoreCommerceProvider,
|
CommerceProvider as CoreCommerceProvider,
|
||||||
useCommerce as useCoreCommerce,
|
useCommerce as useCoreCommerce,
|
||||||
|
HookHandler,
|
||||||
} from '@commerce'
|
} from '@commerce'
|
||||||
import { FetcherError } from '@commerce/utils/errors'
|
import { FetcherError } from '@commerce/utils/errors'
|
||||||
|
import type { CartInput } from '@commerce/cart/use-cart'
|
||||||
|
import { normalizeCart } from './lib/normalize'
|
||||||
|
import { Cart } from './types'
|
||||||
|
|
||||||
async function getText(res: Response) {
|
async function getText(res: Response) {
|
||||||
try {
|
try {
|
||||||
@ -23,6 +28,57 @@ async function getError(res: Response) {
|
|||||||
return new FetcherError({ message: await getText(res), status: res.status })
|
return new FetcherError({ message: await getText(res), status: res.status })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetcher: Fetcher<any> = async ({
|
||||||
|
url,
|
||||||
|
method = 'GET',
|
||||||
|
variables,
|
||||||
|
body: bodyObj,
|
||||||
|
}) => {
|
||||||
|
const hasBody = Boolean(variables || bodyObj)
|
||||||
|
const body = hasBody
|
||||||
|
? JSON.stringify(variables ? { variables } : bodyObj)
|
||||||
|
: undefined
|
||||||
|
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||||
|
const res = await fetch(url!, { method, body, headers })
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const { data } = await res.json()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
throw await getError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCart: HookHandler<Cart, CartInput> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/cart',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
fetcher(context) {
|
||||||
|
return undefined as any
|
||||||
|
},
|
||||||
|
onResponse(response) {
|
||||||
|
return Object.create(response, {
|
||||||
|
isEmpty: {
|
||||||
|
get() {
|
||||||
|
return (response.data?.lineItems.length ?? 0) <= 0
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bigcommerceProvider = {
|
||||||
|
locale: 'en-us',
|
||||||
|
cartCookie: 'bc_cartId',
|
||||||
|
fetcher,
|
||||||
|
cartNormalizer: normalizeCart,
|
||||||
|
cart: { useCart },
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigcommerceProvider = typeof bigcommerceProvider
|
||||||
|
|
||||||
export const bigcommerceConfig: CommerceConfig = {
|
export const bigcommerceConfig: CommerceConfig = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'bc_cartId',
|
cartCookie: 'bc_cartId',
|
||||||
@ -52,10 +108,13 @@ export type BigcommerceProps = {
|
|||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||||
return (
|
return (
|
||||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
<CoreCommerceProvider
|
||||||
|
provider={bigcommerceProvider}
|
||||||
|
config={{ ...bigcommerceConfig, ...config }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CoreCommerceProvider>
|
</CoreCommerceProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommerce = () => useCoreCommerce()
|
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||||
|
28
framework/commerce/cart/use-cart-2.tsx
Normal file
28
framework/commerce/cart/use-cart-2.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||||
|
import useData, { ResponseState, SwrOptions } from '../utils/use-data'
|
||||||
|
import type { Cart } from '../types'
|
||||||
|
import { useCommerce } from '..'
|
||||||
|
|
||||||
|
export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
|
||||||
|
|
||||||
|
// Input expected by the `useCart` hook
|
||||||
|
export type CartInput = {
|
||||||
|
cartId?: Cart['id']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useCart<Data extends Cart | null>(
|
||||||
|
options: HookFetcherOptions,
|
||||||
|
input: HookInput,
|
||||||
|
fetcherFn: HookFetcher<Data, CartInput>,
|
||||||
|
swrOptions?: SwrOptions<Data, CartInput>
|
||||||
|
): CartResponse<Data> {
|
||||||
|
const { providerRef, cartCookie } = useCommerce()
|
||||||
|
const fetcher: typeof fetcherFn = (options, input, fetch) => {
|
||||||
|
input.cartId = Cookies.get(cartCookie)
|
||||||
|
return fetcherFn(options, input, fetch)
|
||||||
|
}
|
||||||
|
const response = useData(options, input, fetcher, swrOptions)
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
42
framework/commerce/cart/use-fake.tsx
Normal file
42
framework/commerce/cart/use-fake.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import type { Cart } from '../types'
|
||||||
|
import type { HookFetcherFn } from '../utils/types'
|
||||||
|
import useData from '../utils/use-data-2'
|
||||||
|
import { Provider, useCommerce } from '..'
|
||||||
|
|
||||||
|
// Input expected by the `useCart` hook
|
||||||
|
export type CartInput = {
|
||||||
|
cartId?: Cart['id']
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher: HookFetcherFn<Cart | null, CartInput> = async ({
|
||||||
|
options,
|
||||||
|
input: { cartId },
|
||||||
|
fetch,
|
||||||
|
normalize,
|
||||||
|
}) => {
|
||||||
|
const data = cartId ? await fetch({ ...options }) : null
|
||||||
|
return data && normalize ? normalize(data) : data
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useFake<P extends Provider>() {
|
||||||
|
const { providerRef, cartCookie } = useCommerce<P>()
|
||||||
|
|
||||||
|
const provider = providerRef.current
|
||||||
|
const opts = provider.cart?.useCart
|
||||||
|
const options = opts?.fetchOptions ?? {}
|
||||||
|
const fetcherFn = opts?.fetcher ?? fetcher
|
||||||
|
const wrapper: typeof fetcher = (context) => {
|
||||||
|
context.input.cartId = Cookies.get(cartCookie)
|
||||||
|
return fetcherFn(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = useData(options, [], wrapper, opts?.swrOptions)
|
||||||
|
const memoizedResponse = useMemo(
|
||||||
|
() => (opts?.onResponse ? opts.onResponse(response) : response),
|
||||||
|
[response]
|
||||||
|
)
|
||||||
|
|
||||||
|
return memoizedResponse
|
||||||
|
}
|
@ -7,36 +7,71 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Fetcher } from './utils/types'
|
import { Fetcher, HookFetcherFn, HookFetcherOptions } from './utils/types'
|
||||||
|
import { Cart } from './types'
|
||||||
|
import type { ResponseState, SwrOptions } from './utils/use-data'
|
||||||
|
import type { CartInput } from './cart/use-cart'
|
||||||
|
|
||||||
const Commerce = createContext<CommerceContextValue | {}>({})
|
const Commerce = createContext<CommerceContextValue<any> | {}>({})
|
||||||
|
|
||||||
export type CommerceProps = {
|
export type Provider = CommerceConfig & {
|
||||||
|
cart?: {
|
||||||
|
useCart?: HookHandler<Cart, CartInput>
|
||||||
|
}
|
||||||
|
cartNormalizer(data: any): Cart
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HookHandler<Data, Input, Result = any, Body = any> = {
|
||||||
|
swrOptions?: SwrOptions<Data | null, Input, Result>
|
||||||
|
onResponse?(response: ResponseState<Data | null>): ResponseState<Data | null>
|
||||||
|
onMutation?: any
|
||||||
|
fetchOptions?: HookFetcherOptions
|
||||||
|
} & (
|
||||||
|
| // TODO: Maybe the normalizer is not required if it's used by the API route directly?
|
||||||
|
{
|
||||||
|
fetcher: HookFetcherFn<Data | null, Input, Result, Body>
|
||||||
|
normalizer?(data: Result): Data
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
fetcher?: never
|
||||||
|
normalizer(data: Result): Data
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommerceProps<P extends Provider> = {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
provider: P
|
||||||
config: CommerceConfig
|
config: CommerceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
||||||
CommerceContextValue,
|
CommerceContextValue<any>,
|
||||||
'fetcherRef'
|
'providerRef' | 'fetcherRef'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type CommerceContextValue = {
|
export type CommerceContextValue<P extends Provider> = {
|
||||||
|
providerRef: MutableRefObject<P>
|
||||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
fetcherRef: MutableRefObject<Fetcher<any>>
|
||||||
locale: string
|
locale: string
|
||||||
cartCookie: string
|
cartCookie: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommerceProvider({ children, config }: CommerceProps) {
|
export function CommerceProvider<P extends Provider>({
|
||||||
|
provider,
|
||||||
|
children,
|
||||||
|
config,
|
||||||
|
}: CommerceProps<P>) {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('CommerceProvider requires a valid config object')
|
throw new Error('CommerceProvider requires a valid config object')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providerRef = useRef(provider)
|
||||||
const fetcherRef = useRef(config.fetcher)
|
const fetcherRef = useRef(config.fetcher)
|
||||||
// Because the config is an object, if the parent re-renders this provider
|
// Because the config is an object, if the parent re-renders this provider
|
||||||
// will re-render every consumer unless we memoize the config
|
// will re-render every consumer unless we memoize the config
|
||||||
const cfg = useMemo(
|
const cfg = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
providerRef,
|
||||||
fetcherRef,
|
fetcherRef,
|
||||||
locale: config.locale,
|
locale: config.locale,
|
||||||
cartCookie: config.cartCookie,
|
cartCookie: config.cartCookie,
|
||||||
@ -47,6 +82,6 @@ export function CommerceProvider({ children, config }: CommerceProps) {
|
|||||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommerce<T extends CommerceContextValue>() {
|
export function useCommerce<P extends Provider>() {
|
||||||
return useContext(Commerce) as T
|
return useContext(Commerce) as CommerceContextValue<P>
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,13 @@ export type HookFetcher<Data, Input = null, Result = any> = (
|
|||||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||||
) => Data | Promise<Data>
|
) => Data | Promise<Data>
|
||||||
|
|
||||||
|
export type HookFetcherFn<Data, Input, Result = any, Body = any> = (context: {
|
||||||
|
options: HookFetcherOptions | null
|
||||||
|
input: Input
|
||||||
|
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
||||||
|
normalize?(data: Result): Data
|
||||||
|
}) => Data | Promise<Data>
|
||||||
|
|
||||||
export type HookFetcherOptions = {
|
export type HookFetcherOptions = {
|
||||||
query?: string
|
query?: string
|
||||||
url?: string
|
url?: string
|
||||||
|
79
framework/commerce/utils/use-data-2.ts
Normal file
79
framework/commerce/utils/use-data-2.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import useSWR, { ConfigInterface, responseInterface } from 'swr'
|
||||||
|
import type {
|
||||||
|
HookInput,
|
||||||
|
HookFetcher,
|
||||||
|
HookFetcherOptions,
|
||||||
|
HookFetcherFn,
|
||||||
|
} from './types'
|
||||||
|
import defineProperty from './define-property'
|
||||||
|
import { CommerceError } from './errors'
|
||||||
|
import { useCommerce } from '..'
|
||||||
|
|
||||||
|
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||||
|
Data,
|
||||||
|
CommerceError,
|
||||||
|
HookFetcher<Data, Input, Result>
|
||||||
|
>
|
||||||
|
|
||||||
|
export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseData = <Data = any, Input = null, Result = any>(
|
||||||
|
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
||||||
|
input: HookInput,
|
||||||
|
fetcherFn: HookFetcherFn<Data, Input, Result>,
|
||||||
|
swrOptions?: SwrOptions<Data, Input, Result>
|
||||||
|
) => ResponseState<Data>
|
||||||
|
|
||||||
|
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
|
||||||
|
const { fetcherRef } = useCommerce()
|
||||||
|
const fetcher = async (
|
||||||
|
url?: string,
|
||||||
|
query?: string,
|
||||||
|
method?: string,
|
||||||
|
...args: any[]
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await fetcherFn({
|
||||||
|
options: { url, query, method },
|
||||||
|
// Transform the input array into an object
|
||||||
|
input: args.reduce((obj, val, i) => {
|
||||||
|
obj[input[i][0]!] = val
|
||||||
|
return obj
|
||||||
|
}, {}),
|
||||||
|
fetch: fetcherRef.current,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// SWR will not log errors, but any error that's not an instance
|
||||||
|
// of CommerceError is not welcomed by this hook
|
||||||
|
if (!(error instanceof CommerceError)) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = useSWR(
|
||||||
|
() => {
|
||||||
|
const opts = typeof options === 'function' ? options() : options
|
||||||
|
return opts
|
||||||
|
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
swrOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!('isLoading' in response)) {
|
||||||
|
defineProperty(response, 'isLoading', {
|
||||||
|
get() {
|
||||||
|
return response.data === undefined
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useData
|
Loading…
x
Reference in New Issue
Block a user