Added framework folder

This commit is contained in:
Luis Alvarez
2020-12-29 19:14:49 -05:00
parent 5b5c8702a3
commit ed0783cfb3
88 changed files with 11839 additions and 3 deletions

View 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

View File

@@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useAddItem = useAction
export default useAddItem

View 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 }
}

View 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>
}

View File

@@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useRemoveItem = useAction
export default useRemoveItem

View File

@@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useUpdateItem = useAction
export default useUpdateItem

View 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
}

View File

@@ -0,0 +1,5 @@
import useData from '../utils/use-data'
const useSearch = useData
export default useSearch

View File

@@ -0,0 +1,5 @@
import useData from './utils/use-data'
const useCustomer = useData
export default useCustomer

View File

@@ -0,0 +1,5 @@
import useAction from './utils/use-action'
const useLogin = useAction
export default useLogin

View File

@@ -0,0 +1,5 @@
import useAction from './utils/use-action'
const useLogout = useAction
export default useLogout

View 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
}

View File

@@ -0,0 +1,5 @@
import useAction from './utils/use-action'
const useSignup = useAction
export default useSignup

View 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
}
}

View 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][]

View 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]
)
}

View 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

View File

@@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useAddItem = useAction
export default useAddItem

View File

@@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useRemoveItem = useAction
export default useRemoveItem

View 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>
}