mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 22:42:33 +00:00
Merge branch 'agnostic' of github.com:vercel/commerce into agnostic
This commit is contained in:
commit
c8536ff720
@ -3,7 +3,6 @@
|
||||
--primary-2: #f1f3f5;
|
||||
--secondary: #000000;
|
||||
--secondary-2: #111;
|
||||
|
||||
--selection: var(--cyan);
|
||||
|
||||
--text-base: #000000;
|
||||
@ -13,18 +12,14 @@
|
||||
--hover: rgba(0, 0, 0, 0.075);
|
||||
--hover-1: rgba(0, 0, 0, 0.15);
|
||||
--hover-2: rgba(0, 0, 0, 0.25);
|
||||
|
||||
--cyan: #22b8cf;
|
||||
--green: #37b679;
|
||||
--red: #da3c3c;
|
||||
--pink: #e64980;
|
||||
--purple: #f81ce5;
|
||||
|
||||
--blue: #0070f3;
|
||||
|
||||
--violet-light: #7048e8;
|
||||
--violet: #5f3dc4;
|
||||
|
||||
--violet-light: #7048e8;
|
||||
--accents-0: #f8f9fa;
|
||||
--accents-1: #f1f3f5;
|
||||
--accents-2: #e9ecef;
|
||||
@ -132,3 +127,4 @@ a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
.fit {
|
||||
min-height: calc(100vh - 88px);
|
||||
}
|
||||
}
|
@ -75,6 +75,8 @@ const CartItem = ({
|
||||
setRemoving(false)
|
||||
}
|
||||
}
|
||||
// TODO: Add a type for this
|
||||
const options = (item as any).options
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the quantity state if the item quantity changes
|
||||
@ -95,8 +97,8 @@ const CartItem = ({
|
||||
className={s.productImage}
|
||||
width={150}
|
||||
height={150}
|
||||
src={item.variant.image.url}
|
||||
alt={item.variant.image.altText}
|
||||
src={item.variant.image!.url}
|
||||
alt={item.variant.image!.altText}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
@ -109,15 +111,15 @@ const CartItem = ({
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
{item.options && item.options.length > 0 ? (
|
||||
{options && options.length > 0 ? (
|
||||
<div className="">
|
||||
{item.options.map((option: ItemOption, i: number) => (
|
||||
{options.map((option: ItemOption, i: number) => (
|
||||
<span
|
||||
key={`${item.id}-${option.name}`}
|
||||
className="text-sm font-semibold text-accents-7"
|
||||
>
|
||||
{option.value}
|
||||
{i === item.options.length - 1 ? '' : ', '}
|
||||
{i === options.length - 1 ? '' : ', '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FC, useState, useMemo, useRef, useEffect } from 'react'
|
||||
import { getRandomPairOfColors } from '@lib/colors'
|
||||
import { FC, useRef, useEffect } from 'react'
|
||||
import { useUserAvatar } from '@lib/hooks/useUserAvatar'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
@ -7,18 +7,13 @@ interface Props {
|
||||
}
|
||||
|
||||
const Avatar: FC<Props> = ({}) => {
|
||||
const [bg] = useState(useMemo(() => getRandomPairOfColors, []))
|
||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && ref.current) {
|
||||
ref.current.style.backgroundImage = `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`
|
||||
}
|
||||
}, [bg])
|
||||
let { userAvatar } = useUserAvatar()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ backgroundImage: userAvatar }}
|
||||
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition linear-out duration-150"
|
||||
>
|
||||
{/* Add an image - We're generating a gradient as placeholder <img></img> */}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { Product } from '@commerce/types'
|
||||
import { Grid } from '@components/ui'
|
||||
import { ProductCard } from '@components/product'
|
||||
import s from './HomeAllProductsGrid.module.css'
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
@apply outline-none shadow-outline-2;
|
||||
@apply outline-none shadow-outline-normal;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
|
@ -24,7 +24,11 @@
|
||||
}
|
||||
|
||||
.bagCount {
|
||||
@apply border border-accents-1 bg-secondary text-secondary h-4 w-4 absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
||||
@apply border border-accents-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
||||
padding-left: 2.5px;
|
||||
padding-right: 2.5px;
|
||||
min-width: 1.25rem;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.avatarButton {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import type { Product } from '@commerce/types'
|
||||
import s from './ProductCard.module.css'
|
||||
import Image, { ImageProps } from 'next/image'
|
||||
import frameworkConfig from '@framework/config.json'
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
.leftControl:hover,
|
||||
.rightControl:hover {
|
||||
@apply outline-none shadow-outline-blue;
|
||||
@apply outline-none shadow-outline-normal;
|
||||
}
|
||||
|
||||
.leftControl {
|
||||
@ -70,7 +70,7 @@
|
||||
}
|
||||
|
||||
.positionIndicator:focus .dot {
|
||||
@apply shadow-outline-blue;
|
||||
@apply shadow-outline-normal;
|
||||
}
|
||||
|
||||
.positionIndicatorActive .dot {
|
||||
|
@ -8,6 +8,7 @@ import { useUI } from '@components/ui'
|
||||
import { Swatch, ProductSlider } from '@components/product'
|
||||
import { Button, Container, Text } from '@components/ui'
|
||||
|
||||
import type { Product } from '@commerce/types'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import { useAddItem } from '@framework/cart'
|
||||
|
||||
@ -41,8 +42,8 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: product.id,
|
||||
variantId: variant ? variant.id : product.variants[0].id,
|
||||
productId: String(product.id),
|
||||
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
|
||||
export type SelectedOptions = {
|
||||
size: string | null
|
||||
color: string | null
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.root:focus {
|
||||
@apply shadow-outline outline-none;
|
||||
@apply shadow-outline-normal outline-none;
|
||||
}
|
||||
|
||||
.root[data-active] {
|
||||
|
@ -3,5 +3,5 @@
|
||||
}
|
||||
|
||||
.root:focus {
|
||||
@apply outline-none shadow-outline-gray;
|
||||
@apply outline-none shadow-outline-normal;
|
||||
}
|
||||
|
@ -52,6 +52,10 @@ type Action =
|
||||
type: 'SET_MODAL_VIEW'
|
||||
view: MODAL_VIEWS
|
||||
}
|
||||
| {
|
||||
type: 'SET_USER_AVATAR'
|
||||
value: string
|
||||
}
|
||||
|
||||
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
|
||||
type ToastText = string
|
||||
@ -123,6 +127,12 @@ function uiReducer(state: State, action: Action) {
|
||||
toastText: action.text,
|
||||
}
|
||||
}
|
||||
case 'SET_USER_AVATAR': {
|
||||
return {
|
||||
...state,
|
||||
userAvatar: action.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +157,9 @@ export const UIProvider: FC = (props) => {
|
||||
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
|
||||
const closeToast = () => dispatch({ type: 'CLOSE_TOAST' })
|
||||
|
||||
const setUserAvatar = (value: string) =>
|
||||
dispatch({ type: 'SET_USER_AVATAR', value })
|
||||
|
||||
const setModalView = (view: MODAL_VIEWS) =>
|
||||
dispatch({ type: 'SET_MODAL_VIEW', view })
|
||||
|
||||
@ -164,6 +177,7 @@ export const UIProvider: FC = (props) => {
|
||||
setModalView,
|
||||
openToast,
|
||||
closeToast,
|
||||
setUserAvatar,
|
||||
}),
|
||||
[state]
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ import cn from 'classnames'
|
||||
import { Heart } from '@components/icons'
|
||||
|
||||
import { useUI } from '@components/ui'
|
||||
import type { Product, ProductVariant } from '@commerce/types'
|
||||
import useCustomer from '@framework/customer/use-customer'
|
||||
import useAddItem from '@framework/wishlist/use-add-item'
|
||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||
|
@ -7,6 +7,7 @@ import { Trash } from '@components/icons'
|
||||
import { Button, Text } from '@components/ui'
|
||||
|
||||
import { useUI } from '@components/ui/context'
|
||||
import type { Product } from '@commerce/types'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useAddItem from '@framework/cart/use-add-item'
|
||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||
@ -42,8 +43,8 @@ const WishlistCard: FC<Props> = ({ product }) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: product.id,
|
||||
variantId: product.variants[0].id,
|
||||
productId: String(product.id),
|
||||
variantId: String(product.variants[0].id),
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Product } from 'framework/types'
|
||||
import { Product } from '@commerce/types'
|
||||
import getAllProducts, { ProductEdge } from '../../../product/get-all-products'
|
||||
import type { ProductsHandlers } from '../products'
|
||||
|
||||
@ -60,7 +60,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: Product
|
||||
}>((prods, p) => {
|
||||
prods[p.id] = p
|
||||
prods[Number(p.id)] = p
|
||||
return prods
|
||||
}, {})
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
import isAllowedMethod from '../utils/is-allowed-method'
|
||||
import createApiHandler, {
|
||||
BigcommerceApiHandler,
|
||||
@ -5,7 +6,6 @@ import createApiHandler, {
|
||||
} from '../utils/create-api-handler'
|
||||
import { BigcommerceApiError } from '../utils/errors'
|
||||
import getProducts from './handlers/get-products'
|
||||
import { Product } from 'framework/types'
|
||||
|
||||
export type SearchProductsData = {
|
||||
products: Product[]
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
import getWishlist from './handlers/get-wishlist'
|
||||
import addItem from './handlers/add-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
import type { Product, ProductVariant, Customer } from '@commerce/types'
|
||||
|
||||
export type { Wishlist, WishlistItem }
|
||||
|
||||
@ -24,7 +25,7 @@ export type AddItemBody = { item: ItemBody }
|
||||
export type RemoveItemBody = { itemId: Product['id'] }
|
||||
|
||||
export type WishlistBody = {
|
||||
customer_id: Customer['id']
|
||||
customer_id: Customer['entityId']
|
||||
is_public: number
|
||||
name: string
|
||||
items: any[]
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHandler } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCartAddItem, {
|
||||
AddItemInput as UseAddItemInput,
|
||||
} from '@commerce/cart/use-add-item'
|
||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
AddCartItemBody,
|
||||
@ -12,55 +9,45 @@ import type {
|
||||
CartItemBody,
|
||||
} from '../types'
|
||||
import useCart from './use-cart'
|
||||
import { BigcommerceProvider } from '..'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type AddItemInput = UseAddItemInput<CartItemBody>
|
||||
export default useAddItem as UseAddItem<BigcommerceProvider, CartItemBody>
|
||||
|
||||
export const fetcher: HookFetcher<Cart, AddCartItemBody> = async (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
) => {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { item }, options, fetch }) {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
|
||||
return normalizeCart(data)
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = () => {
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook() {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
const data = await fn({ item: input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
return async function addItem({ input, fetch }) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,4 +1,42 @@
|
||||
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
||||
import { useMemo } from 'react'
|
||||
import { HookHandler } from '@commerce/utils/types'
|
||||
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type { Cart } from '../types'
|
||||
import type { BigcommerceProvider } from '..'
|
||||
|
||||
export default useCart as UseCart<BigcommerceProvider>
|
||||
|
||||
export const handler: HookHandler<
|
||||
Cart | null,
|
||||
{},
|
||||
FetchCartInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { cartId }, options, fetch }) {
|
||||
const data = cartId ? await fetch(options) : null
|
||||
return data && normalizeCart(data)
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input.swrOptions },
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ async function getCustomerWishlist({
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: ProductEdge
|
||||
}>((prods, p) => {
|
||||
prods[p.node.entityId] = p
|
||||
prods[Number(p.node.entityId)] = p as any
|
||||
return prods
|
||||
}, {})
|
||||
// Populate the wishlist items with the graphql products
|
||||
|
@ -1,4 +1,25 @@
|
||||
import { HookHandler } from '@commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||
import type { Customer, CustomerData } from '../api/customers'
|
||||
import type { BigcommerceProvider } from '..'
|
||||
|
||||
export default useCustomer as UseCustomer<BigcommerceProvider>
|
||||
|
||||
export const handler: HookHandler<Customer | null> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch<CustomerData | null>(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
41
framework/bigcommerce/fetcher.ts
Normal file
41
framework/bigcommerce/fetcher.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { Fetcher } from '@commerce/utils/types'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
|
||||
const fetcher: Fetcher = 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)
|
||||
}
|
||||
|
||||
export default fetcher
|
@ -1,3 +1,4 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
import type { Cart, BigcommerceCart, LineItem } from '../types'
|
||||
import update from './immutability'
|
||||
|
||||
|
@ -2,6 +2,7 @@ import type {
|
||||
GetAllProductsQuery,
|
||||
GetAllProductsQueryVariables,
|
||||
} from '../schema'
|
||||
import type { Product } from '@commerce/types'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||
@ -94,6 +95,7 @@ async function getAllProducts({
|
||||
variables?: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
// TODO: fix the product type here
|
||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||
config = getConfig(config)
|
||||
|
||||
|
@ -3,6 +3,7 @@ import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||
import { productInfoFragment } from '../api/fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { normalizeProduct } from '@framework/lib/normalize'
|
||||
import type { Product } from '@commerce/types'
|
||||
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct(
|
||||
|
@ -1,4 +1,54 @@
|
||||
import useSearch, { UseSearch } from '@commerce/cart/products/use-search'
|
||||
import { HookHandler } from '@commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@commerce/products/use-search'
|
||||
import type { SearchProductsData } from '../api/catalog/products'
|
||||
import type { BigcommerceProvider } from '..'
|
||||
|
||||
export default useSearch as UseSearch<BigcommerceProvider>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const handler: HookHandler<
|
||||
SearchProductsData,
|
||||
SearchProductsInput,
|
||||
SearchProductsInput
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
18
framework/bigcommerce/provider.ts
Normal file
18
framework/bigcommerce/provider.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { handler as useCart } from './cart/use-cart'
|
||||
import { handler as useAddItem } from './cart/use-add-item'
|
||||
import { handler as useWishlist } from './wishlist/use-wishlist'
|
||||
import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useSearch } from './product/use-search'
|
||||
import fetcher from './fetcher'
|
||||
|
||||
export const bigcommerceProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem },
|
||||
wishlist: { useWishlist },
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
}
|
||||
|
||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
@ -1,212 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { Fetcher, HookHandler } from '@commerce/utils/types'
|
||||
import type { FetchCartInput } from '@commerce/cart/use-cart'
|
||||
import { normalizeCart } from './lib/normalize'
|
||||
import type { Wishlist } from './api/wishlist'
|
||||
import type { Customer, CustomerData } from './api/customers'
|
||||
import type { SearchProductsData } from './api/catalog/products'
|
||||
import useCustomer from './customer/use-customer'
|
||||
import type { Cart } from './types'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
|
||||
const fetcher: Fetcher = 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 | null,
|
||||
{},
|
||||
FetchCartInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { cartId }, options, fetch }) {
|
||||
const data = cartId ? await fetch(options) : null
|
||||
return data && normalizeCart(data)
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input.swrOptions },
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const useWishlist: HookHandler<
|
||||
Wishlist | null,
|
||||
{ includeProducts?: boolean },
|
||||
{ customerId?: number; includeProducts: boolean },
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||
if (!customerId) return null
|
||||
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useData({
|
||||
input: [
|
||||
['customerId', customer?.id],
|
||||
['includeProducts', input.includeProducts],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const useCustomerHandler: HookHandler<Customer | null> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch<CustomerData | null>(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
const useSearch: HookHandler<
|
||||
SearchProductsData,
|
||||
SearchProductsInput,
|
||||
SearchProductsInput
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const bigcommerceProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
fetcher,
|
||||
cartNormalizer: normalizeCart,
|
||||
cart: { useCart },
|
||||
wishlist: { useWishlist },
|
||||
customer: { useCustomer: useCustomerHandler },
|
||||
products: { useSearch },
|
||||
}
|
||||
|
||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
@ -23,11 +23,11 @@ export type BigcommerceCart = {
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
export interface Cart extends Core.Cart {
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: LineItem[]
|
||||
}
|
||||
|
||||
export interface LineItem extends Core.LineItem {}
|
||||
export type LineItem = Core.LineItem
|
||||
|
||||
/**
|
||||
* Cart mutations
|
||||
@ -38,25 +38,24 @@ export type OptionSelections = {
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export interface CartItemBody extends Core.CartItemBody {
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export interface GetCartHandlerBody extends Core.GetCartHandlerBody {}
|
||||
type X = Core.CartItemBody extends CartItemBody ? any : never
|
||||
type Y = CartItemBody extends Core.CartItemBody ? any : never
|
||||
|
||||
export interface AddCartItemBody extends Core.AddCartItemBody<CartItemBody> {}
|
||||
export type GetCartHandlerBody = Core.GetCartHandlerBody
|
||||
|
||||
export interface AddCartItemHandlerBody
|
||||
extends Core.AddCartItemHandlerBody<CartItemBody> {}
|
||||
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
||||
|
||||
export interface UpdateCartItemBody
|
||||
extends Core.UpdateCartItemBody<CartItemBody> {}
|
||||
export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody<CartItemBody>
|
||||
|
||||
export interface UpdateCartItemHandlerBody
|
||||
extends Core.UpdateCartItemHandlerBody<CartItemBody> {}
|
||||
export type UpdateCartItemBody = Core.UpdateCartItemBody<CartItemBody>
|
||||
|
||||
export interface RemoveCartItemBody extends Core.RemoveCartItemBody {}
|
||||
export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody<CartItemBody>
|
||||
|
||||
export interface RemoveCartItemHandlerBody
|
||||
extends Core.RemoveCartItemHandlerBody {}
|
||||
export type RemoveCartItemBody = Core.RemoveCartItemBody
|
||||
|
||||
export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody
|
||||
|
@ -1,19 +1,23 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
||||
import useWishlistAddItem, {
|
||||
AddItemInput,
|
||||
} from '@commerce/wishlist/use-add-item'
|
||||
import { UseWishlistInput } from '@commerce/wishlist/use-wishlist'
|
||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useWishlist from './use-wishlist'
|
||||
import type { BigcommerceProvider } from '..'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
// export type AddItemInput = ItemBody
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
||||
export const fetcher: HookFetcher<any, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
@ -27,13 +31,13 @@ export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = (opts?: UseWishlistOptions) => {
|
||||
const useAddItem = (opts?: UseWishlistInput<BigcommerceProvider>) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
async function addItem(input: AddItemInput<any>) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
|
@ -4,7 +4,7 @@ import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
@ -15,7 +15,7 @@ export type RemoveItemInput = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
||||
export const fetcher: HookFetcher<any | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
@ -28,10 +28,10 @@ export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
||||
const useRemoveItem = (opts?: any) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
||||
const fn = useWishlistRemoveItem<any | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
|
@ -1,4 +1,59 @@
|
||||
import { useMemo } from 'react'
|
||||
import { HookHandler } from '@commerce/utils/types'
|
||||
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||
import type { Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import type { BigcommerceProvider } from '..'
|
||||
|
||||
export default useWishlist as UseWishlist<BigcommerceProvider>
|
||||
|
||||
export const handler: HookHandler<
|
||||
Wishlist | null,
|
||||
{ includeProducts?: boolean },
|
||||
{ customerId?: number; includeProducts: boolean },
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||
if (!customerId) return null
|
||||
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook({ input, useData }) {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useData({
|
||||
input: [
|
||||
['customerId', (customer as any)?.id],
|
||||
['includeProducts', input.includeProducts],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -1,9 +1,69 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import type { CartItemBody } from '../types'
|
||||
import { useCallback } from 'react'
|
||||
import type {
|
||||
Prop,
|
||||
HookFetcherFn,
|
||||
UseHookInput,
|
||||
UseHookResponse,
|
||||
} from '../utils/types'
|
||||
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
|
||||
import { Provider, useCommerce } from '..'
|
||||
import { BigcommerceProvider } from '@framework'
|
||||
|
||||
export type UseAddItemHandler<P extends Provider> = Prop<
|
||||
Prop<P, 'cart'>,
|
||||
'useAddItem'
|
||||
>
|
||||
|
||||
// Input expected by the action returned by the `useAddItem` hook
|
||||
export type AddItemInput<T extends CartItemBody> = T
|
||||
export type UseAddItemInput<P extends Provider> = UseHookInput<
|
||||
UseAddItemHandler<P>
|
||||
>
|
||||
|
||||
const useAddItem = useAction
|
||||
export type UseAddItemResult<P extends Provider> = ReturnType<
|
||||
UseHookResponse<UseAddItemHandler<P>>
|
||||
>
|
||||
|
||||
export default useAddItem
|
||||
export type UseAddItem<P extends Provider, Input> = Partial<
|
||||
UseAddItemInput<P>
|
||||
> extends UseAddItemInput<P>
|
||||
? (input?: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
|
||||
: (input: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
|
||||
|
||||
export const fetcher: HookFetcherFn<
|
||||
Cart,
|
||||
AddCartItemBody<CartItemBody>
|
||||
> = async ({ options, input, fetch }) => {
|
||||
return fetch({ ...options, body: input })
|
||||
}
|
||||
|
||||
type X = UseAddItemResult<BigcommerceProvider>
|
||||
|
||||
export default function useAddItem<P extends Provider, Input>(
|
||||
input: UseAddItemInput<P>
|
||||
) {
|
||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
||||
|
||||
const provider = providerRef.current
|
||||
const opts = provider.cart?.useAddItem
|
||||
|
||||
const fetcherFn = opts?.fetcher ?? fetcher
|
||||
const useHook = opts?.useHook ?? (() => () => {})
|
||||
const fetchFn = provider.fetcher ?? fetcherRef.current
|
||||
const action = useHook({ input })
|
||||
|
||||
return useCallback(
|
||||
function addItem(input: Input) {
|
||||
return action({
|
||||
input,
|
||||
fetch({ input }) {
|
||||
return fetcherFn({
|
||||
input,
|
||||
options: opts!.fetchOptions,
|
||||
fetch: fetchFn,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
[input, fetchFn, opts?.fetchOptions]
|
||||
)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { Fetcher, HookHandler } from './utils/types'
|
||||
import { Fetcher, HookHandler, MutationHandler } from './utils/types'
|
||||
import type { FetchCartInput } from './cart/use-cart'
|
||||
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
|
||||
|
||||
@ -16,6 +16,7 @@ export type Provider = CommerceConfig & {
|
||||
fetcher: Fetcher
|
||||
cart?: {
|
||||
useCart?: HookHandler<Cart | null, any, FetchCartInput>
|
||||
useAddItem?: MutationHandler<Cart, any, any>
|
||||
}
|
||||
wishlist?: {
|
||||
useWishlist?: HookHandler<Wishlist | null, any, any>
|
||||
|
75
framework/commerce/types.d.ts
vendored
75
framework/commerce/types.d.ts
vendored
@ -1,75 +0,0 @@
|
||||
interface Entity {
|
||||
id: string | number
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
interface Product extends Entity {
|
||||
name: string
|
||||
description: string
|
||||
slug?: string
|
||||
path?: string
|
||||
images: ProductImage[]
|
||||
variants: ProductVariant[]
|
||||
price: ProductPrice
|
||||
options: ProductOption[]
|
||||
sku?: string
|
||||
}
|
||||
|
||||
interface ProductOption extends Entity {
|
||||
displayName: string
|
||||
values: ProductOptionValues[]
|
||||
}
|
||||
|
||||
interface ProductOptionValues {
|
||||
label: string
|
||||
hexColors?: string[]
|
||||
}
|
||||
|
||||
interface ProductImage {
|
||||
url: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
interface ProductVariant {
|
||||
id: string | number
|
||||
options: ProductOption[]
|
||||
}
|
||||
|
||||
interface ProductPrice {
|
||||
value: number
|
||||
currencyCode: 'USD' | 'ARS' | string | undefined
|
||||
retailPrice?: number
|
||||
salePrice?: number
|
||||
listPrice?: number
|
||||
extendedSalePrice?: number
|
||||
extendedListPrice?: number
|
||||
}
|
||||
|
||||
interface CartItem extends Entity {
|
||||
quantity: number
|
||||
productId: Product['id']
|
||||
variantId: ProductVariant['id']
|
||||
images: ProductImage[]
|
||||
}
|
||||
|
||||
interface Wishlist extends Entity {
|
||||
products: Pick<Product, 'id' | 'name' | 'prices'>[]
|
||||
}
|
||||
|
||||
interface Order {}
|
||||
|
||||
interface Customer extends Entity {}
|
||||
|
||||
type UseCustomerResponse = {
|
||||
customer: Customer
|
||||
} | null
|
||||
|
||||
interface Category extends Entity {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Brand extends Entity {
|
||||
name: string
|
||||
}
|
||||
|
||||
type Features = 'wishlist' | 'customer'
|
@ -2,12 +2,12 @@ import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
|
||||
import type { Customer as BCCustomer } from '@framework/api/customers'
|
||||
import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products'
|
||||
|
||||
export interface Discount {
|
||||
export type Discount = {
|
||||
// The value of the discount, can be an amount or percentage
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface LineItem {
|
||||
export type LineItem = {
|
||||
id: string
|
||||
variantId: string
|
||||
productId: string
|
||||
@ -19,19 +19,19 @@ export interface LineItem {
|
||||
variant: ProductVariant
|
||||
}
|
||||
|
||||
export interface Measurement {
|
||||
export type Measurement = {
|
||||
value: number
|
||||
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
export type Image = {
|
||||
url: string
|
||||
altText?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface ProductVariant {
|
||||
export type ProductVariant = {
|
||||
id: string
|
||||
// The SKU (stock keeping unit) associated with the product variant.
|
||||
sku: string
|
||||
@ -66,7 +66,7 @@ export interface ProductVariant {
|
||||
}
|
||||
|
||||
// Shopping cart, a.k.a Checkout
|
||||
export interface Cart {
|
||||
export type Cart = {
|
||||
id: string
|
||||
// ID of the customer to which the cart belongs.
|
||||
customerId?: string
|
||||
@ -105,53 +105,99 @@ export interface SearchProductsData extends BCSearchProductsData {}
|
||||
*/
|
||||
|
||||
// Base cart item body used for cart mutations
|
||||
export interface CartItemBody {
|
||||
export type CartItemBody = {
|
||||
variantId: string
|
||||
productId?: string
|
||||
quantity?: number
|
||||
}
|
||||
|
||||
// Body used by the `getCart` operation handler
|
||||
export interface GetCartHandlerBody {
|
||||
export type GetCartHandlerBody = {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Body used by the add item to cart operation
|
||||
export interface AddCartItemBody<T extends CartItemBody> {
|
||||
export type AddCartItemBody<T extends CartItemBody> = {
|
||||
item: T
|
||||
}
|
||||
|
||||
// Body expected by the add item to cart operation handler
|
||||
export interface AddCartItemHandlerBody<T extends CartItemBody>
|
||||
extends Partial<AddCartItemBody<T>> {
|
||||
export type AddCartItemHandlerBody<T extends CartItemBody> = Partial<
|
||||
AddCartItemBody<T>
|
||||
> & {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Body used by the update cart item operation
|
||||
export interface UpdateCartItemBody<T extends CartItemBody> {
|
||||
export type UpdateCartItemBody<T extends CartItemBody> = {
|
||||
itemId: string
|
||||
item: T
|
||||
}
|
||||
|
||||
// Body expected by the update cart item operation handler
|
||||
export interface UpdateCartItemHandlerBody<T extends CartItemBody>
|
||||
extends Partial<UpdateCartItemBody<T>> {
|
||||
export type UpdateCartItemHandlerBody<T extends CartItemBody> = Partial<
|
||||
UpdateCartItemBody<T>
|
||||
> & {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Body used by the remove cart item operation
|
||||
export interface RemoveCartItemBody {
|
||||
export type RemoveCartItemBody = {
|
||||
itemId: string
|
||||
}
|
||||
|
||||
// Body expected by the remove cart item operation handler
|
||||
export interface RemoveCartItemHandlerBody extends Partial<RemoveCartItemBody> {
|
||||
export type RemoveCartItemHandlerBody = Partial<RemoveCartItemBody> & {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Features API
|
||||
type Features = 'wishlist' | 'checkout'
|
||||
/**
|
||||
* Temporal types
|
||||
*/
|
||||
|
||||
export interface FrameworkConfig {
|
||||
features: Record<Features, boolean>
|
||||
interface Entity {
|
||||
id: string | number
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export interface Product extends Entity {
|
||||
name: string
|
||||
description: string
|
||||
slug?: string
|
||||
path?: string
|
||||
images: ProductImage[]
|
||||
variants: ProductVariant2[]
|
||||
price: ProductPrice
|
||||
options: ProductOption[]
|
||||
sku?: string
|
||||
}
|
||||
|
||||
interface ProductOption extends Entity {
|
||||
displayName: string
|
||||
values: ProductOptionValues[]
|
||||
}
|
||||
|
||||
interface ProductOptionValues {
|
||||
label: string
|
||||
hexColors?: string[]
|
||||
}
|
||||
|
||||
interface ProductImage {
|
||||
url: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
interface ProductVariant2 {
|
||||
id: string | number
|
||||
options: ProductOption[]
|
||||
}
|
||||
|
||||
interface ProductPrice {
|
||||
value: number
|
||||
currencyCode: 'USD' | 'ARS' | string | undefined
|
||||
retailPrice?: number
|
||||
salePrice?: number
|
||||
listPrice?: number
|
||||
extendedSalePrice?: number
|
||||
extendedListPrice?: number
|
||||
}
|
||||
|
@ -76,6 +76,24 @@ export type HookHandler<
|
||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||
}
|
||||
|
||||
export type MutationHandler<
|
||||
// Data obj returned by the hook and fetch operation
|
||||
Data,
|
||||
// Input expected by the hook
|
||||
Input extends { [k: string]: unknown } = {},
|
||||
// Input expected before doing a fetch operation
|
||||
FetchInput extends { [k: string]: unknown } = {}
|
||||
> = {
|
||||
useHook?(context: {
|
||||
input: Input
|
||||
}): (context: {
|
||||
input: FetchInput
|
||||
fetch: (context: { input: FetchInput }) => Data | Promise<Data>
|
||||
}) => Data | Promise<Data>
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||
}
|
||||
|
||||
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||
Data,
|
||||
CommerceError,
|
||||
@ -87,14 +105,18 @@ export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||
*/
|
||||
export type Prop<T, K extends keyof T> = NonNullable<T[K]>
|
||||
|
||||
export type UseHookParameters<H extends HookHandler<any>> = Parameters<
|
||||
export type HookHandlerType =
|
||||
| HookHandler<any, any, any>
|
||||
| MutationHandler<any, any, any>
|
||||
|
||||
export type UseHookParameters<H extends HookHandlerType> = Parameters<
|
||||
Prop<H, 'useHook'>
|
||||
>
|
||||
|
||||
export type UseHookResponse<H extends HookHandler<any>> = ReturnType<
|
||||
export type UseHookResponse<H extends HookHandlerType> = ReturnType<
|
||||
Prop<H, 'useHook'>
|
||||
>
|
||||
|
||||
export type UseHookInput<
|
||||
H extends HookHandler<any>
|
||||
H extends HookHandlerType
|
||||
> = UseHookParameters<H>[0]['input']
|
||||
|
@ -1,4 +1,11 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import type { CartItemBody } from '../types'
|
||||
|
||||
// Input expected by the action returned by the `useAddItem` hook
|
||||
// export interface AddItemInput {
|
||||
// includeProducts?: boolean
|
||||
// }
|
||||
export type AddItemInput<T extends CartItemBody> = T
|
||||
|
||||
const useAddItem = useAction
|
||||
|
||||
|
26
lib/hooks/useUserAvatar.ts
Normal file
26
lib/hooks/useUserAvatar.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { getRandomPairOfColors } from '@lib/colors'
|
||||
|
||||
export const useUserAvatar = (name = 'userAvatar') => {
|
||||
const { userAvatar, setUserAvatar } = useUI()
|
||||
|
||||
useEffect(() => {
|
||||
if (!userAvatar && localStorage.getItem(name)) {
|
||||
// Get bg from localStorage and push it to the context.
|
||||
setUserAvatar(localStorage.getItem(name))
|
||||
}
|
||||
if (!localStorage.getItem(name)) {
|
||||
// bg not set locally, generating one, setting localStorage and context to persist.
|
||||
const bg = getRandomPairOfColors()
|
||||
const value = `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`
|
||||
localStorage.setItem(name, value)
|
||||
setUserAvatar(value)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
userAvatar,
|
||||
setUserAvatar,
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import bunyan from 'bunyan'
|
||||
import PrettyStream from 'bunyan-prettystream'
|
||||
|
||||
const prettyStdOut = new PrettyStream()
|
||||
|
||||
const log = bunyan.createLogger({
|
||||
name: 'Next.js - Commerce',
|
||||
level: 'debug',
|
||||
streams: [
|
||||
{
|
||||
level: 'debug',
|
||||
type: 'raw',
|
||||
stream: prettyStdOut,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default log
|
15
package.json
15
package.json
@ -21,7 +21,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@reach/portal": "^0.11.2",
|
||||
"@tailwindcss/ui": "^0.6.2",
|
||||
"@vercel/fetch": "^6.1.0",
|
||||
"body-scroll-lock": "^3.1.5",
|
||||
"bowser": "^2.11.0",
|
||||
@ -35,18 +34,18 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.random": "^3.2.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"next": "^10.0.5",
|
||||
"next": "^10.0.7-canary.3",
|
||||
"next-seo": "^4.11.0",
|
||||
"next-themes": "^0.0.4",
|
||||
"normalizr": "^3.6.1",
|
||||
"postcss": "^8.2.4",
|
||||
"postcss-nesting": "^7.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-ticker": "^1.2.2",
|
||||
"swr": "^0.4.0",
|
||||
"tabbable": "^5.1.5",
|
||||
"tailwindcss": "^1.9"
|
||||
"tailwindcss": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^1.20.0",
|
||||
@ -56,8 +55,6 @@
|
||||
"@manifoldco/swagger-to-ts": "^2.1.0",
|
||||
"@next/bundle-analyzer": "^10.0.1",
|
||||
"@types/body-scroll-lock": "^2.6.1",
|
||||
"@types/bunyan": "^1.8.6",
|
||||
"@types/bunyan-prettystream": "^0.1.31",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
@ -66,8 +63,6 @@
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/node": "^14.14.16",
|
||||
"@types/react": "^17.0.0",
|
||||
"bunyan": "^1.8.14",
|
||||
"bunyan-prettystream": "^0.1.3",
|
||||
"graphql": "^15.4.0",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^10.5.3",
|
||||
|
@ -61,7 +61,7 @@ export default function Slug({
|
||||
return router.isFallback ? (
|
||||
<h1>Loading...</h1> // TODO (BC) Add Skeleton Views
|
||||
) : (
|
||||
<ProductView product={product} />
|
||||
<ProductView product={product as any} />
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ export default function Search({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleClick(e, 'categories')}
|
||||
className="flex justify-between w-full rounded-sm border border-gray-300 px-4 py-3 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150"
|
||||
className="flex justify-between w-full rounded-sm border border-gray-300 px-4 py-3 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150"
|
||||
id="options-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="true"
|
||||
@ -194,7 +194,7 @@ export default function Search({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleClick(e, 'brands')}
|
||||
className="flex justify-between w-full rounded-sm border border-gray-300 px-4 py-3 bg-white text-sm leading-5 font-medium text-gray-900 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150"
|
||||
className="flex justify-between w-full rounded-sm border border-gray-300 px-4 py-3 bg-white text-sm leading-5 font-medium text-gray-900 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150"
|
||||
id="options-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="true"
|
||||
@ -371,7 +371,7 @@ export default function Search({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleClick(e, 'sort')}
|
||||
className="flex justify-between w-full rounded-sm border border-gray-300 px-4 py-3 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150"
|
||||
className="flex justify-between w-full rounded-sm border border-gray-300 px-4 py-3 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150"
|
||||
id="options-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="true"
|
||||
|
@ -59,7 +59,7 @@ export default function Wishlist() {
|
||||
) : (
|
||||
data &&
|
||||
data.items?.map((item) => (
|
||||
<WishlistCard key={item.id} item={item} />
|
||||
<WishlistCard key={item.id} product={item as any} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
@ -51,7 +51,7 @@ module.exports = {
|
||||
secondary: 'var(--text-secondary)',
|
||||
},
|
||||
boxShadow: {
|
||||
'outline-2': '0 0 0 2px var(--accents-2)',
|
||||
'outline-normal': '0 0 0 2px var(--accents-2)',
|
||||
magical:
|
||||
'rgba(0, 0, 0, 0.02) 0px 30px 30px, rgba(0, 0, 0, 0.03) 0px 0px 8px, rgba(0, 0, 0, 0.05) 0px 1px 0px',
|
||||
},
|
||||
@ -63,5 +63,4 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/ui')],
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user