mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Add metafields
This commit is contained in:
parent
a56c84375b
commit
d8f6703b21
@ -67,6 +67,9 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
altText
|
altText
|
||||||
isDefault
|
isDefault
|
||||||
}
|
}
|
||||||
|
prices {
|
||||||
|
...productPrices
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,6 +83,15 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
customFields @include(if: $withCustomFields) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
|
localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
|
||||||
@include(if: $hasLocale) {
|
@include(if: $hasLocale) {
|
||||||
edges {
|
edges {
|
||||||
|
@ -17,6 +17,7 @@ import { normalizeProduct } from '../../lib/normalize'
|
|||||||
export const getAllProductsQuery = /* GraphQL */ `
|
export const getAllProductsQuery = /* GraphQL */ `
|
||||||
query getAllProducts(
|
query getAllProducts(
|
||||||
$hasLocale: Boolean = false
|
$hasLocale: Boolean = false
|
||||||
|
$withCustomFields: Boolean = false
|
||||||
$locale: String = "null"
|
$locale: String = "null"
|
||||||
$entityIds: [Int!]
|
$entityIds: [Int!]
|
||||||
$first: Int = 10
|
$first: Int = 10
|
||||||
|
@ -12,6 +12,7 @@ import { normalizeProduct } from '../../lib/normalize'
|
|||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
query getProduct(
|
query getProduct(
|
||||||
$hasLocale: Boolean = false
|
$hasLocale: Boolean = false
|
||||||
|
$withCustomFields: Boolean = false
|
||||||
$locale: String = "null"
|
$locale: String = "null"
|
||||||
$path: String!
|
$path: String!
|
||||||
) {
|
) {
|
||||||
@ -98,6 +99,7 @@ export default function getAllProductPathsOperation({
|
|||||||
const config = commerce.getConfig(cfg)
|
const config = commerce.getConfig(cfg)
|
||||||
const { locale } = config
|
const { locale } = config
|
||||||
const variables: GetProductQueryVariables = {
|
const variables: GetProductQueryVariables = {
|
||||||
|
...vars,
|
||||||
locale,
|
locale,
|
||||||
hasLocale: !!locale,
|
hasLocale: !!locale,
|
||||||
path: slug ? `/${slug}/` : vars.path!,
|
path: slug ? `/${slug}/` : vars.path!,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"provider": "bigcommerce",
|
"provider": "bigcommerce",
|
||||||
"features": {
|
"features": {
|
||||||
"wishlist": true,
|
"wishlist": true,
|
||||||
"customerAuth": true
|
"customerAuth": true,
|
||||||
|
"customFields": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Product } from '../types/product'
|
import type { Product, ProductCustomField } from '../types/product'
|
||||||
import type { Cart, BigcommerceCart, LineItem } from '../types/cart'
|
import type { Cart, BigcommerceCart, LineItem } from '../types/cart'
|
||||||
import type { Page } from '../types/page'
|
import type { Page } from '../types/page'
|
||||||
import type { BCCategory, Category } from '../types/site'
|
import type { BCCategory, Category } from '../types/site'
|
||||||
@ -18,10 +18,23 @@ function normalizeProductOption(productOption: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCustomFieldsValue(field: any): ProductCustomField {
|
||||||
|
const {
|
||||||
|
node: { entityId, name, value },
|
||||||
|
} = field
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: String(entityId),
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeProduct(productNode: any): Product {
|
export function normalizeProduct(productNode: any): Product {
|
||||||
const {
|
const {
|
||||||
entityId: id,
|
entityId: id,
|
||||||
productOptions,
|
productOptions,
|
||||||
|
customFields,
|
||||||
prices,
|
prices,
|
||||||
path,
|
path,
|
||||||
id: _,
|
id: _,
|
||||||
@ -31,28 +44,63 @@ export function normalizeProduct(productNode: any): Product {
|
|||||||
return update(productNode, {
|
return update(productNode, {
|
||||||
id: { $set: String(id) },
|
id: { $set: String(id) },
|
||||||
images: {
|
images: {
|
||||||
$apply: ({ edges }: any) =>
|
$apply: ({ edges }: any) => [
|
||||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
...edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||||
url: urlOriginal,
|
url: urlOriginal,
|
||||||
alt: altText,
|
alt: altText,
|
||||||
...rest,
|
...rest,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
...productNode.variants?.edges
|
||||||
|
?.map(({ node: { defaultImage } }: any) =>
|
||||||
|
defaultImage
|
||||||
|
? { url: defaultImage.urlOriginal, alt: defaultImage.altText }
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
.filter(Boolean),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
$apply: ({ edges }: any) =>
|
$apply: ({ edges }: any) =>
|
||||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
edges?.map(
|
||||||
id: entityId,
|
({
|
||||||
options: productOptions?.edges
|
node: { entityId, productOptions, prices, defaultImage, ...rest },
|
||||||
? productOptions.edges.map(normalizeProductOption)
|
}: any) => ({
|
||||||
: [],
|
id: entityId,
|
||||||
...rest,
|
...(defaultImage && {
|
||||||
})),
|
image: {
|
||||||
|
url: defaultImage.urlOriginal,
|
||||||
|
alt: defaultImage.altText,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
price: {
|
||||||
|
value: prices?.price.value,
|
||||||
|
currencyCode: prices?.price.currencyCode,
|
||||||
|
...(prices?.salePrice?.value && {
|
||||||
|
salePrice: prices?.salePrice.value,
|
||||||
|
}),
|
||||||
|
...(prices?.retailPrice?.value && {
|
||||||
|
retailPrice: prices?.retailPrice.value,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
options: productOptions?.edges
|
||||||
|
? productOptions.edges.map(normalizeProductOption)
|
||||||
|
: [],
|
||||||
|
...rest,
|
||||||
|
})
|
||||||
|
),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
$set: productOptions.edges
|
$set: productOptions?.edges
|
||||||
? productOptions?.edges.map(normalizeProductOption)
|
? productOptions?.edges.map(normalizeProductOption)
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
|
customFields: {
|
||||||
|
$set: customFields?.edges
|
||||||
|
? customFields?.edges.map(normalizeCustomFieldsValue)
|
||||||
|
: [],
|
||||||
|
},
|
||||||
brand: {
|
brand: {
|
||||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
||||||
},
|
},
|
||||||
@ -63,6 +111,10 @@ export function normalizeProduct(productNode: any): Product {
|
|||||||
$set: {
|
$set: {
|
||||||
value: prices?.price.value,
|
value: prices?.price.value,
|
||||||
currencyCode: prices?.price.currencyCode,
|
currencyCode: prices?.price.currencyCode,
|
||||||
|
...(prices?.salePrice?.value && { salePrice: prices?.salePrice.value }),
|
||||||
|
...(prices?.retailPrice?.value && {
|
||||||
|
retailPrice: prices?.retailPrice.value,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
$unset: ['entityId'],
|
$unset: ['entityId'],
|
||||||
|
@ -35,11 +35,14 @@ export type ProductVariant = {
|
|||||||
image?: ProductImage
|
image?: ProductImage
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductMetafield = {
|
export type ProductCustomField = {
|
||||||
key: string
|
key: string
|
||||||
|
name: string
|
||||||
value: string
|
value: string
|
||||||
description?: string
|
|
||||||
type?: string
|
type?: string
|
||||||
|
htmlValue?: string
|
||||||
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
@ -56,7 +59,7 @@ export type Product = {
|
|||||||
options: ProductOption[]
|
options: ProductOption[]
|
||||||
vendor?: string
|
vendor?: string
|
||||||
seo?: SEO
|
seo?: SEO
|
||||||
metafields?: ProductMetafield[]
|
customFields?: ProductCustomField[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchProductsBody = {
|
export type SearchProductsBody = {
|
||||||
@ -108,5 +111,5 @@ export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
|
|||||||
|
|
||||||
export type GetProductOperation<T extends ProductTypes = ProductTypes> = {
|
export type GetProductOperation<T extends ProductTypes = ProductTypes> = {
|
||||||
data: { product?: T['product'] }
|
data: { product?: T['product'] }
|
||||||
variables: { slug?: string; path?: string; withMetafields?: boolean }
|
variables: { slug?: string; path?: string; withCustomFields?: boolean }
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/commerce": "^0.0.1",
|
"@vercel/commerce": "^0.0.1",
|
||||||
"@vercel/fetch": "^6.1.1",
|
"@vercel/fetch": "^6.1.1",
|
||||||
|
"humanize-string": "^3.0.0",
|
||||||
"lodash.debounce": "^4.0.8"
|
"lodash.debounce": "^4.0.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
45
packages/shopify/schema.d.ts
vendored
45
packages/shopify/schema.d.ts
vendored
@ -1283,6 +1283,8 @@ export enum CheckoutErrorCode {
|
|||||||
GiftCardUnusable = 'GIFT_CARD_UNUSABLE',
|
GiftCardUnusable = 'GIFT_CARD_UNUSABLE',
|
||||||
/** The input value should be greater than or equal to the minimum value allowed. */
|
/** The input value should be greater than or equal to the minimum value allowed. */
|
||||||
GreaterThanOrEqualTo = 'GREATER_THAN_OR_EQUAL_TO',
|
GreaterThanOrEqualTo = 'GREATER_THAN_OR_EQUAL_TO',
|
||||||
|
/** Higher value discount applied. */
|
||||||
|
HigherValueDiscountApplied = 'HIGHER_VALUE_DISCOUNT_APPLIED',
|
||||||
/** The input value is invalid. */
|
/** The input value is invalid. */
|
||||||
Invalid = 'INVALID',
|
Invalid = 'INVALID',
|
||||||
/** Cannot specify country and presentment currency code. */
|
/** Cannot specify country and presentment currency code. */
|
||||||
@ -6867,47 +6869,6 @@ export type CustomerAccessTokenFragment = {
|
|||||||
expiresAt: any
|
expiresAt: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type Media_ExternalVideo_Fragment = {
|
|
||||||
__typename?: 'ExternalVideo'
|
|
||||||
mediaContentType: MediaContentType
|
|
||||||
alt?: string | null
|
|
||||||
previewImage?: { __typename?: 'Image'; url: any } | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type Media_MediaImage_Fragment = {
|
|
||||||
__typename?: 'MediaImage'
|
|
||||||
id: string
|
|
||||||
mediaContentType: MediaContentType
|
|
||||||
alt?: string | null
|
|
||||||
image?: {
|
|
||||||
__typename?: 'Image'
|
|
||||||
url: any
|
|
||||||
width?: number | null
|
|
||||||
height?: number | null
|
|
||||||
} | null
|
|
||||||
previewImage?: { __typename?: 'Image'; url: any } | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type Media_Model3d_Fragment = {
|
|
||||||
__typename?: 'Model3d'
|
|
||||||
mediaContentType: MediaContentType
|
|
||||||
alt?: string | null
|
|
||||||
previewImage?: { __typename?: 'Image'; url: any } | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type Media_Video_Fragment = {
|
|
||||||
__typename?: 'Video'
|
|
||||||
mediaContentType: MediaContentType
|
|
||||||
alt?: string | null
|
|
||||||
previewImage?: { __typename?: 'Image'; url: any } | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MediaFragment =
|
|
||||||
| Media_ExternalVideo_Fragment
|
|
||||||
| Media_MediaImage_Fragment
|
|
||||||
| Media_Model3d_Fragment
|
|
||||||
| Media_Video_Fragment
|
|
||||||
|
|
||||||
export type ProductCardFragment = {
|
export type ProductCardFragment = {
|
||||||
__typename?: 'Product'
|
__typename?: 'Product'
|
||||||
id: string
|
id: string
|
||||||
@ -7849,7 +7810,7 @@ export type GetPageQuery = {
|
|||||||
|
|
||||||
export type GetProductBySlugQueryVariables = Exact<{
|
export type GetProductBySlugQueryVariables = Exact<{
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
withMetafields?: InputMaybe<Scalars['Boolean']>
|
withCustomFields?: InputMaybe<Scalars['Boolean']>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type GetProductBySlugQuery = {
|
export type GetProductBySlugQuery = {
|
||||||
|
@ -2175,6 +2175,11 @@ enum CheckoutErrorCode {
|
|||||||
"""
|
"""
|
||||||
GREATER_THAN_OR_EQUAL_TO
|
GREATER_THAN_OR_EQUAL_TO
|
||||||
|
|
||||||
|
"""
|
||||||
|
Higher value discount applied.
|
||||||
|
"""
|
||||||
|
HIGHER_VALUE_DISCOUNT_APPLIED
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The input value is invalid.
|
The input value is invalid.
|
||||||
"""
|
"""
|
||||||
|
@ -2,13 +2,17 @@ import type {
|
|||||||
OperationContext,
|
OperationContext,
|
||||||
OperationOptions,
|
OperationOptions,
|
||||||
} from '@vercel/commerce/api/operations'
|
} from '@vercel/commerce/api/operations'
|
||||||
import { GetAllProductsOperation } from '../../types/product'
|
|
||||||
|
import type { GetAllProductsOperation } from '../../types/product'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GetAllProductsQuery,
|
GetAllProductsQuery,
|
||||||
GetAllProductsQueryVariables,
|
GetAllProductsQueryVariables,
|
||||||
Product as ShopifyProduct,
|
Product as ShopifyProduct,
|
||||||
} from '../../../schema'
|
} from '../../../schema'
|
||||||
|
|
||||||
import type { ShopifyConfig, Provider } from '..'
|
import type { ShopifyConfig, Provider } from '..'
|
||||||
|
|
||||||
import { normalizeProduct, getAllProductsQuery } from '../../utils'
|
import { normalizeProduct, getAllProductsQuery } from '../../utils'
|
||||||
|
|
||||||
export default function getAllProductsOperation({
|
export default function getAllProductsOperation({
|
||||||
|
@ -61,7 +61,7 @@ export default function getProductOperation({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...(productByHandle && {
|
...(productByHandle && {
|
||||||
product: normalizeProduct(productByHandle as ShopifyProduct),
|
product: normalizeProduct(productByHandle as ShopifyProduct, locale),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"wishlist": false,
|
"wishlist": false,
|
||||||
"customerAuth": true,
|
"customerAuth": true,
|
||||||
"metafields": true
|
"customFields": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,10 +42,10 @@ export type MetaieldTypes =
|
|||||||
|
|
||||||
export type MetafieldType = LiteralUnion<MetaieldTypes>
|
export type MetafieldType = LiteralUnion<MetaieldTypes>
|
||||||
|
|
||||||
export type ProductMetafield = Core.ProductMetafield & {
|
export type ProductCustomField = Core.ProductCustomField & {
|
||||||
type?: MetafieldType
|
type?: MetafieldType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Product = Core.Product & {
|
export type Product = Core.Product & {
|
||||||
metafields?: ProductMetafield[]
|
metafields?: ProductCustomField[]
|
||||||
}
|
}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
# }
|
|
||||||
}
|
|
||||||
`
|
|
@ -1,5 +1,18 @@
|
|||||||
import Cookies, { CookieAttributes } from 'js-cookie'
|
import Cookies, { CookieAttributes } from 'js-cookie'
|
||||||
import { SearchProductsBody } from '../types/product'
|
|
||||||
|
import type {
|
||||||
|
CartLineInput,
|
||||||
|
CollectionEdge,
|
||||||
|
CartCreateMutation,
|
||||||
|
CartDetailsFragment,
|
||||||
|
CartCreateMutationVariables,
|
||||||
|
GetAllProductVendorsQuery,
|
||||||
|
GetAllProductVendorsQueryVariables,
|
||||||
|
} from '../../schema'
|
||||||
|
|
||||||
|
import type { Category } from '../types/site'
|
||||||
|
import type { MetafieldType, SearchProductsBody } from '../types/product'
|
||||||
|
import type { FetcherOptions } from '@vercel/commerce/utils/types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SHOPIFY_CART_URL_COOKIE,
|
SHOPIFY_CART_URL_COOKIE,
|
||||||
@ -8,6 +21,109 @@ import {
|
|||||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||||
} from '../const'
|
} from '../const'
|
||||||
|
|
||||||
|
import { ShopifyConfig } from '../api'
|
||||||
|
import { normalizeCategory } from './normalize'
|
||||||
|
import { throwUserErrors } from './throw-user-errors'
|
||||||
|
import { cartCreateMutation } from './mutations/cart-mutations'
|
||||||
|
import { getAllProductVendors, getSiteCollectionsQuery } from './queries'
|
||||||
|
|
||||||
|
export const cartCreate = async (
|
||||||
|
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
|
||||||
|
lines?: Array<CartLineInput> | CartLineInput
|
||||||
|
): Promise<CartDetailsFragment | null | undefined> => {
|
||||||
|
const { cartCreate } = await fetch<
|
||||||
|
CartCreateMutation,
|
||||||
|
CartCreateMutationVariables
|
||||||
|
>({
|
||||||
|
query: cartCreateMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
lines,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const cart = cartCreate?.cart
|
||||||
|
|
||||||
|
throwUserErrors(cartCreate?.userErrors)
|
||||||
|
|
||||||
|
if (cart?.id) {
|
||||||
|
const options = {
|
||||||
|
expires: SHOPIFY_COOKIE_EXPIRE,
|
||||||
|
}
|
||||||
|
Cookies.set(SHOPIFY_CART_ID_COOKIE, cart.id, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCartUrlCookie(cart?.checkoutUrl)
|
||||||
|
|
||||||
|
return cart
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCategories = async ({
|
||||||
|
fetch,
|
||||||
|
locale,
|
||||||
|
}: ShopifyConfig): Promise<Category[]> => {
|
||||||
|
const { data } = await fetch(
|
||||||
|
getSiteCollectionsQuery,
|
||||||
|
{
|
||||||
|
variables: {
|
||||||
|
first: 250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(locale && {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': locale,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
data.collections?.edges?.map(({ node }: CollectionEdge) =>
|
||||||
|
normalizeCategory(node)
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Brand = {
|
||||||
|
entityId: string
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BrandEdge = {
|
||||||
|
node: Brand
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Brands = BrandEdge[]
|
||||||
|
|
||||||
|
export const getBrands = async (
|
||||||
|
config: ShopifyConfig
|
||||||
|
): Promise<BrandEdge[]> => {
|
||||||
|
const { data } = await config.fetch<
|
||||||
|
GetAllProductVendorsQuery,
|
||||||
|
GetAllProductVendorsQueryVariables
|
||||||
|
>(getAllProductVendors, {
|
||||||
|
variables: {
|
||||||
|
first: 250,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let vendorsStrings = data.products.edges.map(({ node: { vendor } }) => vendor)
|
||||||
|
|
||||||
|
return [...new Set(vendorsStrings)].map((v) => {
|
||||||
|
const id = v.replace(/\s+/g, '-').toLowerCase()
|
||||||
|
return {
|
||||||
|
node: {
|
||||||
|
entityId: id,
|
||||||
|
name: v,
|
||||||
|
path: `brands/${id}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const setCartUrlCookie = (cartUrl: string) => {
|
export const setCartUrlCookie = (cartUrl: string) => {
|
||||||
if (cartUrl) {
|
if (cartUrl) {
|
||||||
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
|
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
|
||||||
@ -27,34 +143,28 @@ export const getSortVariables = (
|
|||||||
sort?: string,
|
sort?: string,
|
||||||
isCategory: boolean = false
|
isCategory: boolean = false
|
||||||
) => {
|
) => {
|
||||||
let output = {}
|
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'price-asc':
|
case 'price-asc':
|
||||||
output = {
|
return {
|
||||||
sortKey: 'PRICE',
|
sortKey: 'PRICE',
|
||||||
reverse: false,
|
reverse: false,
|
||||||
}
|
}
|
||||||
break
|
|
||||||
case 'price-desc':
|
case 'price-desc':
|
||||||
output = {
|
return {
|
||||||
sortKey: 'PRICE',
|
sortKey: 'PRICE',
|
||||||
reverse: true,
|
reverse: true,
|
||||||
}
|
}
|
||||||
break
|
|
||||||
case 'trending-desc':
|
case 'trending-desc':
|
||||||
output = {
|
return {
|
||||||
sortKey: 'BEST_SELLING',
|
sortKey: 'BEST_SELLING',
|
||||||
reverse: false,
|
reverse: false,
|
||||||
}
|
}
|
||||||
break
|
|
||||||
case 'latest-desc':
|
case 'latest-desc':
|
||||||
output = {
|
return {
|
||||||
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
|
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
|
||||||
reverse: true,
|
reverse: true,
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSearchVariables = ({
|
export const getSearchVariables = ({
|
||||||
@ -102,3 +212,92 @@ export const setCustomerToken = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const parseJson = (value: string): any => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (e) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitConversion: Record<string, string> = {
|
||||||
|
MILLIMETERS: 'millimeter',
|
||||||
|
CENTIMETERS: 'centimeter',
|
||||||
|
METERS: 'meter',
|
||||||
|
|
||||||
|
MILLILITERS: 'milliliter',
|
||||||
|
LITERS: 'liter',
|
||||||
|
FLUID_OUNCES: 'fluid-ounce',
|
||||||
|
IMPERIAL_FLUID_OUNCES: 'fluid-ounce',
|
||||||
|
GALLONS: 'gallon',
|
||||||
|
|
||||||
|
KILOGRAMS: 'kilogram',
|
||||||
|
GRAMS: 'gram',
|
||||||
|
OUNCES: 'ounce',
|
||||||
|
POUNDS: 'pound',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMeasurment = (input: string, locale: string = 'en-US') => {
|
||||||
|
try {
|
||||||
|
let { unit, value } = JSON.parse(input)
|
||||||
|
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
unit: unitConversion[unit],
|
||||||
|
style: 'unit',
|
||||||
|
}).format(parseFloat(value))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetafieldValue = (
|
||||||
|
type: MetafieldType,
|
||||||
|
value: string,
|
||||||
|
locale: string = 'en-US'
|
||||||
|
) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'boolean':
|
||||||
|
return value === 'true' ? '✓' : 'No'
|
||||||
|
case 'number_integer':
|
||||||
|
return parseInt(value).toLocaleString(locale)
|
||||||
|
case 'number_decimal':
|
||||||
|
return parseFloat(value).toLocaleString(locale)
|
||||||
|
case 'date':
|
||||||
|
return Intl.DateTimeFormat(locale, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
}).format(new Date(value))
|
||||||
|
case 'date_time':
|
||||||
|
return Intl.DateTimeFormat(locale, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'long',
|
||||||
|
}).format(new Date(value))
|
||||||
|
case 'dimension':
|
||||||
|
case 'volume':
|
||||||
|
case 'weight':
|
||||||
|
return getMeasurment(value, locale)
|
||||||
|
case 'rating':
|
||||||
|
const { scale_max, value: val } = JSON.parse(value)
|
||||||
|
return Array.from({ length: scale_max }, (_, i) =>
|
||||||
|
i <= val - 1 ? '★' : '☆'
|
||||||
|
).join('')
|
||||||
|
case 'color':
|
||||||
|
return `<figure style="background-color: ${value}; width: 1rem; height:1rem; display:block; margin-top: 2px; border-radius: 100%;"/>`
|
||||||
|
case 'url':
|
||||||
|
return `<a href="${value}" target="_blank">${value}</a>`
|
||||||
|
case 'multi_line_text_field':
|
||||||
|
return value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => `<p>${line}</p>`)
|
||||||
|
.join('')
|
||||||
|
case 'json':
|
||||||
|
case 'single_line_text_field':
|
||||||
|
case 'product_reference':
|
||||||
|
case 'page_reference':
|
||||||
|
case 'variant_reference':
|
||||||
|
case 'file_reference':
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
export { throwUserErrors } from './throw-user-errors'
|
export { throwUserErrors } from './throw-user-errors'
|
||||||
export { handleFetchResponse } from './handle-fetch-response'
|
export { handleFetchResponse } from './handle-fetch-response'
|
||||||
export { cartCreate } from './cart-create'
|
|
||||||
export { handleLogin, handleAutomaticLogin } from './handle-login'
|
export {
|
||||||
export { handleAccountActivation } from './handle-account-activation'
|
handleLogin,
|
||||||
export { getCategories } from './get-categories'
|
handleAutomaticLogin,
|
||||||
export { getBrands } from './get-brands'
|
handleAccountActivation,
|
||||||
|
} from './handle-login'
|
||||||
|
|
||||||
export * from './helpers'
|
export * from './helpers'
|
||||||
export * from './queries'
|
export * from './queries'
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { Page } from '../types/page'
|
import type { Page } from '../types/page'
|
||||||
import type { Product, ProductPrice } from '../types/product'
|
import type { MetafieldType, 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'
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
Product as ShopifyProduct,
|
Product as ShopifyProduct,
|
||||||
SelectedOption,
|
SelectedOption,
|
||||||
ImageConnection,
|
ImageConnection,
|
||||||
@ -14,12 +14,15 @@ import {
|
|||||||
PageEdge,
|
PageEdge,
|
||||||
Collection,
|
Collection,
|
||||||
CartDetailsFragment,
|
CartDetailsFragment,
|
||||||
MetafieldConnection,
|
|
||||||
} from '../../schema'
|
} from '../../schema'
|
||||||
import { colorMap } from './colors'
|
|
||||||
|
import humanizeString from 'humanize-string'
|
||||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||||
|
|
||||||
type MoneyProps = MoneyV2 & { retailPrice?: string }
|
import { colorMap } from './colors'
|
||||||
|
import { getMetafieldValue, parseJson } from './helpers'
|
||||||
|
|
||||||
|
type MoneyProps = MoneyV2 & { retailPrice?: string | number }
|
||||||
|
|
||||||
const money = (money: MoneyProps): ProductPrice => {
|
const money = (money: MoneyProps): ProductPrice => {
|
||||||
const { amount, currencyCode, retailPrice } = money || { currencyCode: 'USD' }
|
const { amount, currencyCode, retailPrice } = money || { currencyCode: 'USD' }
|
||||||
@ -101,20 +104,24 @@ const normalizeProductVariants = (variants: ProductVariantConnection) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeProduct({
|
export function normalizeProduct(
|
||||||
id,
|
{
|
||||||
title: name,
|
id,
|
||||||
vendor,
|
title: name,
|
||||||
images,
|
vendor,
|
||||||
variants,
|
images,
|
||||||
description,
|
variants,
|
||||||
descriptionHtml,
|
description,
|
||||||
handle,
|
descriptionHtml,
|
||||||
options,
|
handle,
|
||||||
metafields,
|
options,
|
||||||
...rest
|
metafields,
|
||||||
}: ShopifyProduct): Product {
|
...rest
|
||||||
|
}: ShopifyProduct,
|
||||||
|
locale?: string
|
||||||
|
): Product {
|
||||||
const variant = variants?.nodes?.[0]
|
const variant = variants?.nodes?.[0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -132,7 +139,14 @@ export function normalizeProduct({
|
|||||||
.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
|
||||||
.map((o) => normalizeProductOption(o))
|
.map((o) => normalizeProductOption(o))
|
||||||
: [],
|
: [],
|
||||||
metafields: metafields?.nodes?.map((m) => m) || [],
|
customFields:
|
||||||
|
metafields?.nodes?.map(({ key, type, value }) => ({
|
||||||
|
key,
|
||||||
|
name: humanizeString(key),
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
htmlValue: normalizeMetafieldValue(type, value, locale),
|
||||||
|
})) || [],
|
||||||
...(description && { description }),
|
...(description && { description }),
|
||||||
...(descriptionHtml && { descriptionHtml }),
|
...(descriptionHtml && { descriptionHtml }),
|
||||||
...rest,
|
...rest,
|
||||||
@ -212,3 +226,18 @@ export const normalizeCategory = ({
|
|||||||
slug: handle,
|
slug: handle,
|
||||||
path: `/${handle}`,
|
path: `/${handle}`,
|
||||||
})
|
})
|
||||||
|
export const normalizeMetafieldValue = (
|
||||||
|
type: MetafieldType,
|
||||||
|
value: string,
|
||||||
|
locale?: string
|
||||||
|
) => {
|
||||||
|
if (type.startsWith('list.')) {
|
||||||
|
const arr = parseJson(value)
|
||||||
|
return Array.isArray(arr)
|
||||||
|
? arr
|
||||||
|
.map((v) => getMetafieldValue(type.split('.')[1], v, locale))
|
||||||
|
.join(' • ')
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
return getMetafieldValue(type, value, locale)
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
query getProductBySlug($slug: String!, $withMetafields: Boolean = false) {
|
query getProductBySlug($slug: String!, $withCustomFields: Boolean = false) {
|
||||||
productByHandle(handle: $slug) {
|
productByHandle(handle: $slug) {
|
||||||
id
|
id
|
||||||
handle
|
handle
|
||||||
@ -46,7 +46,7 @@ export const getProductQuery = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metafields(first: 25) @include(if: $withMetafields) {
|
metafields(first: 25) @include(if: $withCustomFields) {
|
||||||
nodes {
|
nodes {
|
||||||
key
|
key
|
||||||
value
|
value
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import type { ProductCustomField } from '@framework/types/product'
|
||||||
|
|
||||||
|
const ProductCustomFields = ({ fields }: { fields: ProductCustomField[] }) => {
|
||||||
|
return (
|
||||||
|
<ul className="flex flex-col space-y-2 divide-y divide-dashed">
|
||||||
|
{fields.map((m) => (
|
||||||
|
<li
|
||||||
|
className="flex space-x-2 justify-start items-start text-sm pt-2"
|
||||||
|
key={m.key}
|
||||||
|
>
|
||||||
|
<span className="font-bold capitalize whitespace-nowrap">
|
||||||
|
{m.name}
|
||||||
|
</span>
|
||||||
|
:
|
||||||
|
{m.htmlValue ? (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: m.htmlValue,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{m.value}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCustomFields
|
1
site/components/product/ProductCustomFields/index.ts
Normal file
1
site/components/product/ProductCustomFields/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProductCustomFields'
|
@ -5,6 +5,7 @@ import { ProductOptions } from '@components/product'
|
|||||||
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
||||||
|
|
||||||
import { useProduct } from '../context'
|
import { useProduct } from '../context'
|
||||||
|
import ProductCustomFields from '../ProductCustomFields'
|
||||||
interface ProductSidebarProps {
|
interface ProductSidebarProps {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@ -59,23 +60,6 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{process.env.COMMERCE_METAFIELDS_ENABLED &&
|
|
||||||
product.metafields &&
|
|
||||||
product.metafields?.length > 0 && (
|
|
||||||
<Collapse title="Metafields">
|
|
||||||
<ul className="flex flex-col space-y-2 divide-y divide-dashed">
|
|
||||||
{product.metafields.map((m) => (
|
|
||||||
<li
|
|
||||||
className="flex space-x-2 justify-start items-center text-sm pt-2"
|
|
||||||
key={m.key}
|
|
||||||
>
|
|
||||||
<span className="font-bold capitalize">{m.key}</span>:
|
|
||||||
<span>{m.value}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Collapse>
|
|
||||||
)}
|
|
||||||
<Collapse title="Care">
|
<Collapse title="Care">
|
||||||
This is a limited edition production run. Printing starts when the
|
This is a limited edition production run. Printing starts when the
|
||||||
drop ends.
|
drop ends.
|
||||||
@ -85,6 +69,13 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
|
|||||||
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
||||||
to COVID-19.
|
to COVID-19.
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
{process.env.COMMERCE_CUSTOMFIELDS_ENABLED &&
|
||||||
|
product.customFields &&
|
||||||
|
product.customFields.length > 0 && (
|
||||||
|
<Collapse title="Technical Details">
|
||||||
|
<ProductCustomFields fields={product.customFields} />
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -20,7 +20,7 @@ export async function getStaticProps({
|
|||||||
const productPromise = commerce.getProduct({
|
const productPromise = commerce.getProduct({
|
||||||
variables: {
|
variables: {
|
||||||
slug: params!.slug,
|
slug: params!.slug,
|
||||||
withMetafields: Boolean(process.env.COMMERCE_METAFIELDS_ENABLED),
|
withCustomFields: Boolean(process.env.COMMERCE_CUSTOMFIELDS_ENABLED),
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
preview,
|
preview,
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -2853,6 +2853,11 @@ decamelize@^1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||||
|
|
||||||
|
decamelize@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-6.0.0.tgz#8cad4d916fde5c41a264a43d0ecc56fe3d31749e"
|
||||||
|
integrity sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==
|
||||||
|
|
||||||
decode-uri-component@^0.2.0:
|
decode-uri-component@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||||
@ -3986,6 +3991,13 @@ humanize-ms@^1.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.0.0"
|
ms "^2.0.0"
|
||||||
|
|
||||||
|
humanize-string@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/humanize-string/-/humanize-string-3.0.0.tgz#4ea0ef1daf1d23fd8d8c7864adf0117f74939455"
|
||||||
|
integrity sha512-jhWD2GAZRMELz0IEIfqpEdi0M4CMQF1GpJpBYIopFN6wT+78STiujfQTKcKqZzOJgUkIgJSo2xFeHdsg922JZQ==
|
||||||
|
dependencies:
|
||||||
|
decamelize "^6.0.0"
|
||||||
|
|
||||||
husky@^7.0.4:
|
husky@^7.0.4:
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
|
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user