Update queries & structure

This commit is contained in:
cond0r 2022-07-14 09:51:51 +03:00
parent 0b41b4e618
commit d4858910e4
43 changed files with 583 additions and 609 deletions

View File

@ -6,7 +6,16 @@ import { GetSiteInfoQueryVariables } from '../../../schema'
import type { ShopifyConfig, Provider } from '..' import type { ShopifyConfig, Provider } from '..'
import { GetSiteInfoOperation } from '../../types/site' import { GetSiteInfoOperation } from '../../types/site'
import { getCategories, getBrands, getSiteInfoQuery } from '../../utils' import { getBrands } from '../utils/get-brands'
import { getCategories } from '../utils/get-categories'
export const getSiteInfoQuery = /* GraphQL */ `
query getSiteInfo {
shop {
name
}
}
`
export default function getSiteInfoOperation({ export default function getSiteInfoOperation({
commerce, commerce,
@ -37,20 +46,6 @@ export default function getSiteInfoOperation({
const categoriesPromise = getCategories(cfg) const categoriesPromise = getCategories(cfg)
const brandsPromise = getBrands(cfg) const brandsPromise = getBrands(cfg)
/*
const { fetch, locale } = cfg
const { data } = await fetch<GetSiteInfoQuery, GetSiteInfoQueryVariables>(
query,
{ variables },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
*/
return { return {
categories: await categoriesPromise, categories: await categoriesPromise,

View File

@ -3,6 +3,7 @@ import fetch from './fetch'
import { API_URL, API_TOKEN } from '../../const' import { API_URL, API_TOKEN } from '../../const'
import { getError } from '../../utils/handle-fetch-response' import { getError } from '../../utils/handle-fetch-response'
import { CommerceError } from '@vercel/commerce/utils/errors'
const fetchGraphqlApi: GraphQLFetcher = async ( const fetchGraphqlApi: GraphQLFetcher = async (
query: string, query: string,
@ -32,14 +33,20 @@ const fetchGraphqlApi: GraphQLFetcher = async (
return { data, res } return { data, res }
} catch (err) { } catch (err) {
throw getError( // if the error is a CommerceError, we can use it as-is
[ if (err instanceof CommerceError) {
{ throw err
message: `${err} \n Most likely related to an unexpected output. e.g the store might be protected with password or not available.`, } else {
}, // otherwise, we'll wrap unknown errors in a CommerceError
], throw getError(
500 [
) {
message: `${err} \n Most likely related to an unexpected output. e.g the store might be protected with password or not available.`,
},
],
500
)
}
} }
} }
export default fetchGraphqlApi export default fetchGraphqlApi

View File

@ -1,11 +1,11 @@
import { ShopifyConfig } from '../api' import { ShopifyConfig } from '..'
import { import {
GetAllProductVendorsQuery, GetAllProductVendorsQuery,
GetAllProductVendorsQueryVariables, GetAllProductVendorsQueryVariables,
} from '../../schema' } from '../../../schema'
import { getAllProductVendors } from './queries' import { getAllProductVendors } from '../../utils/queries'
export type Brand = { export type Brand = {
entityId: string entityId: string

View File

@ -1,8 +1,7 @@
import type { Category } from '../types/site' import { ShopifyConfig } from '..'
import { ShopifyConfig } from '../api' import type { Category } from '../../types/site'
import { CollectionEdge } from '../../schema' import { CollectionEdge } from '../../../schema'
import { normalizeCategory } from './normalize' import { normalizeCategory, getSiteCollectionsQuery } from '../../utils'
import { getSiteCollectionsQuery } from './queries/get-all-collections-query'
export const getCategories = async ({ export const getCategories = async ({
fetch, fetch,

View File

@ -1,15 +1,13 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import useCommerceCart, { UseCart } from '@vercel/commerce/cart/use-cart' import useCommerceCart, { UseCart } from '@vercel/commerce/cart/use-cart'
import { SWRHook } from '@vercel/commerce/utils/types' import { SWRHook } from '@vercel/commerce/utils/types'
import { getCartQuery, normalizeCart } from '../utils'
import { GetCartHook } from '../types/cart'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { GetCartQueryVariables, QueryRoot } from '../../schema' import { getCartQuery, normalizeCart, setCartUrlCookie } from '../utils'
import { GetCartHook } from '../types/cart'
import { GetCartQueryVariables, QueryRoot } from '../../schema'
import { SHOPIFY_CART_ID_COOKIE } from '../const' import { SHOPIFY_CART_ID_COOKIE } from '../const'
import { setCartUrlCookie } from '../utils/set-cart-url-cookie'
export default useCommerceCart as UseCart<typeof handler> export default useCommerceCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = { export const handler: SWRHook<GetCartHook> = {

View File

@ -10,6 +10,18 @@ import useRemoveItem, {
import type { Cart, LineItem, RemoveItemHook } from '../types/cart' import type { Cart, LineItem, RemoveItemHook } from '../types/cart'
import useCart from './use-cart' import useCart from './use-cart'
import {
getCartId,
normalizeCart,
throwUserErrors,
cartLinesRemoveMutation,
} from '../utils'
import {
CartLinesRemoveMutation,
CartLinesRemoveMutationVariables,
} from '../../schema'
export type RemoveItemFn<T = any> = T extends LineItem export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined> ? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null> : (input: RemoveItemActionInput<T>) => Promise<Cart | null>
@ -20,17 +32,6 @@ export type RemoveItemActionInput<T = any> = T extends LineItem
export default useRemoveItem as UseRemoveItem<typeof handler> export default useRemoveItem as UseRemoveItem<typeof handler>
import {
cartLinesRemoveMutation,
getCartId,
normalizeCart,
throwUserErrors,
} from '../utils'
import {
CartLinesRemoveMutation,
CartLinesRemoveMutationVariables,
} from '../../schema'
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
query: cartLinesRemoveMutation, query: cartLinesRemoveMutation,

View File

@ -11,9 +11,12 @@ import useUpdateItem, {
import useCart from './use-cart' import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item' import { handler as removeItemHandler } from './use-remove-item'
import { getCartId, normalizeCart, cartLinesUpdateMutation } from '../utils'
import type { UpdateItemHook, LineItem } from '../types/cart' import type { UpdateItemHook, LineItem } from '../types/cart'
import { getCartId, cartLinesUpdateMutation, normalizeCart } from '../utils'
import { import type {
CartLinesUpdateMutation, CartLinesUpdateMutation,
CartLinesUpdateMutationVariables, CartLinesUpdateMutationVariables,
} from '../../schema' } from '../../schema'

View File

@ -1,13 +1,16 @@
export const API_VERSION = '2022-04'
export const SHOPIFY_CART_ID_COOKIE = 'shopify_cartId' export const SHOPIFY_CART_ID_COOKIE = 'shopify_cartId'
export const SHOPIFY_CART_URL_COOKIE = 'shopify_cartUrl' export const SHOPIFY_CART_URL_COOKIE = 'shopify_cartUrl'
export const SHOPIFY_CUSTOMER_TOKEN_COOKIE = 'shopify_customerToken' export const SHOPIFY_CUSTOMER_TOKEN_COOKIE = 'shopify_customerToken'
export const STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN export const STORE_DOMAIN =
process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN?.replace(/(^\w+:|^)\/\//, '')
export const SHOPIFY_COOKIE_EXPIRE = 30 export const SHOPIFY_COOKIE_EXPIRE = 30
export const API_URL = `https://${STORE_DOMAIN}/api/2022-04/graphql.json` export const API_URL = `https://${STORE_DOMAIN}/api/${API_VERSION}/graphql.json`
export const API_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN export const API_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN

View File

@ -1,6 +1,6 @@
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { SHOPIFY_CART_ID_COOKIE, SHOPIFY_COOKIE_EXPIRE } from '../const' import { SHOPIFY_CART_ID_COOKIE, SHOPIFY_COOKIE_EXPIRE } from '../const'
import { cartCreateMutation } from './mutations/cart-create' import { cartCreateMutation } from './mutations/cart-mutations'
import { import {
CartCreateMutation, CartCreateMutation,
@ -9,9 +9,9 @@ import {
CartLineInput, CartLineInput,
} from '../../schema' } from '../../schema'
import { setCartUrlCookie } from './helpers'
import { throwUserErrors } from './throw-user-errors' import { throwUserErrors } from './throw-user-errors'
import { setCartUrlCookie } from './set-cart-url-cookie' import type { FetcherOptions } from '@vercel/commerce/utils/types'
import { FetcherOptions } from '@vercel/commerce/utils/types'
export const cartCreate = async ( export const cartCreate = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>, fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
@ -44,5 +44,3 @@ export const cartCreate = async (
return cart return cart
} }
export default cartCreate

View File

@ -1,21 +0,0 @@
import Cookies, { CookieAttributes } from 'js-cookie'
import { SHOPIFY_COOKIE_EXPIRE, SHOPIFY_CUSTOMER_TOKEN_COOKIE } from '../const'
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
export const setCustomerToken = (
token: string | null,
options?: CookieAttributes
) => {
if (!token) {
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
} else {
Cookies.set(
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
token,
options ?? {
expires: SHOPIFY_COOKIE_EXPIRE,
}
)
}
}

View File

@ -0,0 +1,82 @@
export const cartDetailsFragment = /* GraphQL */ `
fragment cartDetails on Cart {
id
checkoutUrl
createdAt
updatedAt
lines(first: 10) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
id
sku
title
selectedOptions {
name
value
}
image {
url
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
product {
title
handle
}
}
}
}
}
}
attributes {
key
value
}
buyerIdentity {
email
customer {
id
}
}
estimatedCost {
totalAmount {
amount
currencyCode
}
subtotalAmount {
amount
currencyCode
}
totalTaxAmount {
amount
currencyCode
}
totalDutyAmount {
amount
currencyCode
}
}
}
`
export const userErrorsFragment = /* GraphQL */ `
fragment userErrors on UserError {
code
field
message
}
`

View File

@ -0,0 +1,15 @@
export const customerUserErrorsFragment = /* GraphQL */ `
fragment customerUserErrors on CustomerUserErrors {
code
field
message
}
`
export const customerAccessTokenFragment = /* GraphQL */ `
fragment customerAccessToken on CustomerAccessToken {
code
field
message
}
`

View File

@ -0,0 +1,36 @@
export const mediaFragment = /* GraphQL */ `
fragment Media on Media {
mediaContentType
alt
previewImage {
url
}
... on MediaImage {
id
image {
url
width
height
}
}
# ... on Video {
# id
# sources {
# mimeType
# url
# }
# }
# ... on Model3d {
# id
# sources {
# mimeType
# url
# }
# }
# ... on ExternalVideo {
# id
# embedUrl
# host
# }
}
`

View File

@ -0,0 +1,36 @@
export const productCardFragment = /* GraphQL */ `
fragment productCard on Product {
id
handle
availableForSale
title
productType
vendor
variants(first: 1) {
nodes {
id
title
requiresShipping
availableForSale
selectedOptions {
name
value
}
image {
url
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
}
}
}
`

View File

@ -1,6 +0,0 @@
import Cookies from 'js-cookie'
import { SHOPIFY_CART_ID_COOKIE } from '../const'
export const getCartId = (id?: string) => {
return id || Cookies.get(SHOPIFY_CART_ID_COOKIE)
}

View File

@ -1,29 +0,0 @@
import { getSortVariables } from './get-sort-variables'
import { SearchProductsBody } from '../types/product'
export const getSearchVariables = ({
brandId,
search,
categoryId,
sort,
locale,
}: SearchProductsBody) => {
let query = ''
if (search) {
query += `product_type:${search} OR title:${search} OR tag:${search} `
}
if (brandId) {
query += `${search ? 'AND ' : ''}vendor:${brandId}`
}
return {
categoryId,
query,
...getSortVariables(sort, !!categoryId),
...(locale && {
locale,
}),
}
}

View File

@ -1,33 +0,0 @@
export const getSortVariables = (
sort?: string,
isCategory: boolean = false
) => {
let output = {}
switch (sort) {
case 'price-asc':
output = {
sortKey: 'PRICE',
reverse: false,
}
break
case 'price-desc':
output = {
sortKey: 'PRICE',
reverse: true,
}
break
case 'trending-desc':
output = {
sortKey: 'BEST_SELLING',
reverse: false,
}
break
case 'latest-desc':
output = {
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
reverse: true,
}
break
}
return output
}

View File

@ -1,12 +1,17 @@
import { FetcherError } from '@vercel/commerce/utils/errors' import { FetcherError } from '@vercel/commerce/utils/errors'
export function getError(errors: any[] | null, status: number) { export function getError(err: any[] | string | null, status: number) {
errors = errors ?? [{ message: 'Failed to fetch Shopify API' }] console.log(JSON.stringify(err, null, 2))
const errors = Array.isArray(err)
? err
: [{ message: err || 'Failed to fetch Shopify API' }]
return new FetcherError({ errors, status }) return new FetcherError({ errors, status })
} }
export async function getAsyncError(res: Response) { export async function getAsyncError(res: Response) {
const data = await res.json() const data = await res.json()
return getError(data.errors, res.status) return getError(data.errors, res.status)
} }

View File

@ -1,6 +1,6 @@
import { FetcherOptions } from '@vercel/commerce/utils/types' import { FetcherOptions } from '@vercel/commerce/utils/types'
import { CustomerAccessTokenCreateInput } from '../../schema' import { CustomerAccessTokenCreateInput } from '../../schema'
import { setCustomerToken } from './customer-token' import { setCustomerToken } from './helpers'
import { customerAccessTokenCreateMutation } from './mutations' import { customerAccessTokenCreateMutation } from './mutations'
import { throwUserErrors } from './throw-user-errors' import { throwUserErrors } from './throw-user-errors'

View File

@ -0,0 +1,104 @@
import Cookies, { CookieAttributes } from 'js-cookie'
import { SearchProductsBody } from '../types/product'
import {
SHOPIFY_CART_URL_COOKIE,
SHOPIFY_COOKIE_EXPIRE,
SHOPIFY_CART_ID_COOKIE,
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
} from '../const'
export const setCartUrlCookie = (cartUrl: string) => {
if (cartUrl) {
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
if (oldCookie !== cartUrl) {
Cookies.set(SHOPIFY_CART_URL_COOKIE, cartUrl, {
expires: SHOPIFY_COOKIE_EXPIRE,
})
}
}
}
export const getCartId = (id?: string) => {
return id || Cookies.get(SHOPIFY_CART_ID_COOKIE)
}
export const getSortVariables = (
sort?: string,
isCategory: boolean = false
) => {
let output = {}
switch (sort) {
case 'price-asc':
output = {
sortKey: 'PRICE',
reverse: false,
}
break
case 'price-desc':
output = {
sortKey: 'PRICE',
reverse: true,
}
break
case 'trending-desc':
output = {
sortKey: 'BEST_SELLING',
reverse: false,
}
break
case 'latest-desc':
output = {
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
reverse: true,
}
break
}
return output
}
export const getSearchVariables = ({
brandId,
search,
categoryId,
sort,
locale,
}: SearchProductsBody) => {
let query = ''
if (search) {
query += `product_type:${search} OR title:${search} OR tag:${search} `
}
if (brandId) {
query += `${search ? 'AND ' : ''}vendor:${brandId}`
}
return {
categoryId,
query,
...getSortVariables(sort, !!categoryId),
...(locale && {
locale,
}),
}
}
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
export const setCustomerToken = (
token: string | null,
options?: CookieAttributes
) => {
if (!token) {
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
} else {
Cookies.set(
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
token,
options ?? {
expires: SHOPIFY_COOKIE_EXPIRE,
}
)
}
}

View File

@ -1,15 +1,10 @@
export { throwUserErrors } from './throw-user-errors'
export { handleFetchResponse } from './handle-fetch-response' export { handleFetchResponse } from './handle-fetch-response'
export { getSearchVariables } from './get-search-variables'
export { getSortVariables } from './get-sort-variables'
export { getBrands } from './get-brands'
export { getCategories } from './get-categories'
export { getCartId } from './get-cart-id'
export { cartCreate } from './cart-create' export { cartCreate } from './cart-create'
export { handleLogin, handleAutomaticLogin } from './handle-login' export { handleLogin, handleAutomaticLogin } from './handle-login'
export { handleAccountActivation } from './handle-account-activation' export { handleAccountActivation } from './handle-account-activation'
export { throwUserErrors } from './throw-user-errors'
export * from './helpers'
export * from './queries' export * from './queries'
export * from './mutations' export * from './mutations'
export * from './normalize' export * from './normalize'
export * from './customer-token'

View File

@ -1,17 +0,0 @@
export const associateCustomerWithCheckoutMutation = /* GraphQl */ `
mutation associateCustomerWithCheckout($checkoutId: ID!, $customerAccessToken: String!) {
checkoutCustomerAssociateV2(checkoutId: $checkoutId, customerAccessToken: $customerAccessToken) {
checkout {
id
}
checkoutUserErrors {
code
field
message
}
customer {
id
}
}
}
`

View File

@ -1,17 +0,0 @@
import { cartDetailsFragment } from '../queries/get-cart-query'
export const cartCreateMutation = /* GraphQL */ `
mutation cartCreate($input: CartInput = {}) {
cartCreate(input: $input) {
cart {
...cartDetails
}
userErrors {
code
field
message
}
}
}
${cartDetailsFragment}
`

View File

@ -1,17 +0,0 @@
import { cartDetailsFragment } from '../queries/get-cart-query'
export const cartLinesAddMutation = /* GraphQL */ `
mutation cartLinesAdd($lines: [CartLineInput!]!, $cartId: ID!) {
cartLinesAdd(lines: $lines, cartId: $cartId) {
cart {
...cartDetails
}
userErrors {
code
field
message
}
}
}
${cartDetailsFragment}
`

View File

@ -1,17 +0,0 @@
import { cartDetailsFragment } from '../queries/get-cart-query'
export const cartLinesRemoveMutation = /* GraphQL */ `
mutation cartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
cart {
...cartDetails
}
userErrors {
code
field
message
}
}
}
${cartDetailsFragment}
`

View File

@ -1,17 +0,0 @@
import { cartDetailsFragment } from '../queries/get-cart-query'
export const cartLinesUpdateMutation = /* GraphQL */ `
mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
cartLinesUpdate(cartId: $cartId, lines: $lines) {
cart {
...cartDetails
}
userErrors {
code
field
message
}
}
}
${cartDetailsFragment}
`

View File

@ -0,0 +1,64 @@
import {
cartDetailsFragment,
userErrorsFragment,
} from '../fragments/cart-details-fragment'
export const cartCreateMutation = /* GraphQL */ `
mutation cartCreate($input: CartInput = {}) {
cartCreate(input: $input) {
cart {
...cartDetails
}
userErrors {
...userErrors
}
}
}
${cartDetailsFragment}
${userErrorsFragment}
`
export const cartLinesAddMutation = /* GraphQL */ `
mutation cartLinesAdd($lines: [CartLineInput!]!, $cartId: ID!) {
cartLinesAdd(lines: $lines, cartId: $cartId) {
cart {
...cartDetails
}
userErrors {
...userErrors
}
}
}
${cartDetailsFragment}
${userErrorsFragment}
`
export const cartLinesRemoveMutation = /* GraphQL */ `
mutation cartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
cart {
...cartDetails
}
userErrors {
...userErrors
}
}
}
${cartDetailsFragment}
${userErrorsFragment}
`
export const cartLinesUpdateMutation = /* GraphQL */ `
mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
cartLinesUpdate(cartId: $cartId, lines: $lines) {
cart {
...cartDetails
}
userErrors {
...userErrors
}
}
}
${cartDetailsFragment}
${userErrorsFragment}
`

View File

@ -1,15 +0,0 @@
export const customerAccessTokenCreateMutation = /* GraphQL */ `
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
customerAccessTokenCreate(input: $input) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`

View File

@ -1,12 +0,0 @@
export const customerAccessTokenDeleteMutation = /* GraphQL */ `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
userErrors {
field
message
}
}
}
`

View File

@ -1,18 +0,0 @@
export const customerActivateByUrlMutation = /* GraphQL */ `
mutation customerActivateByUrl($activationUrl: URL!, $password: String!) {
customerActivateByUrl(activationUrl: $activationUrl, password: $password) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`

View File

@ -1,18 +0,0 @@
export const customerActivateMutation = /* GraphQL */ `
mutation customerActivate($id: ID!, $input: CustomerActivateInput!) {
customerActivate(id: $id, input: $input) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`

View File

@ -1,14 +0,0 @@
export const customerCreateMutation = /* GraphQL */ `
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customerUserErrors {
code
field
message
}
customer {
id
}
}
}
`

View File

@ -0,0 +1,65 @@
import {
customerUserErrorsFragment,
customerAccessTokenFragment,
} from '../fragments/customer-fragments'
export const customerActivateMutation = /* GraphQL */ `
mutation customerActivate($id: ID!, $input: CustomerActivateInput!) {
customerActivate(id: $id, input: $input) {
customer {
id
}
...customerAccessToken
...customerUserErrors
}
}
${customerUserErrorsFragment}
${customerAccessTokenFragment}
`
export const customerActivateByUrlMutation = /* GraphQL */ `
mutation customerActivateByUrl($activationUrl: URL!, $password: String!) {
customerActivateByUrl(activationUrl: $activationUrl, password: $password) {
customer {
id
}
...customerAccessToken
...customerUserErrors
}
}
${customerUserErrorsFragment}
${customerAccessTokenFragment}
`
export const customerAccessTokenCreateMutation = /* GraphQL */ `
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
customerAccessTokenCreate(input: $input) {
...customerAccessToken
...customerUserErrors
}
}
${customerUserErrorsFragment}
${customerAccessTokenFragment}
`
export const customerAccessTokenDeleteMutation = /* GraphQL */ `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
...customerUserErrors
}
}
${customerUserErrorsFragment}
`
export const customerCreateMutation = /* GraphQL */ `
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
...customerUserErrors
customer {
id
}
}
}
${customerUserErrorsFragment}
`

View File

@ -1,9 +1,2 @@
export { customerCreateMutation } from './customer-create' export * from './cart-mutations'
export { cartCreateMutation } from './cart-create' export * from './customer-mutations'
export { cartLinesAddMutation } from './cart-lines-item-add'
export { cartLinesUpdateMutation } from './cart-lines-item-update'
export { cartLinesRemoveMutation } from './cart-lines-item-remove'
export { customerAccessTokenCreateMutation } from './customer-access-token-create'
export { customerAccessTokenDeleteMutation } from './customer-access-token-delete'
export { customerActivateMutation } from './customer-activate'
export { customerActivateByUrlMutation } from './customer-activate-by-url'

View File

@ -1,5 +1,5 @@
import type { Page } from '../types/page' import type { Page } from '../types/page'
import type { Product } from '../types/product' import type { Product, ProductPrice } from '../types/product'
import type { Cart, LineItem } from '../types/cart' import type { Cart, LineItem } from '../types/cart'
import type { Category } from '../types/site' import type { Category } from '../types/site'
@ -18,10 +18,14 @@ import {
import { colorMap } from './colors' import { colorMap } from './colors'
import { CommerceError } from '@vercel/commerce/utils/errors' import { CommerceError } from '@vercel/commerce/utils/errors'
const money = ({ amount, currencyCode }: MoneyV2) => { type MoneyProps = MoneyV2 & { retailPrice?: string }
const money = (money: MoneyProps): ProductPrice => {
const { amount, currencyCode, retailPrice } = money || { currencyCode: 'USD' }
return { return {
value: +amount, value: +amount,
currencyCode, currencyCode,
...(retailPrice && { retailPrice: +retailPrice }),
} }
} }
@ -52,46 +56,47 @@ const normalizeProductOption = ({
} }
} }
const normalizeProductImages = ({ edges }: ImageConnection) => const normalizeProductImages = (images: ImageConnection) =>
edges?.map(({ node: { altText: alt, url, ...rest } }) => ({ images?.nodes?.map(({ altText: alt, url, ...rest }) => ({
...rest, ...rest,
url, url,
alt, alt,
})) })) ?? []
const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { const normalizeProductVariants = (variants: ProductVariantConnection) => {
return edges?.map( return (
({ variants?.nodes?.map(
node: { ({
id, id,
selectedOptions, selectedOptions,
sku, sku,
title, title,
priceV2, priceV2,
image,
compareAtPriceV2, compareAtPriceV2,
requiresShipping, requiresShipping,
availableForSale, availableForSale,
}, }) => {
}) => { return {
return { id,
id, name: title,
name: title, sku: sku ?? id,
sku: sku ?? id, image,
price: +priceV2.amount, price: money({ ...priceV2, retailPrice: compareAtPriceV2?.amount }),
listPrice: +compareAtPriceV2?.amount, requiresShipping,
requiresShipping, availableForSale,
availableForSale, options: selectedOptions.map(({ name, value }: SelectedOption) => {
options: selectedOptions.map(({ name, value }: SelectedOption) => { const options = normalizeProductOption({
const options = normalizeProductOption({ id,
id, name,
name, values: [value],
values: [value], })
})
return options return options
}), }),
}
} }
} ) ?? []
) )
} }
@ -104,20 +109,24 @@ export function normalizeProduct({
description, description,
descriptionHtml, descriptionHtml,
handle, handle,
priceRange,
options, options,
metafields, metafields,
...rest ...rest
}: ShopifyProduct): Product { }: ShopifyProduct): Product {
const variant = variants?.nodes?.[0]
return { return {
id, id,
name, name,
vendor, vendor,
path: `/${handle}`, path: `/${handle}`,
slug: handle?.replace(/^\/+|\/+$/g, ''), slug: handle?.replace(/^\/+|\/+$/g, ''),
price: money(priceRange?.minVariantPrice), price: money({
images: normalizeProductImages(images), ...variant?.priceV2,
variants: variants ? normalizeProductVariants(variants) : [], retailPrice: variant?.compareAtPriceV2?.amount,
}),
images: images ? normalizeProductImages(images) : [variant?.image],
variants: normalizeProductVariants(variants),
options: options options: options
? options ? options
.filter((o) => o.name !== 'Title') // By default Shopify adds a 'Title' name when there's only one option. We don't need it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095 .filter((o) => o.name !== 'Title') // By default Shopify adds a 'Title' name when there's only one option. We don't need it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095
@ -148,7 +157,7 @@ function normalizeLineItem({
}, },
requiresShipping: variant?.requiresShipping ?? false, requiresShipping: variant?.requiresShipping ?? false,
price: +variant?.priceV2?.amount, price: +variant?.priceV2?.amount,
listPrice: variant?.compareAtPriceV2?.amount, listPrice: +variant?.compareAtPriceV2?.amount,
}, },
path: variant?.product?.handle, path: variant?.product?.handle,
discounts: [], discounts: [],

View File

@ -1,39 +1,4 @@
export const productConnectionFragment = /* GraphQL */ ` import { productCardFragment } from '../fragments/product-card-fragment'
fragment productConnection on ProductConnection {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
vendor
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
url
altText
width
height
}
}
}
}
}
}
`
export const getAllProductsQuery = /* GraphQL */ ` export const getAllProductsQuery = /* GraphQL */ `
query getAllProducts( query getAllProducts(
@ -48,9 +13,16 @@ export const getAllProductsQuery = /* GraphQL */ `
reverse: $reverse reverse: $reverse
query: $query query: $query
) { ) {
...productConnection pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
...productCard
}
}
} }
} }
${productCardFragment}
${productConnectionFragment}
` `

View File

@ -1,77 +1,4 @@
export const cartDetailsFragment = /* GraphQL */ ` import { cartDetailsFragment } from '../fragments/cart-details-fragment'
fragment cartDetails on Cart {
id
checkoutUrl
createdAt
updatedAt
lines(first: 10) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
id
sku
title
selectedOptions {
name
value
}
image {
url
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
product {
title
handle
}
}
}
}
}
}
attributes {
key
value
}
buyerIdentity {
email
customer {
id
}
}
estimatedCost {
totalAmount {
amount
currencyCode
}
subtotalAmount {
amount
currencyCode
}
totalTaxAmount {
amount
currencyCode
}
totalDutyAmount {
amount
currencyCode
}
}
}
`
export const getCartQuery = /* GraphQL */ ` export const getCartQuery = /* GraphQL */ `
query getCart($cartId: ID!) { query getCart($cartId: ID!) {

View File

@ -1,69 +0,0 @@
export const checkoutDetailsFragment = /* GraphQL */ `
fragment checkoutDetails on Checkout {
id
webUrl
subtotalPriceV2 {
amount
currencyCode
}
totalTaxV2 {
amount
currencyCode
}
totalPriceV2 {
amount
currencyCode
}
completedAt
createdAt
taxesIncluded
lineItems(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
variant {
id
sku
title
selectedOptions {
name
value
}
image {
url
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
product {
handle
}
}
quantity
}
}
}
}
`
export const getCheckoutQuery = /* GraphQL */ `
query getCheckout($checkoutId: ID!) {
node(id: $checkoutId) {
...checkoutDetails
}
}
${checkoutDetailsFragment}
`

View File

@ -1,4 +1,4 @@
import { productConnectionFragment } from './get-all-products-query' import { productCardFragment } from '../fragments/product-card-fragment'
export const getCollectionProductsQuery = /* GraphQL */ ` export const getCollectionProductsQuery = /* GraphQL */ `
query getProductsFromCollection( query getProductsFromCollection(
@ -11,10 +11,20 @@ export const getCollectionProductsQuery = /* GraphQL */ `
id id
... on Collection { ... on Collection {
products(first: $first, sortKey: $sortKey, reverse: $reverse) { products(first: $first, sortKey: $sortKey, reverse: $reverse) {
...productConnection pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
...productCard
}
}
} }
} }
} }
} }
${productConnectionFragment} ${productCardFragment}
` `

View File

@ -14,57 +14,56 @@ export const getProductQuery = /* GraphQL */ `
name name
values values
} }
priceRange { seo {
maxVariantPrice { description
amount title
currencyCode
}
minVariantPrice {
amount
currencyCode
}
} }
variants(first: 250) { variants(first: 100) {
pageInfo { nodes {
hasNextPage id
hasPreviousPage title
} sku
edges { availableForSale
node { requiresShipping
image {
id id
title
sku
availableForSale
requiresShipping
selectedOptions {
name
value
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
}
}
}
images(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
url
altText altText
url
width width
height height
} }
selectedOptions {
name
value
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
} }
} }
images(first: 15) {
nodes {
url
altText
width
height
}
}
}
shop {
shippingPolicy {
body
handle
}
refundPolicy {
body
handle
}
} }
} }
` `

View File

@ -1,7 +0,0 @@
export const getSiteInfoQuery = /* GraphQL */ `
query getSiteInfo {
shop {
name
}
}
`

View File

@ -7,5 +7,4 @@ export { getCollectionProductsQuery } from './get-collection-products-query'
export { getAllPagesQuery } from './get-all-pages-query' export { getAllPagesQuery } from './get-all-pages-query'
export { getPageQuery } from './get-page-query' export { getPageQuery } from './get-page-query'
export { getCustomerQuery } from './get-customer-query' export { getCustomerQuery } from './get-customer-query'
export { getSiteInfoQuery } from './get-site-info-query'
export { getCartQuery } from './get-cart-query' export { getCartQuery } from './get-cart-query'

View File

@ -1,14 +0,0 @@
import Cookies from 'js-cookie'
import { SHOPIFY_CART_URL_COOKIE, SHOPIFY_COOKIE_EXPIRE } from '../const'
export const setCartUrlCookie = (cartUrl: string) => {
if (cartUrl) {
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
if (oldCookie !== cartUrl) {
Cookies.set(SHOPIFY_CART_URL_COOKIE, cartUrl, {
expires: SHOPIFY_COOKIE_EXPIRE,
})
}
}
}