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

View File

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

View File

@ -1,3 +1,3 @@
.fit {
min-height: calc(100vh - 88px);
}
}

View File

@ -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> */}

View File

@ -27,7 +27,7 @@ const HomeAllProductsGrid: FC<Props> = ({
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getCategoryPath('')}>
<a>All Collections</a>
<a>All Categories</a>
</Link>
</li>
{categories.map((cat: any) => (
@ -38,6 +38,20 @@ const HomeAllProductsGrid: FC<Props> = ({
</li>
))}
</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 className="flex-1">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import useCart from './use-cart'
import { ShopifyProvider } from '..'
import { Cart, AddCartItemBody, CartItemBody } from '../types'
import { Cart, CartItemBody } from '../types'
import { checkoutLineItemAddMutation, getCheckoutId } 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: {
query: checkoutLineItemAddMutation,
},
async fetcher({ input, options, fetch }) {
const item = input?.item ?? input
async fetcher({ input: item, options, fetch }) {
if (
item.quantity &&
(!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,
variables: {
checkoutId: getCheckoutId(),
lineItems: [
{
variantId: item.variantId,
quantity: item.quantity ?? 1,
},
],
checkoutId: getCheckoutId(),
},
})
return checkoutToCart(checkoutLineItemsAdd)
},
useHook() {
useHook: ({ fetch }) => () => {
const { mutate } = useCart()
return async function addItem({ input, fetch }) {
const data = await fetch({ input })
await mutate(data, false)
return data
}
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,16 @@ import { SHOPIFY_CHECKOUT_ID_COOKIE, STORE_DOMAIN } from './const'
import { handler as useCart } from './cart/use-cart'
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 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'
export const shopifyProvider = {
@ -11,9 +19,13 @@ export const shopifyProvider = {
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
storeDomain: STORE_DOMAIN,
fetcher,
cart: { useCart, useAddItem },
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
features: {
wishlist: false,
},
}
export type ShopifyProvider = typeof shopifyProvider

View File

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

View File

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

View File

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

View File

@ -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 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 customerAccessTokenDeleteMutation } from './customer-access-token-delete'

View File

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

View File

@ -7,4 +7,4 @@ export { default as getCollectionProductsQuery } from './get-collection-products
export { default as getCheckoutQuery } from './get-checkout-query'
export { default as getAllPagesQuery } from './get-all-pages-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'

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

View File

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

View File

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

View File

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

View File

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

View File

@ -106,7 +106,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"
@ -205,7 +205,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"
@ -383,7 +383,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"

View File

@ -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')],
}

View File

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

View File

@ -7257,4 +7257,4 @@ yn@3.1.1:
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==