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
|
||||
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"])
|
||||
@include(if: $hasLocale) {
|
||||
edges {
|
||||
|
@ -17,6 +17,7 @@ import { normalizeProduct } from '../../lib/normalize'
|
||||
export const getAllProductsQuery = /* GraphQL */ `
|
||||
query getAllProducts(
|
||||
$hasLocale: Boolean = false
|
||||
$withCustomFields: Boolean = false
|
||||
$locale: String = "null"
|
||||
$entityIds: [Int!]
|
||||
$first: Int = 10
|
||||
|
@ -12,6 +12,7 @@ import { normalizeProduct } from '../../lib/normalize'
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct(
|
||||
$hasLocale: Boolean = false
|
||||
$withCustomFields: Boolean = false
|
||||
$locale: String = "null"
|
||||
$path: String!
|
||||
) {
|
||||
@ -98,6 +99,7 @@ export default function getAllProductPathsOperation({
|
||||
const config = commerce.getConfig(cfg)
|
||||
const { locale } = config
|
||||
const variables: GetProductQueryVariables = {
|
||||
...vars,
|
||||
locale,
|
||||
hasLocale: !!locale,
|
||||
path: slug ? `/${slug}/` : vars.path!,
|
||||
|
@ -2,6 +2,7 @@
|
||||
"provider": "bigcommerce",
|
||||
"features": {
|
||||
"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 { Page } from '../types/page'
|
||||
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 {
|
||||
const {
|
||||
entityId: id,
|
||||
productOptions,
|
||||
customFields,
|
||||
prices,
|
||||
path,
|
||||
id: _,
|
||||
@ -31,28 +44,63 @@ export function normalizeProduct(productNode: any): Product {
|
||||
return update(productNode, {
|
||||
id: { $set: String(id) },
|
||||
images: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
$apply: ({ edges }: any) => [
|
||||
...edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
url: urlOriginal,
|
||||
alt: altText,
|
||||
...rest,
|
||||
})),
|
||||
|
||||
...productNode.variants?.edges
|
||||
?.map(({ node: { defaultImage } }: any) =>
|
||||
defaultImage
|
||||
? { url: defaultImage.urlOriginal, alt: defaultImage.altText }
|
||||
: null
|
||||
)
|
||||
.filter(Boolean),
|
||||
],
|
||||
},
|
||||
|
||||
variants: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||
id: entityId,
|
||||
options: productOptions?.edges
|
||||
? productOptions.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
...rest,
|
||||
})),
|
||||
edges?.map(
|
||||
({
|
||||
node: { entityId, productOptions, prices, defaultImage, ...rest },
|
||||
}: any) => ({
|
||||
id: entityId,
|
||||
...(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: {
|
||||
$set: productOptions.edges
|
||||
$set: productOptions?.edges
|
||||
? productOptions?.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
},
|
||||
customFields: {
|
||||
$set: customFields?.edges
|
||||
? customFields?.edges.map(normalizeCustomFieldsValue)
|
||||
: [],
|
||||
},
|
||||
brand: {
|
||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
||||
},
|
||||
@ -63,6 +111,10 @@ export function normalizeProduct(productNode: any): Product {
|
||||
$set: {
|
||||
value: prices?.price.value,
|
||||
currencyCode: prices?.price.currencyCode,
|
||||
...(prices?.salePrice?.value && { salePrice: prices?.salePrice.value }),
|
||||
...(prices?.retailPrice?.value && {
|
||||
retailPrice: prices?.retailPrice.value,
|
||||
}),
|
||||
},
|
||||
},
|
||||
$unset: ['entityId'],
|
||||
|
@ -35,11 +35,14 @@ export type ProductVariant = {
|
||||
image?: ProductImage
|
||||
}
|
||||
|
||||
export type ProductMetafield = {
|
||||
export type ProductCustomField = {
|
||||
key: string
|
||||
name: string
|
||||
value: string
|
||||
description?: string
|
||||
|
||||
type?: string
|
||||
htmlValue?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type Product = {
|
||||
@ -56,7 +59,7 @@ export type Product = {
|
||||
options: ProductOption[]
|
||||
vendor?: string
|
||||
seo?: SEO
|
||||
metafields?: ProductMetafield[]
|
||||
customFields?: ProductCustomField[]
|
||||
}
|
||||
|
||||
export type SearchProductsBody = {
|
||||
@ -108,5 +111,5 @@ export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
|
||||
|
||||
export type GetProductOperation<T extends ProductTypes = ProductTypes> = {
|
||||
data: { product?: T['product'] }
|
||||
variables: { slug?: string; path?: string; withMetafields?: boolean }
|
||||
variables: { slug?: string; path?: string; withCustomFields?: boolean }
|
||||
}
|
||||
|
@ -51,6 +51,7 @@
|
||||
"dependencies": {
|
||||
"@vercel/commerce": "^0.0.1",
|
||||
"@vercel/fetch": "^6.1.1",
|
||||
"humanize-string": "^3.0.0",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"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',
|
||||
/** The input value should be greater than or equal to the minimum value allowed. */
|
||||
GreaterThanOrEqualTo = 'GREATER_THAN_OR_EQUAL_TO',
|
||||
/** Higher value discount applied. */
|
||||
HigherValueDiscountApplied = 'HIGHER_VALUE_DISCOUNT_APPLIED',
|
||||
/** The input value is invalid. */
|
||||
Invalid = 'INVALID',
|
||||
/** Cannot specify country and presentment currency code. */
|
||||
@ -6867,47 +6869,6 @@ export type CustomerAccessTokenFragment = {
|
||||
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 = {
|
||||
__typename?: 'Product'
|
||||
id: string
|
||||
@ -7849,7 +7810,7 @@ export type GetPageQuery = {
|
||||
|
||||
export type GetProductBySlugQueryVariables = Exact<{
|
||||
slug: Scalars['String']
|
||||
withMetafields?: InputMaybe<Scalars['Boolean']>
|
||||
withCustomFields?: InputMaybe<Scalars['Boolean']>
|
||||
}>
|
||||
|
||||
export type GetProductBySlugQuery = {
|
||||
|
@ -2175,6 +2175,11 @@ enum CheckoutErrorCode {
|
||||
"""
|
||||
GREATER_THAN_OR_EQUAL_TO
|
||||
|
||||
"""
|
||||
Higher value discount applied.
|
||||
"""
|
||||
HIGHER_VALUE_DISCOUNT_APPLIED
|
||||
|
||||
"""
|
||||
The input value is invalid.
|
||||
"""
|
||||
|
@ -2,13 +2,17 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import { GetAllProductsOperation } from '../../types/product'
|
||||
|
||||
import type { GetAllProductsOperation } from '../../types/product'
|
||||
|
||||
import {
|
||||
GetAllProductsQuery,
|
||||
GetAllProductsQueryVariables,
|
||||
Product as ShopifyProduct,
|
||||
} from '../../../schema'
|
||||
|
||||
import type { ShopifyConfig, Provider } from '..'
|
||||
|
||||
import { normalizeProduct, getAllProductsQuery } from '../../utils'
|
||||
|
||||
export default function getAllProductsOperation({
|
||||
|
@ -61,7 +61,7 @@ export default function getProductOperation({
|
||||
|
||||
return {
|
||||
...(productByHandle && {
|
||||
product: normalizeProduct(productByHandle as ShopifyProduct),
|
||||
product: normalizeProduct(productByHandle as ShopifyProduct, locale),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"features": {
|
||||
"wishlist": false,
|
||||
"customerAuth": true,
|
||||
"metafields": true
|
||||
"customFields": true
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ export type MetaieldTypes =
|
||||
|
||||
export type MetafieldType = LiteralUnion<MetaieldTypes>
|
||||
|
||||
export type ProductMetafield = Core.ProductMetafield & {
|
||||
export type ProductCustomField = Core.ProductCustomField & {
|
||||
type?: MetafieldType
|
||||
}
|
||||
|
||||
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 { 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 {
|
||||
SHOPIFY_CART_URL_COOKIE,
|
||||
@ -8,6 +21,109 @@ import {
|
||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||
} 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) => {
|
||||
if (cartUrl) {
|
||||
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
|
||||
@ -27,34 +143,28 @@ export const getSortVariables = (
|
||||
sort?: string,
|
||||
isCategory: boolean = false
|
||||
) => {
|
||||
let output = {}
|
||||
switch (sort) {
|
||||
case 'price-asc':
|
||||
output = {
|
||||
return {
|
||||
sortKey: 'PRICE',
|
||||
reverse: false,
|
||||
}
|
||||
break
|
||||
case 'price-desc':
|
||||
output = {
|
||||
return {
|
||||
sortKey: 'PRICE',
|
||||
reverse: true,
|
||||
}
|
||||
break
|
||||
case 'trending-desc':
|
||||
output = {
|
||||
return {
|
||||
sortKey: 'BEST_SELLING',
|
||||
reverse: false,
|
||||
}
|
||||
break
|
||||
case 'latest-desc':
|
||||
output = {
|
||||
return {
|
||||
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
|
||||
reverse: true,
|
||||
}
|
||||
break
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
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 { handleFetchResponse } from './handle-fetch-response'
|
||||
export { cartCreate } from './cart-create'
|
||||
export { handleLogin, handleAutomaticLogin } from './handle-login'
|
||||
export { handleAccountActivation } from './handle-account-activation'
|
||||
export { getCategories } from './get-categories'
|
||||
export { getBrands } from './get-brands'
|
||||
|
||||
export {
|
||||
handleLogin,
|
||||
handleAutomaticLogin,
|
||||
handleAccountActivation,
|
||||
} from './handle-login'
|
||||
|
||||
export * from './helpers'
|
||||
export * from './queries'
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 { Category } from '../types/site'
|
||||
|
||||
import {
|
||||
import type {
|
||||
Product as ShopifyProduct,
|
||||
SelectedOption,
|
||||
ImageConnection,
|
||||
@ -14,12 +14,15 @@ import {
|
||||
PageEdge,
|
||||
Collection,
|
||||
CartDetailsFragment,
|
||||
MetafieldConnection,
|
||||
} from '../../schema'
|
||||
import { colorMap } from './colors'
|
||||
|
||||
import humanizeString from 'humanize-string'
|
||||
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 { amount, currencyCode, retailPrice } = money || { currencyCode: 'USD' }
|
||||
@ -101,20 +104,24 @@ const normalizeProductVariants = (variants: ProductVariantConnection) => {
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeProduct({
|
||||
id,
|
||||
title: name,
|
||||
vendor,
|
||||
images,
|
||||
variants,
|
||||
description,
|
||||
descriptionHtml,
|
||||
handle,
|
||||
options,
|
||||
metafields,
|
||||
...rest
|
||||
}: ShopifyProduct): Product {
|
||||
export function normalizeProduct(
|
||||
{
|
||||
id,
|
||||
title: name,
|
||||
vendor,
|
||||
images,
|
||||
variants,
|
||||
description,
|
||||
descriptionHtml,
|
||||
handle,
|
||||
options,
|
||||
metafields,
|
||||
...rest
|
||||
}: ShopifyProduct,
|
||||
locale?: string
|
||||
): Product {
|
||||
const variant = variants?.nodes?.[0]
|
||||
|
||||
return {
|
||||
id,
|
||||
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
|
||||
.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 }),
|
||||
...(descriptionHtml && { descriptionHtml }),
|
||||
...rest,
|
||||
@ -212,3 +226,18 @@ export const normalizeCategory = ({
|
||||
slug: 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 */ `
|
||||
query getProductBySlug($slug: String!, $withMetafields: Boolean = false) {
|
||||
query getProductBySlug($slug: String!, $withCustomFields: Boolean = false) {
|
||||
productByHandle(handle: $slug) {
|
||||
id
|
||||
handle
|
||||
@ -46,7 +46,7 @@ export const getProductQuery = /* GraphQL */ `
|
||||
}
|
||||
}
|
||||
}
|
||||
metafields(first: 25) @include(if: $withMetafields) {
|
||||
metafields(first: 25) @include(if: $withCustomFields) {
|
||||
nodes {
|
||||
key
|
||||
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 { useProduct } from '../context'
|
||||
import ProductCustomFields from '../ProductCustomFields'
|
||||
interface ProductSidebarProps {
|
||||
className?: string
|
||||
}
|
||||
@ -59,23 +60,6 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
|
||||
)}
|
||||
</div>
|
||||
<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">
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends.
|
||||
@ -85,6 +69,13 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
|
||||
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
||||
to COVID-19.
|
||||
</Collapse>
|
||||
{process.env.COMMERCE_CUSTOMFIELDS_ENABLED &&
|
||||
product.customFields &&
|
||||
product.customFields.length > 0 && (
|
||||
<Collapse title="Technical Details">
|
||||
<ProductCustomFields fields={product.customFields} />
|
||||
</Collapse>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ export async function getStaticProps({
|
||||
const productPromise = commerce.getProduct({
|
||||
variables: {
|
||||
slug: params!.slug,
|
||||
withMetafields: Boolean(process.env.COMMERCE_METAFIELDS_ENABLED),
|
||||
withCustomFields: Boolean(process.env.COMMERCE_CUSTOMFIELDS_ENABLED),
|
||||
},
|
||||
config,
|
||||
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"
|
||||
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:
|
||||
version "0.2.0"
|
||||
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:
|
||||
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:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
|
||||
|
Loading…
x
Reference in New Issue
Block a user