mirror of
https://github.com/vercel/commerce.git
synced 2025-07-26 03:31:23 +00:00
Added framework folder
This commit is contained in:
36
framework/commerce/api/index.ts
Normal file
36
framework/commerce/api/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RequestInit, Response } from '@vercel/fetch'
|
||||
|
||||
export interface CommerceAPIConfig {
|
||||
locale?: string
|
||||
commerceUrl: string
|
||||
apiToken: string
|
||||
cartCookie: string
|
||||
cartCookieMaxAge: number
|
||||
customerCookie: string
|
||||
fetch<Data = any, Variables = any>(
|
||||
query: string,
|
||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<GraphQLFetcherResult<Data>>
|
||||
}
|
||||
|
||||
export type GraphQLFetcher<
|
||||
Data extends GraphQLFetcherResult = GraphQLFetcherResult,
|
||||
Variables = any
|
||||
> = (
|
||||
query: string,
|
||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||
fetchOptions?: RequestInit
|
||||
) => Promise<Data>
|
||||
|
||||
export interface GraphQLFetcherResult<Data = any> {
|
||||
data: Data
|
||||
res: Response
|
||||
}
|
||||
|
||||
export interface CommerceAPIFetchOptions<Variables> {
|
||||
variables?: Variables
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
// TODO: define interfaces for all the available operations and API endpoints
|
5
framework/commerce/cart/use-add-item.tsx
Normal file
5
framework/commerce/cart/use-add-item.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from '../utils/use-action'
|
||||
|
||||
const useAddItem = useAction
|
||||
|
||||
export default useAddItem
|
17
framework/commerce/cart/use-cart-actions.tsx
Normal file
17
framework/commerce/cart/use-cart-actions.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
import useUpdateItem from './use-update-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useCartActions<T, Input>(
|
||||
options: HookFetcherOptions,
|
||||
fetcher: HookFetcher<T, Input>
|
||||
) {
|
||||
const addItem = useAddItem<T, Input>(options, fetcher)
|
||||
const updateItem = useUpdateItem<T, Input>(options, fetcher)
|
||||
const removeItem = useRemoveItem<T, Input>(options, fetcher)
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
}
|
31
framework/commerce/cart/use-cart.tsx
Normal file
31
framework/commerce/cart/use-cart.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { responseInterface } from 'swr'
|
||||
import Cookies from 'js-cookie'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useData, { SwrOptions } from '../utils/use-data'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export type CartResponse<Result> = responseInterface<Result, Error> & {
|
||||
isEmpty: boolean
|
||||
}
|
||||
|
||||
export type CartInput = {
|
||||
cartId: string | undefined
|
||||
}
|
||||
|
||||
export default function useCart<Result>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, CartInput>,
|
||||
swrOptions?: SwrOptions<Result, CartInput>
|
||||
) {
|
||||
const { 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 Object.assign(response, { isEmpty: true }) as CartResponse<Result>
|
||||
}
|
5
framework/commerce/cart/use-remove-item.tsx
Normal file
5
framework/commerce/cart/use-remove-item.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from '../utils/use-action'
|
||||
|
||||
const useRemoveItem = useAction
|
||||
|
||||
export default useRemoveItem
|
5
framework/commerce/cart/use-update-item.tsx
Normal file
5
framework/commerce/cart/use-update-item.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from '../utils/use-action'
|
||||
|
||||
const useUpdateItem = useAction
|
||||
|
||||
export default useUpdateItem
|
52
framework/commerce/index.tsx
Normal file
52
framework/commerce/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
ReactNode,
|
||||
MutableRefObject,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import * as React from 'react'
|
||||
import { Fetcher } from './utils/types'
|
||||
|
||||
const Commerce = createContext<CommerceContextValue | {}>({})
|
||||
|
||||
export type CommerceProps = {
|
||||
children?: ReactNode
|
||||
config: CommerceConfig
|
||||
}
|
||||
|
||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
||||
CommerceContextValue,
|
||||
'fetcherRef'
|
||||
>
|
||||
|
||||
export type CommerceContextValue = {
|
||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
||||
locale: string
|
||||
cartCookie: string
|
||||
}
|
||||
|
||||
export function CommerceProvider({ children, config }: CommerceProps) {
|
||||
if (!config) {
|
||||
throw new Error('CommerceProvider requires a valid config object')
|
||||
}
|
||||
|
||||
const fetcherRef = useRef(config.fetcher)
|
||||
// Because the config is an object, if the parent re-renders this provider
|
||||
// will re-render every consumer unless we memoize the config
|
||||
const cfg = useMemo(
|
||||
() => ({
|
||||
fetcherRef,
|
||||
locale: config.locale,
|
||||
cartCookie: config.cartCookie,
|
||||
}),
|
||||
[config.locale, config.cartCookie]
|
||||
)
|
||||
|
||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
||||
}
|
||||
|
||||
export function useCommerce<T extends CommerceContextValue>() {
|
||||
return useContext(Commerce) as T
|
||||
}
|
5
framework/commerce/products/use-search.tsx
Normal file
5
framework/commerce/products/use-search.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useData from '../utils/use-data'
|
||||
|
||||
const useSearch = useData
|
||||
|
||||
export default useSearch
|
5
framework/commerce/use-customer.tsx
Normal file
5
framework/commerce/use-customer.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useData from './utils/use-data'
|
||||
|
||||
const useCustomer = useData
|
||||
|
||||
export default useCustomer
|
5
framework/commerce/use-login.tsx
Normal file
5
framework/commerce/use-login.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from './utils/use-action'
|
||||
|
||||
const useLogin = useAction
|
||||
|
||||
export default useLogin
|
5
framework/commerce/use-logout.tsx
Normal file
5
framework/commerce/use-logout.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from './utils/use-action'
|
||||
|
||||
const useLogout = useAction
|
||||
|
||||
export default useLogout
|
64
framework/commerce/use-price.tsx
Normal file
64
framework/commerce/use-price.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useCommerce } from '.'
|
||||
|
||||
export function formatPrice({
|
||||
amount,
|
||||
currencyCode,
|
||||
locale,
|
||||
}: {
|
||||
amount: number
|
||||
currencyCode: string
|
||||
locale: string
|
||||
}) {
|
||||
const formatCurrency = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
})
|
||||
|
||||
return formatCurrency.format(amount)
|
||||
}
|
||||
|
||||
export function formatVariantPrice({
|
||||
amount,
|
||||
baseAmount,
|
||||
currencyCode,
|
||||
locale,
|
||||
}: {
|
||||
baseAmount: number
|
||||
amount: number
|
||||
currencyCode: string
|
||||
locale: string
|
||||
}) {
|
||||
const hasDiscount = baseAmount > amount
|
||||
const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' })
|
||||
const discount = hasDiscount
|
||||
? formatDiscount.format((baseAmount - amount) / baseAmount)
|
||||
: null
|
||||
|
||||
const price = formatPrice({ amount, currencyCode, locale })
|
||||
const basePrice = hasDiscount
|
||||
? formatPrice({ amount: baseAmount, currencyCode, locale })
|
||||
: null
|
||||
|
||||
return { price, basePrice, discount }
|
||||
}
|
||||
|
||||
export default function usePrice(
|
||||
data?: {
|
||||
amount: number
|
||||
baseAmount?: number
|
||||
currencyCode: string
|
||||
} | null
|
||||
) {
|
||||
const { amount, baseAmount, currencyCode } = data ?? {}
|
||||
const { locale } = useCommerce()
|
||||
const value = useMemo(() => {
|
||||
if (typeof amount !== 'number' || !currencyCode) return ''
|
||||
|
||||
return baseAmount
|
||||
? formatVariantPrice({ amount, baseAmount, currencyCode, locale })
|
||||
: formatPrice({ amount, currencyCode, locale })
|
||||
}, [amount, baseAmount, currencyCode])
|
||||
|
||||
return typeof value === 'string' ? { price: value } : value
|
||||
}
|
5
framework/commerce/use-signup.tsx
Normal file
5
framework/commerce/use-signup.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from './utils/use-action'
|
||||
|
||||
const useSignup = useAction
|
||||
|
||||
export default useSignup
|
40
framework/commerce/utils/errors.ts
Normal file
40
framework/commerce/utils/errors.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export type ErrorData = {
|
||||
message: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
export type ErrorProps = {
|
||||
code?: string
|
||||
} & (
|
||||
| { message: string; errors?: never }
|
||||
| { message?: never; errors: ErrorData[] }
|
||||
)
|
||||
|
||||
export class CommerceError extends Error {
|
||||
code?: string
|
||||
errors: ErrorData[]
|
||||
|
||||
constructor({ message, code, errors }: ErrorProps) {
|
||||
const error: ErrorData = message
|
||||
? { message, ...(code ? { code } : {}) }
|
||||
: errors![0]
|
||||
|
||||
super(error.message)
|
||||
this.errors = message ? [error] : errors!
|
||||
|
||||
if (error.code) this.code = error.code
|
||||
}
|
||||
}
|
||||
|
||||
export class FetcherError extends CommerceError {
|
||||
status: number
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
status: number
|
||||
} & ErrorProps
|
||||
) {
|
||||
super(options)
|
||||
this.status = options.status
|
||||
}
|
||||
}
|
24
framework/commerce/utils/types.ts
Normal file
24
framework/commerce/utils/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Core fetcher added by CommerceProvider
|
||||
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
|
||||
|
||||
export type FetcherOptions = {
|
||||
url?: string
|
||||
query?: string
|
||||
method?: string
|
||||
variables?: any
|
||||
body?: any
|
||||
}
|
||||
|
||||
export type HookFetcher<Result, Input = null> = (
|
||||
options: HookFetcherOptions | null,
|
||||
input: Input,
|
||||
fetch: <T = Result>(options: FetcherOptions) => Promise<T>
|
||||
) => Result | Promise<Result>
|
||||
|
||||
export type HookFetcherOptions = {
|
||||
query?: string
|
||||
url?: string
|
||||
method?: string
|
||||
}
|
||||
|
||||
export type HookInput = [string, string | number | boolean | undefined][]
|
15
framework/commerce/utils/use-action.tsx
Normal file
15
framework/commerce/utils/use-action.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher, HookFetcherOptions } from './types'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export default function useAction<T, Input = null>(
|
||||
options: HookFetcherOptions,
|
||||
fetcher: HookFetcher<T, Input>
|
||||
) {
|
||||
const { fetcherRef } = useCommerce()
|
||||
|
||||
return useCallback(
|
||||
(input: Input) => fetcher(options, input, fetcherRef.current),
|
||||
[fetcher]
|
||||
)
|
||||
}
|
60
framework/commerce/utils/use-data.tsx
Normal file
60
framework/commerce/utils/use-data.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import useSWR, { ConfigInterface, responseInterface } from 'swr'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
|
||||
import { CommerceError } from './errors'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export type SwrOptions<Result, Input = null> = ConfigInterface<
|
||||
Result,
|
||||
CommerceError,
|
||||
HookFetcher<Result, Input>
|
||||
>
|
||||
|
||||
export type UseData = <Result = any, Input = null>(
|
||||
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, Input>,
|
||||
swrOptions?: SwrOptions<Result, Input>
|
||||
) => responseInterface<Result, CommerceError>
|
||||
|
||||
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(
|
||||
{ url, query, method },
|
||||
// Transform the input array into an object
|
||||
args.reduce((obj, val, i) => {
|
||||
obj[input[i][0]!] = val
|
||||
return obj
|
||||
}, {}),
|
||||
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
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export default useData
|
5
framework/commerce/wishlist/use-add-item.tsx
Normal file
5
framework/commerce/wishlist/use-add-item.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from '../utils/use-action'
|
||||
|
||||
const useAddItem = useAction
|
||||
|
||||
export default useAddItem
|
5
framework/commerce/wishlist/use-remove-item.tsx
Normal file
5
framework/commerce/wishlist/use-remove-item.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import useAction from '../utils/use-action'
|
||||
|
||||
const useRemoveItem = useAction
|
||||
|
||||
export default useRemoveItem
|
17
framework/commerce/wishlist/use-wishlist.tsx
Normal file
17
framework/commerce/wishlist/use-wishlist.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { responseInterface } from 'swr'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useData, { SwrOptions } from '../utils/use-data'
|
||||
|
||||
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
|
||||
isEmpty: boolean
|
||||
}
|
||||
|
||||
export default function useWishlist<Result, Input = null>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, Input>,
|
||||
swrOptions?: SwrOptions<Result, Input>
|
||||
) {
|
||||
const response = useData(options, input, fetcherFn, swrOptions)
|
||||
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
|
||||
}
|
Reference in New Issue
Block a user