Add metafields

This commit is contained in:
cond0r 2022-07-22 14:17:47 +03:00
parent a56c84375b
commit d8f6703b21
23 changed files with 423 additions and 154 deletions

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
"provider": "bigcommerce", "provider": "bigcommerce",
"features": { "features": {
"wishlist": true, "wishlist": true,
"customerAuth": true "customerAuth": true,
"customFields": true
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@ export default function getProductOperation({
return { return {
...(productByHandle && { ...(productByHandle && {
product: normalizeProduct(productByHandle as ShopifyProduct), product: normalizeProduct(productByHandle as ShopifyProduct, locale),
}), }),
} }
} }

View File

@ -3,6 +3,6 @@
"features": { "features": {
"wishlist": false, "wishlist": false,
"customerAuth": true, "customerAuth": true,
"metafields": true "customFields": true
} }
} }

View File

@ -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[]
} }

View File

@ -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
# }
}
`

View File

@ -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' ? '&#10003;' : '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 ? '&#9733;' : '&#9734;'
).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
}
}

View File

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

View File

@ -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(' &#8226; ')
: value
}
return getMetafieldValue(type, value, locale)
}

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './ProductCustomFields'

View File

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

View File

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

View File

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