1
0
mirror of https://github.com/vercel/commerce.git synced 2025-05-21 17:06:58 +00:00

Moved auth & cart hooks + several fixes

This commit is contained in:
cond0r 2021-02-22 14:06:34 +02:00
parent 005fe9d6c9
commit 528d7556a8
53 changed files with 447 additions and 331 deletions

@ -3,7 +3,6 @@
--primary-2: #f1f3f5; --primary-2: #f1f3f5;
--secondary: #000000; --secondary: #000000;
--secondary-2: #111; --secondary-2: #111;
--selection: var(--cyan); --selection: var(--cyan);
--text-base: #000000; --text-base: #000000;
@ -13,18 +12,14 @@
--hover: rgba(0, 0, 0, 0.075); --hover: rgba(0, 0, 0, 0.075);
--hover-1: rgba(0, 0, 0, 0.15); --hover-1: rgba(0, 0, 0, 0.15);
--hover-2: rgba(0, 0, 0, 0.25); --hover-2: rgba(0, 0, 0, 0.25);
--cyan: #22b8cf; --cyan: #22b8cf;
--green: #37b679; --green: #37b679;
--red: #da3c3c; --red: #da3c3c;
--pink: #e64980; --pink: #e64980;
--purple: #f81ce5; --purple: #f81ce5;
--blue: #0070f3; --blue: #0070f3;
--violet-light: #7048e8;
--violet: #5f3dc4; --violet: #5f3dc4;
--violet-light: #7048e8;
--accents-0: #f8f9fa; --accents-0: #f8f9fa;
--accents-1: #f1f3f5; --accents-1: #f1f3f5;
--accents-2: #e9ecef; --accents-2: #e9ecef;
@ -132,3 +127,4 @@ a {
opacity: 1; opacity: 1;
} }
} }

@ -1,5 +1,5 @@
import { FC, useState, useMemo, useRef, useEffect } from 'react' import { FC, useRef, useEffect } from 'react'
import { getRandomPairOfColors } from '@lib/colors' import { useUserAvatar } from '@lib/hooks/useUserAvatar'
interface Props { interface Props {
className?: string className?: string
@ -7,18 +7,13 @@ interface Props {
} }
const Avatar: FC<Props> = ({}) => { const Avatar: FC<Props> = ({}) => {
const [bg] = useState(useMemo(() => getRandomPairOfColors, []))
let ref = useRef() as React.MutableRefObject<HTMLInputElement> let ref = useRef() as React.MutableRefObject<HTMLInputElement>
let { userAvatar } = useUserAvatar()
useEffect(() => {
if (ref && ref.current) {
ref.current.style.backgroundImage = `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`
}
}, [bg])
return ( return (
<div <div
ref={ref} 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" 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> */} {/* Add an image - We're generating a gradient as placeholder <img></img> */}

@ -27,7 +27,7 @@ const HomeAllProductsGrid: FC<Props> = ({
<ul className="mb-10"> <ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide"> <li className="py-1 text-base font-bold tracking-wide">
<Link href={getCategoryPath('')}> <Link href={getCategoryPath('')}>
<a>All Collections</a> <a>All Categories</a>
</Link> </Link>
</li> </li>
{categories.map((cat: any) => ( {categories.map((cat: any) => (
@ -38,6 +38,20 @@ const HomeAllProductsGrid: FC<Props> = ({
</li> </li>
))} ))}
</ul> </ul>
<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getDesignerPath('')}>
<a>All Designers</a>
</Link>
</li>
{brands.flatMap(({ node }: any) => (
<li key={node.path} className="py-1 text-accents-8 text-base">
<Link href={getDesignerPath(node.path)}>
<a>{node.name}</a>
</Link>
</li>
))}
</ul>
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1">

@ -58,7 +58,7 @@ const Layout: FC<Props> = ({
} = useUI() } = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies() const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter() const { locale = 'en-US' } = useRouter()
const isWishlistEnabled = commerceFeatures.wishlist const isWishlistEnabled = commerceFeatures?.wishlist
return ( return (
<CommerceProvider locale={locale}> <CommerceProvider locale={locale}>
<div className={cn(s.root)}> <div className={cn(s.root)}>

@ -7,7 +7,7 @@
} }
.input:focus { .input:focus {
@apply outline-none shadow-outline-2; @apply outline-none shadow-outline-normal;
} }
.iconContainer { .iconContainer {

@ -24,7 +24,11 @@
} }
.bagCount { .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 { .avatarButton {

@ -132,5 +132,5 @@
} }
.productImage { .productImage {
@apply transform transition-transform duration-500 object-cover; @apply transform transition-transform duration-500 object-cover scale-120;
} }

@ -28,8 +28,8 @@ const ProductCard: FC<Props> = ({
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}> <a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
{variant === 'slim' ? ( {variant === 'slim' ? (
<div className="relative overflow-hidden box-border"> <div className="relative overflow-hidden box-border">
<div className="absolute inset-0 flex items-start justify-end m-1 z-20"> <div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
<span className="text-black inline-block p-3 font-bold text-xl break-words"> <span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{product.name} {product.name}
</span> </span>
</div> </div>

@ -15,7 +15,7 @@
.leftControl:hover, .leftControl:hover,
.rightControl:hover { .rightControl:hover {
@apply outline-none shadow-outline-blue; @apply outline-none shadow-outline-normal;
} }
.leftControl { .leftControl {
@ -70,7 +70,7 @@
} }
.positionIndicator:focus .dot { .positionIndicator:focus .dot {
@apply shadow-outline-blue; @apply shadow-outline-normal;
} }
.positionIndicatorActive .dot { .positionIndicatorActive .dot {

@ -7,7 +7,7 @@
} }
.root:focus { .root:focus {
@apply shadow-outline outline-none; @apply shadow-outline-normal outline-none;
} }
.root[data-active] { .root[data-active] {

@ -3,5 +3,5 @@
} }
.root:focus { .root:focus {
@apply outline-none shadow-outline-gray; @apply outline-none shadow-outline-normal;
} }

@ -127,6 +127,12 @@ function uiReducer(state: State, action: Action) {
toastText: action.text, toastText: action.text,
} }
} }
case 'SET_USER_AVATAR': {
return {
...state,
userAvatar: action.value,
}
}
} }
} }

@ -8,6 +8,7 @@ const fetchGraphqlApi: GraphQLFetcher = async (
{ variables, preview } = {}, { variables, preview } = {},
fetchOptions fetchOptions
) => { ) => {
// log.warn(query)
const config = getConfig() const config = getConfig()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions, ...fetchOptions,

@ -2,7 +2,7 @@ import type { GetProductQuery, GetProductQueryVariables } from '../schema'
import setProductLocaleMeta from '../api/utils/set-product-locale-meta' import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
import { productInfoFragment } from '../api/fragments/product' import { productInfoFragment } from '../api/fragments/product'
import { BigcommerceConfig, getConfig } from '../api' import { BigcommerceConfig, getConfig } from '../api'
import { normalizeProduct } from '@framework/utils/normalize' import { normalizeProduct } from '@framework/lib/normalize'
import type { Product } from '@commerce/types' import type { Product } from '@commerce/types'
export const getProductQuery = /* GraphQL */ ` export const getProductQuery = /* GraphQL */ `

@ -18,8 +18,8 @@ const fetchAllProducts = async ({
variables: { ...variables, cursor }, variables: { ...variables, cursor },
}) })
const edges: ProductEdge[] = data?.products?.edges ?? [] const edges: ProductEdge[] = data.products?.edges ?? []
const hasNextPage = data?.products?.pageInfo?.hasNextPage const hasNextPage = data.products?.pageInfo?.hasNextPage
acc = acc.concat(edges) acc = acc.concat(edges)
if (hasNextPage) { if (hasNextPage) {

@ -29,6 +29,6 @@ const fetchGraphqlApi: GraphQLFetcher = async (
throw getError(errors, status) throw getError(errors, status)
} }
return { data: data, res } return { data, res }
} }
export default fetchGraphqlApi export default fetchGraphqlApi

@ -1,56 +1,80 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError, ValidationError } from '@commerce/utils/errors' import { CommerceError, ValidationError } from '@commerce/utils/errors'
import useCommerceLogin from '@commerce/use-login'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import createCustomerAccessTokenMutation from '../utils/mutations/customer-access-token-create' import createCustomerAccessTokenMutation from '../utils/mutations/customer-access-token-create'
import { CustomerAccessTokenCreateInput } from '@framework/schema' import {
import handleLogin from '@framework/utils/handle-login' CustomerAccessToken,
CustomerAccessTokenCreateInput,
CustomerAccessTokenCreatePayload,
CustomerUserError,
Mutation,
MutationCheckoutCreateArgs,
} from '@framework/schema'
import useLogin, { UseLogin } from '@commerce/use-login'
import { setCustomerToken } from '@framework/utils'
const defaultOpts = { export default useLogin as UseLogin<typeof handler>
query: createCustomerAccessTokenMutation,
}
export const fetcher: HookFetcher<null, CustomerAccessTokenCreateInput> = ( const getErrorMessage = ({ code, message }: CustomerUserError) => {
options, console.log(code)
input,
fetch switch (code) {
) => { case 'UNIDENTIFIED_CUSTOMER':
if (!(input.email && input.password)) { message = 'Cannot find an account that matches the provided credentials'
throw new CommerceError({ break
message:
'A first name, last name, email and password are required to login',
})
} }
return message
return fetch({
...defaultOpts,
...options,
variables: { input },
}).then(handleLogin)
} }
export function extendHook(customFetcher: typeof fetcher) { export const handler: MutationHook<null, {}, CustomerAccessTokenCreateInput> = {
const useLogin = () => { fetchOptions: {
query: createCustomerAccessTokenMutation,
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to login',
})
}
const { customerAccessTokenCreate } = await fetch<
Mutation,
MutationCheckoutCreateArgs
>({
...options,
variables: {
input: { email, password },
},
})
const errors = customerAccessTokenCreate?.customerUserErrors
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return null
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer() const { revalidate } = useCustomer()
const fn = useCommerceLogin<null, CustomerAccessTokenCreateInput>(
defaultOpts,
customFetcher
)
return useCallback( return useCallback(
async function login(input: CustomerAccessTokenCreateInput) { async function login(input) {
const data = await fn(input) const data = await fetch({ input })
await revalidate() await revalidate()
return data return data
}, },
[fn] [fetch, revalidate]
) )
} },
useLogin.extend = extendHook
return useLogin
} }
export default extendHook(fetcher)

@ -1,6 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import useCommerceLogout from '@commerce/use-logout' import useLogout, { UseLogout } from '@commerce/use-logout'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import customerAccessTokenDeleteMutation from '@framework/utils/mutations/customer-access-token-delete' import customerAccessTokenDeleteMutation from '@framework/utils/mutations/customer-access-token-delete'
import { import {
@ -8,38 +8,32 @@ import {
setCustomerToken, setCustomerToken,
} from '@framework/utils/customer-token' } from '@framework/utils/customer-token'
const defaultOpts = { export default useLogout as UseLogout<typeof handler>
query: customerAccessTokenDeleteMutation,
}
export const fetcher: HookFetcher<null> = (options, _, fetch) => { export const handler: MutationHook<null> = {
return fetch({ fetchOptions: {
...defaultOpts, query: customerAccessTokenDeleteMutation,
...options, },
variables: { async fetcher({ options, fetch }) {
customerAccessToken: getCustomerToken(), await fetch({
}, ...options,
}).then((d) => setCustomerToken(null)) variables: {
} customerAccessToken: getCustomerToken(),
},
export function extendHook(customFetcher: typeof fetcher) { })
const useLogout = () => { setCustomerToken(null)
return null
},
useHook: ({ fetch }) => () => {
const { mutate } = useCustomer() const { mutate } = useCustomer()
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
return useCallback( return useCallback(
async function login() { async function logout() {
const data = await fn(null) const data = await fetch()
await mutate(null, false) await mutate(null, false)
return data return data
}, },
[fn] [fetch, mutate]
) )
} },
useLogout.extend = extendHook
return useLogout
} }
export default extendHook(fetcher)

@ -1,7 +1,7 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useCommerceSignup from '@commerce/use-signup' import useSignup, { UseSignup } from '@commerce/use-signup'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import { CustomerCreateInput } from '@framework/schema' import { CustomerCreateInput } from '@framework/schema'
@ -11,63 +11,64 @@ import {
} from '@framework/utils/mutations' } from '@framework/utils/mutations'
import handleLogin from '@framework/utils/handle-login' import handleLogin from '@framework/utils/handle-login'
const defaultOpts = { export default useSignup as UseSignup<typeof handler>
query: customerCreateMutation,
}
export const fetcher: HookFetcher<null, CustomerCreateInput> = ( export const handler: MutationHook<
options, null,
input, {},
fetch CustomerCreateInput,
) => { CustomerCreateInput
if (!(input.firstName && input.lastName && input.email && input.password)) { > = {
throw new CommerceError({ fetchOptions: {
message: query: customerCreateMutation,
'A first name, last name, email and password are required to signup', },
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
const data = await fetch({
...options,
variables: {
input: {
firstName,
lastName,
email,
password,
},
},
}) })
}
return fetch({
...defaultOpts,
...options,
variables: { input },
}).then(async (data) => {
try { try {
const loginData = await fetch({ const loginData = await fetch({
query: customerAccessTokenCreateMutation, query: customerAccessTokenCreateMutation,
variables: { variables: {
input: { input: {
email: input.email, email,
password: input.password, password,
}, },
}, },
}) })
handleLogin(loginData) handleLogin(loginData)
} catch (error) {} } catch (error) {}
return data return data
}) },
} useHook: ({ fetch }) => () => {
export function extendHook(customFetcher: typeof fetcher) {
const useSignup = () => {
const { revalidate } = useCustomer() const { revalidate } = useCustomer()
const fn = useCommerceSignup<null, CustomerCreateInput>(
defaultOpts,
customFetcher
)
return useCallback( return useCallback(
async function signup(input: CustomerCreateInput) { async function signup(input) {
const data = await fn(input) const data = await fetch({ input })
await revalidate() await revalidate()
return data return data
}, },
[fn] [fetch, revalidate]
) )
} },
useSignup.extend = extendHook
return useSignup
} }
export default extendHook(fetcher)

@ -1,22 +1,20 @@
import type { MutationHandler } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import useCart from './use-cart' import useCart from './use-cart'
import { ShopifyProvider } from '..' import { Cart, CartItemBody } from '../types'
import { Cart, AddCartItemBody, CartItemBody } from '../types'
import { checkoutLineItemAddMutation, getCheckoutId } from '../utils' import { checkoutLineItemAddMutation, getCheckoutId } from '../utils'
import { checkoutToCart } from './utils' import { checkoutToCart } from './utils'
import { Mutation } from '../schema' import { Mutation, MutationCheckoutLineItemsAddArgs } from '../schema'
import { useCallback } from 'react'
export default useAddItem as UseAddItem<ShopifyProvider, CartItemBody> export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHandler<Cart, {}, AddCartItemBody> = { export const handler: MutationHook<Cart, {}, CartItemBody> = {
fetchOptions: { fetchOptions: {
query: checkoutLineItemAddMutation, query: checkoutLineItemAddMutation,
}, },
async fetcher({ input, options, fetch }) { async fetcher({ input: item, options, fetch }) {
const item = input?.item ?? input
if ( if (
item.quantity && item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1) (!Number.isInteger(item.quantity) || item.quantity! < 1)
@ -26,27 +24,34 @@ export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
}) })
} }
const { checkoutLineItemsAdd }: Mutation = await fetch<any, any>({ const { checkoutLineItemsAdd } = await fetch<
Mutation,
MutationCheckoutLineItemsAddArgs
>({
...options, ...options,
variables: { variables: {
checkoutId: getCheckoutId(),
lineItems: [ lineItems: [
{ {
variantId: item.variantId, variantId: item.variantId,
quantity: item.quantity ?? 1, quantity: item.quantity ?? 1,
}, },
], ],
checkoutId: getCheckoutId(),
}, },
}) })
return checkoutToCart(checkoutLineItemsAdd) return checkoutToCart(checkoutLineItemsAdd)
}, },
useHook() { useHook: ({ fetch }) => () => {
const { mutate } = useCart() const { mutate } = useCart()
return async function addItem({ input, fetch }) {
const data = await fetch({ input }) return useCallback(
await mutate(data, false) async function addItem(input) {
return data const data = await fetch({ input })
} await mutate(data, false)
return data
},
[fetch, mutate]
)
}, },
} }

@ -7,14 +7,13 @@ import useCommerceCart, {
} from '@commerce/cart/use-cart' } from '@commerce/cart/use-cart'
import { Cart } from '@commerce/types' import { Cart } from '@commerce/types'
import { HookHandler } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import { checkoutCreate, checkoutToCart } from './utils'
import fetcher from './utils/fetcher' import getCheckoutQuery from '../utils/queries/get-checkout-query'
import getCheckoutQuery from '@framework/utils/queries/get-checkout-query'
export default useCommerceCart as UseCart<ShopifyProvider> export default useCommerceCart as UseCart<ShopifyProvider>
export const handler: HookHandler< export const handler: SWRHook<
Cart | null, Cart | null,
{}, {},
FetchCartInput, FetchCartInput,
@ -23,10 +22,27 @@ export const handler: HookHandler<
fetchOptions: { fetchOptions: {
query: getCheckoutQuery, query: getCheckoutQuery,
}, },
fetcher, async fetcher({ input: { cartId: checkoutId }, options, fetch }) {
useHook({ input, useData }) { let checkout
if (checkoutId) {
const data = await fetch({
...options,
variables: {
checkoutId,
},
})
checkout = data.node
}
if (checkout?.completedAt || !checkoutId) {
checkout = await checkoutCreate(fetch)
}
return checkoutToCart({ checkout })
},
useHook: ({ useData }) => (input) => {
const response = useData({ const response = useData({
swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
}) })
return useMemo( return useMemo(
() => () =>

@ -1,48 +1,61 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { HookFetcher } from '@commerce/utils/types'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors' import { ValidationError } from '@commerce/utils/errors'
import useCartRemoveItem, {
RemoveItemInput as UseRemoveItemInput, import useRemoveItem, {
RemoveItemInput as RemoveItemInputBase,
UseRemoveItem,
} from '@commerce/cart/use-remove-item' } from '@commerce/cart/use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
import { checkoutLineItemRemoveMutation, getCheckoutId } from '@framework/utils'
import type { Cart, LineItem, RemoveCartItemBody } from '@commerce/types'
import { checkoutLineItemRemoveMutation } from '@framework/utils/mutations'
import getCheckoutId from '@framework/utils/get-checkout-id'
import { checkoutToCart } from './utils' import { checkoutToCart } from './utils'
import { Cart, LineItem } from '@framework/types'
const defaultOpts = { import {
query: checkoutLineItemRemoveMutation, Mutation,
} MutationCheckoutLineItemsRemoveArgs,
} from '@framework/schema'
import { RemoveCartItemBody } from '@commerce/types'
export type RemoveItemFn<T = any> = T extends LineItem export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemInput<T>) => Promise<Cart | null> ? (input?: RemoveItemInput<T>) => Promise<Cart | null>
: (input: RemoveItemInput<T>) => Promise<Cart | null> : (input: RemoveItemInput<T>) => Promise<Cart | null>
export type RemoveItemInput<T = any> = T extends LineItem export type RemoveItemInput<T = any> = T extends LineItem
? Partial<UseRemoveItemInput> ? Partial<RemoveItemInputBase>
: UseRemoveItemInput : RemoveItemInputBase
export const fetcher: HookFetcher<Cart | null, any> = async ( export default useRemoveItem as UseRemoveItem<typeof handler>
options,
{ itemId, checkoutId },
fetch
) => {
const data = await fetch<any>({
...defaultOpts,
...options,
variables: { lineItemIds: [itemId], checkoutId },
})
return checkoutToCart(data.checkoutLineItemsRemove)
}
export function extendHook(customFetcher: typeof fetcher) { export const handler = {
const useRemoveItem = <T extends LineItem | undefined = undefined>( fetchOptions: {
item?: T query: checkoutLineItemRemoveMutation,
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveCartItemBody>) {
const data = await fetch<Mutation, MutationCheckoutLineItemsRemoveArgs>({
...options,
variables: { checkoutId: getCheckoutId(), lineItemIds: [itemId] },
})
return checkoutToCart(data.checkoutLineItemsRemove)
},
useHook: ({
fetch,
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => <
T extends LineItem | undefined = undefined
>(
ctx: { item?: T } = {}
) => { ) => {
const { mutate, data: cart } = useCart() const { item } = ctx
const fn = useCartRemoveItem<Cart | null, any>(defaultOpts, customFetcher) const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => { const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id const itemId = input?.id ?? item?.id
@ -52,21 +65,11 @@ export function extendHook(customFetcher: typeof fetcher) {
}) })
} }
const data = await fn({ const data = await fetch({ input: { itemId } })
checkoutId: getCheckoutId(cart?.id),
itemId,
})
await mutate(data, false) await mutate(data, false)
return data return data
} }
return useCallback(removeItem as RemoveItemFn<T>, [fn, mutate]) return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
} },
useRemoveItem.extend = extendHook
return useRemoveItem
} }
export default extendHook(fetcher)

@ -1,85 +1,110 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import type { HookFetcher } from '@commerce/utils/types' import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors' import { ValidationError } from '@commerce/utils/errors'
import useCartUpdateItem, { import useUpdateItem, {
UpdateItemInput as UseUpdateItemInput, UpdateItemInput as UpdateItemInputBase,
UseUpdateItem,
} from '@commerce/cart/use-update-item' } from '@commerce/cart/use-update-item'
import { fetcher as removeFetcher } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item'
import type { Cart, LineItem, UpdateCartItemBody } from '@commerce/types' import type { Cart, LineItem, UpdateCartItemBody } from '../types'
import { checkoutToCart } from './utils' import { checkoutToCart } from './utils'
import checkoutLineItemUpdateMutation from '@framework/utils/mutations/checkout-line-item-update' import { getCheckoutId, checkoutLineItemUpdateMutation } from '../utils'
import getCheckoutId from '@framework/utils/get-checkout-id' import {
Mutation,
const defaultOpts = { MutationCheckoutLineItemsUpdateArgs,
query: checkoutLineItemUpdateMutation, } from '@framework/schema'
}
export type UpdateItemInput<T = any> = T extends LineItem export type UpdateItemInput<T = any> = T extends LineItem
? Partial<UseUpdateItemInput<LineItem>> ? Partial<UpdateItemInputBase<LineItem>>
: UseUpdateItemInput<LineItem> : UpdateItemInputBase<LineItem>
export const fetcher: HookFetcher<Cart | null, any> = async ( export default useUpdateItem as UseUpdateItem<typeof handler>
options,
{ item, checkoutId }, export const handler = {
fetch fetchOptions: {
) => { query: checkoutLineItemUpdateMutation,
if (Number.isInteger(item.quantity)) { },
// Also allow the update hook to remove an item if the quantity is lower than 1 async fetcher({
if (item.quantity! < 1) { input: { itemId, item },
return removeFetcher(null, { itemId: item.id, checkoutId }, fetch) options,
fetch,
}: HookFetcherContext<UpdateCartItemBody>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
} }
} else if (item.quantity) { const { checkoutLineItemsUpdate } = await fetch<
throw new ValidationError({ Mutation,
message: 'The item quantity has to be a valid integer', MutationCheckoutLineItemsUpdateArgs
>({
...options,
variables: {
checkoutId: getCheckoutId(),
lineItems: [
{
id: itemId,
quantity: item.quantity,
},
],
},
}) })
}
const data = await fetch<any, any>({
...defaultOpts,
...options,
variables: { checkoutId, lineItems: [item] },
})
return checkoutToCart(data.checkoutLineItemsUpdate) return checkoutToCart(checkoutLineItemsUpdate)
} },
useHook: ({
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { fetch,
const useUpdateItem = <T extends LineItem | undefined = undefined>( }: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
item?: T T extends LineItem | undefined = undefined
>(
ctx: {
item?: T
wait?: number
} = {}
) => { ) => {
const { mutate, data: cart } = useCart() const { item } = ctx
const fn = useCartUpdateItem<Cart | null, any>(defaultOpts, customFetcher) const { mutate } = useCart() as any
return useCallback( return useCallback(
debounce(async (input: UpdateItemInput<T>) => { debounce(async (input: UpdateItemInput<T>) => {
const itemId = input.id ?? item?.id const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId const variantId = input.productId ?? item?.variantId
if (!itemId || !productId || !variantId) {
if (!itemId || !variantId) {
throw new ValidationError({ throw new ValidationError({
message: 'Invalid input used for this operation', message: 'Invalid input used for this operation',
}) })
} }
const data = await fn({ const data = await fetch({
item: { id: itemId, variantId, quantity: input.quantity }, input: {
checkoutId: getCheckoutId(cart?.id), item: {
productId,
variantId,
quantity: input.quantity,
},
itemId,
},
}) })
await mutate(data, false) await mutate(data, false)
return data return data
}, cfg?.wait ?? 500), }, ctx.wait ?? 500),
[fn, mutate] [fetch, mutate]
) )
} },
useUpdateItem.extend = extendHook
return useUpdateItem
} }
export default extendHook(fetcher)

@ -11,7 +11,7 @@ export const checkoutCreate = async (fetch: any) => {
query: checkoutCreateMutation, query: checkoutCreateMutation,
}) })
const checkout = data?.checkoutCreate?.checkout const checkout = data.checkoutCreate?.checkout
const checkoutId = checkout?.id const checkoutId = checkout?.id
if (checkoutId) { if (checkoutId) {

@ -3,6 +3,7 @@ import { CommerceError, ValidationError } from '@commerce/utils/errors'
import { import {
CheckoutLineItemsAddPayload, CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload, CheckoutLineItemsUpdatePayload,
Maybe, Maybe,
} from '@framework/schema' } from '@framework/schema'
@ -11,9 +12,10 @@ import { normalizeCart } from '@framework/utils'
export type CheckoutPayload = export type CheckoutPayload =
| CheckoutLineItemsAddPayload | CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload | CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => { const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
if (!checkoutPayload || !checkoutPayload?.checkout) { if (!checkoutPayload) {
throw new CommerceError({ throw new CommerceError({
message: 'Invalid response from Shopify', message: 'Invalid response from Shopify',
}) })
@ -28,6 +30,12 @@ const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
}) })
} }
if (!checkout) {
throw new CommerceError({
message: 'Invalid response from Shopify',
})
}
return normalizeCart(checkout) return normalizeCart(checkout)
} }

@ -17,7 +17,7 @@ const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
checkoutId, checkoutId,
}, },
}) })
checkout = data?.node checkout = data.node
} }
if (checkout?.completedAt || !checkoutId) { if (checkout?.completedAt || !checkoutId) {

@ -1,3 +1,2 @@
export { default as checkoutToCart } from './checkout-to-cart' export { default as checkoutToCart } from './checkout-to-cart'
export { default as checkoutCreate } from './checkout-create' export { default as checkoutCreate } from './checkout-create'
export { default as fetcher } from './fetcher'

@ -19,7 +19,7 @@ const getAllPages = async (options?: {
config = getConfig(config) config = getConfig(config)
const { data } = await config.fetch(getAllPagesQuery, { variables }) const { data } = await config.fetch(getAllPagesQuery, { variables })
const edges = data?.pages?.edges const edges = data.pages?.edges
const pages = edges?.map(({ node }: PageEdge) => ({ const pages = edges?.map(({ node }: PageEdge) => ({
...node, ...node,

@ -24,7 +24,7 @@ const getPage = async (options: {
variables, variables,
}) })
const page: Page = data?.pageByHandle const page: Page = data.pageByHandle
return { return {
page: page page: page

@ -0,0 +1,5 @@
{
"features": {
"wishlist": false
}
}

@ -18,7 +18,7 @@ async function getCustomerId({
}, },
}) })
return data?.customer?.id return data.customer?.id
} }
export default getCustomerId export default getCustomerId

@ -1,24 +1,26 @@
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import { Customer } from '@commerce/types' import { Customer } from '@commerce/types'
import { HookHandler } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import { getCustomerQuery } from '@framework/utils' import { getCustomerQuery, getCustomerToken } from '../utils'
import type { ShopifyProvider } from '..' import type { ShopifyProvider } from '..'
export default useCustomer as UseCustomer<ShopifyProvider> export default useCustomer as UseCustomer<ShopifyProvider>
export const handler: SWRHook<Customer | null> = {
export const handler: HookHandler<Customer | null> = {
fetchOptions: { fetchOptions: {
query: getCustomerQuery, query: getCustomerQuery,
}, },
async fetcher({ options, fetch }) { async fetcher({ options, fetch }) {
const data = await fetch<any | null>(options) const data = await fetch<any | null>({
return data?.customer ?? null ...options,
variables: { customerAccessToken: getCustomerToken() },
})
return data.customer ?? null
}, },
useHook({ input, useData }) { useHook: ({ useData }) => (input) => {
return useData({ return useData({
swrOptions: { swrOptions: {
revalidateOnFocus: false, revalidateOnFocus: false,
...input.swrOptions, ...input?.swrOptions,
}, },
}) })
}, },

@ -11,7 +11,7 @@ const getAllCollections = async (options?: {
config = getConfig(config) config = getConfig(config)
const { data } = await config.fetch(getAllCollectionsQuery, { variables }) const { data } = await config.fetch(getAllCollectionsQuery, { variables })
const edges = data?.collections?.edges ?? [] const edges = data.collections?.edges ?? []
const categories = edges.map( const categories = edges.map(
({ node: { id: entityId, title: name, handle } }: CollectionEdge) => ({ ({ node: { id: entityId, title: name, handle } }: CollectionEdge) => ({

@ -28,7 +28,7 @@ const getAllProducts = async (options: {
{ variables } { variables }
) )
const products = data?.products?.edges?.map(({ node: p }: ProductEdge) => const products = data.products?.edges?.map(({ node: p }: ProductEdge) =>
normalizeProduct(p) normalizeProduct(p)
) )

@ -27,7 +27,7 @@ const getProduct = async (options: {
variables, variables,
}) })
const product = data?.productByHandle const { productByHandle: product } = data
return { return {
product: product ? normalizeProduct(product) : null, product: product ? normalizeProduct(product) : null,

@ -1,6 +1,6 @@
import useSearch, { UseSearch } from '@commerce/products/use-search' import { SWRHook } from '@commerce/utils/types'
import { SearchProductsData } from '@commerce/types' import useSearch, { UseSearch } from '@commerce/product/use-search'
import { HookHandler } from '@commerce/utils/types'
import { ProductEdge } from '@framework/schema' import { ProductEdge } from '@framework/schema'
import { import {
getAllProductsQuery, getAllProductsQuery,
@ -9,6 +9,8 @@ import {
} from '@framework/utils' } from '@framework/utils'
import type { ShopifyProvider } from '..' import type { ShopifyProvider } from '..'
import { Product } from '@commerce/types'
export default useSearch as UseSearch<ShopifyProvider> export default useSearch as UseSearch<ShopifyProvider>
export type SearchProductsInput = { export type SearchProductsInput = {
@ -18,7 +20,11 @@ export type SearchProductsInput = {
sort?: string sort?: string
} }
export const handler: HookHandler< export type SearchProductsData = {
products: Product[]
found: boolean
}
export const handler: SWRHook<
SearchProductsData, SearchProductsData,
SearchProductsInput, SearchProductsInput,
SearchProductsInput SearchProductsInput
@ -38,7 +44,7 @@ export const handler: HookHandler<
found: !!edges?.length, found: !!edges?.length,
} }
}, },
useHook({ input, useData }) { useHook: ({ useData }) => (input = {}) => {
return useData({ return useData({
input: [ input: [
['search', input.search], ['search', input.search],

@ -2,8 +2,16 @@ import { SHOPIFY_CHECKOUT_ID_COOKIE, STORE_DOMAIN } from './const'
import { handler as useCart } from './cart/use-cart' import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item' import { handler as useAddItem } from './cart/use-add-item'
import { handler as useSearch } from './product/use-search' import { handler as useUpdateItem } from './cart/use-update-item'
import { handler as useRemoveItem } from './cart/use-remove-item'
import { handler as useCustomer } from './customer/use-customer' import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup'
import fetcher from './fetcher' import fetcher from './fetcher'
export const shopifyProvider = { export const shopifyProvider = {
@ -11,9 +19,13 @@ export const shopifyProvider = {
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
storeDomain: STORE_DOMAIN, storeDomain: STORE_DOMAIN,
fetcher, fetcher,
cart: { useCart, useAddItem }, cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer }, customer: { useCustomer },
products: { useSearch }, products: { useSearch },
auth: { useLogin, useLogout, useSignup },
features: {
wishlist: false,
},
} }
export type ShopifyProvider = typeof shopifyProvider export type ShopifyProvider = typeof shopifyProvider

@ -16,7 +16,7 @@ const getCategories = async (config: ShopifyConfig): Promise<Category[]> => {
}) })
return ( return (
data?.collections?.edges?.map( data.collections?.edges?.map(
({ node: { title: name, handle } }: CollectionEdge) => ({ ({ node: { title: name, handle } }: CollectionEdge) => ({
entityId: handle, entityId: handle,
name, name,

@ -17,7 +17,7 @@ const getErrorMessage = ({
} }
const handleLogin = (data: any) => { const handleLogin = (data: any) => {
const response = data?.customerAccessTokenCreate const response = data.customerAccessTokenCreate
const errors = response?.customerUserErrors const errors = response?.customerUserErrors
if (errors && errors.length) { if (errors && errors.length) {

@ -4,7 +4,6 @@ export { default as getSortVariables } from './get-sort-variables'
export { default as getVendors } from './get-vendors' export { default as getVendors } from './get-vendors'
export { default as getCategories } from './get-categories' export { default as getCategories } from './get-categories'
export { default as getCheckoutId } from './get-checkout-id' export { default as getCheckoutId } from './get-checkout-id'
export * from './queries' export * from './queries'
export * from './mutations' export * from './mutations'
export * from './normalize' export * from './normalize'

@ -1,10 +1,7 @@
export { default as createCustomerMutation } from './customer-create'
export { default as checkoutCreateMutation } from './checkout-create'
export { default as checkoutLineItemAddMutation } from './checkout-line-item-add'
export { default as checkoutLineItemUpdateMutation } from './checkout-create'
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
export { default as customerCreateMutation } from './customer-create' export { default as customerCreateMutation } from './customer-create'
export { default as checkoutCreateMutation } from './checkout-create'
export { default as checkoutLineItemAddMutation } from './checkout-line-item-add'
export { default as checkoutLineItemUpdateMutation } from './checkout-line-item-update'
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create' export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete' export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'

@ -1,4 +1,4 @@
export const checkoutDetailsFragment = /* GraphQL */ ` export const checkoutDetailsFragment = `
id id
webUrl webUrl
subtotalPrice subtotalPrice

@ -7,4 +7,4 @@ export { default as getCollectionProductsQuery } from './get-collection-products
export { default as getCheckoutQuery } from './get-checkout-query' export { default as getCheckoutQuery } from './get-checkout-query'
export { default as getAllPagesQuery } from './get-all-pages-query' export { default as getAllPagesQuery } from './get-all-pages-query'
export { default as getPageQuery } from './get-page-query' export { default as getPageQuery } from './get-page-query'
export { default as getCustomerQuery } from './get-checkout-query' export { default as getCustomerQuery } from './get-customer-query'

@ -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

@ -34,7 +34,4 @@ module.exports = {
}, },
] ]
}, },
typescript: {
ignoreBuildErrors: true,
},
} }

@ -1,3 +1,3 @@
import customersApi from '@framework/api/customer' import customersApi from '@framework/api/customers'
export default customersApi() export default customersApi()

@ -36,7 +36,7 @@ export async function getStaticProps({
wishlist: isWishlistEnabled, wishlist: isWishlistEnabled,
}, },
}, },
revalidate: 1440, revalidate: 14400,
} }
} }

@ -106,7 +106,7 @@ export default function Search({
<button <button
type="button" type="button"
onClick={(e) => handleClick(e, 'categories')} 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" id="options-menu"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true" aria-expanded="true"
@ -205,7 +205,7 @@ export default function Search({
<button <button
type="button" type="button"
onClick={(e) => handleClick(e, 'brands')} 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" id="options-menu"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true" aria-expanded="true"
@ -383,7 +383,7 @@ export default function Search({
<button <button
type="button" type="button"
onClick={(e) => handleClick(e, 'sort')} 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" id="options-menu"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true" aria-expanded="true"

@ -51,7 +51,7 @@ module.exports = {
secondary: 'var(--text-secondary)', secondary: 'var(--text-secondary)',
}, },
boxShadow: { boxShadow: {
'outline-2': '0 0 0 2px var(--accents-2)', 'outline-normal': '0 0 0 2px var(--accents-2)',
magical: 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', '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')],
} }

@ -22,8 +22,8 @@
"@utils/*": ["utils/*"], "@utils/*": ["utils/*"],
"@commerce/*": ["framework/commerce/*"], "@commerce/*": ["framework/commerce/*"],
"@commerce": ["framework/commerce"], "@commerce": ["framework/commerce"],
"@framework/*": ["framework/shopify/*"], "@framework/*": ["framework/bigcommerce/*"],
"@framework": ["framework/shopify"] "@framework": ["framework/bigcommerce"]
} }
}, },
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],