mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 06:56:59 +00:00
Implement metafields
This commit is contained in:
parent
90aa798891
commit
6699f2fed4
@ -83,7 +83,59 @@ export interface ProductVariant {
|
|||||||
*/
|
*/
|
||||||
image?: Image
|
image?: Image
|
||||||
}
|
}
|
||||||
|
export interface ProductMetafield {
|
||||||
|
/**
|
||||||
|
* The key name for the metafield.
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace for the metafield.
|
||||||
|
* @example `rating`
|
||||||
|
*/
|
||||||
|
namespace: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the metafield.
|
||||||
|
* @example `{"value": 5, "scale_max": 5}`
|
||||||
|
*/
|
||||||
|
value: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically transformed value of the metafield.
|
||||||
|
*/
|
||||||
|
html?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the metafield.
|
||||||
|
* @example `date`
|
||||||
|
*/
|
||||||
|
type?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the metafield, that can be used as a label.
|
||||||
|
*/
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product Metafields, grouped by namespace.
|
||||||
|
* The namespace is the key of the object, and the value is an object with the metafield key and an object with the metafield data.
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* reviews: {
|
||||||
|
* rating: {
|
||||||
|
* key: 'rating',
|
||||||
|
* value: 5,
|
||||||
|
* // ... other metafield properties
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export interface ProductMetafields {
|
||||||
|
[namespace: string]: {
|
||||||
|
[key: string]: ProductMetafield
|
||||||
|
}
|
||||||
|
}
|
||||||
export interface Product {
|
export interface Product {
|
||||||
/**
|
/**
|
||||||
* The unique identifier for the product.
|
* The unique identifier for the product.
|
||||||
@ -117,6 +169,11 @@ export interface Product {
|
|||||||
* List of images associated with the product.
|
* List of images associated with the product.
|
||||||
*/
|
*/
|
||||||
images: Image[]
|
images: Image[]
|
||||||
|
/**
|
||||||
|
* The product’s metafields. This is a list of metafields that are attached to the product.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metafields?: ProductMetafields
|
||||||
/**
|
/**
|
||||||
* List of the product’s variants.
|
* List of the product’s variants.
|
||||||
*/
|
*/
|
||||||
@ -194,7 +251,6 @@ export type ProductsSchema = {
|
|||||||
/**
|
/**
|
||||||
* Product operations
|
* Product operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetAllProductPathsOperation = {
|
export type GetAllProductPathsOperation = {
|
||||||
data: { products: Pick<Product, 'path'>[] }
|
data: { products: Pick<Product, 'path'>[] }
|
||||||
variables: { first?: number }
|
variables: { first?: number }
|
||||||
@ -209,7 +265,40 @@ export type GetAllProductsOperation = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MetafieldsIdentifiers =
|
||||||
|
| Record<string, string[]>
|
||||||
|
| Array<{
|
||||||
|
namespace: string
|
||||||
|
key: string
|
||||||
|
}>
|
||||||
|
|
||||||
export type GetProductOperation = {
|
export type GetProductOperation = {
|
||||||
data: { product?: Product }
|
data: { product?: Product }
|
||||||
variables: { path: string; slug?: never } | { path?: never; slug: string }
|
variables:
|
||||||
|
| {
|
||||||
|
path: string
|
||||||
|
slug?: never
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
path?: never
|
||||||
|
slug: string
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* Metafields identifiers used to fetch the product metafields.
|
||||||
|
* It can be an array of objects with the namespace and key, or an object with the namespace as the key and an array of keys as the value.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* metafields: {
|
||||||
|
* reviews: ['rating', 'count']
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // or
|
||||||
|
*
|
||||||
|
* metafields: [
|
||||||
|
* {namespace: 'reviews', key: 'rating'},
|
||||||
|
* {namespace: 'reviews', key: 'count'},
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
withMetafields?: MetafieldsIdentifiers
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ export default function getProductOperation({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...(productByHandle && {
|
...(productByHandle && {
|
||||||
product: normalizeProduct(productByHandle as ShopifyProduct),
|
product: normalizeProduct(productByHandle as ShopifyProduct, locale),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,14 +30,7 @@ const fetchGraphqlApi: GraphQLFetcher = async (
|
|||||||
|
|
||||||
return { data, res }
|
return { data, res }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw getError(
|
throw getError([err], 500)
|
||||||
[
|
|
||||||
{
|
|
||||||
message: `${err} \n Most likely related to an unexpected output. E.g: NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN & NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN might be incorect.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
500
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default fetchGraphqlApi
|
export default fetchGraphqlApi
|
||||||
|
39
packages/shopify/src/types/metafields.ts
Normal file
39
packages/shopify/src/types/metafields.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
type LiteralUnion<T extends string> = T | Omit<T, T>
|
||||||
|
|
||||||
|
export type MetafieldTypes =
|
||||||
|
| 'integer'
|
||||||
|
| 'boolean'
|
||||||
|
| 'color'
|
||||||
|
| 'json'
|
||||||
|
| 'date'
|
||||||
|
| 'file_reference'
|
||||||
|
| 'date_time'
|
||||||
|
| 'dimension'
|
||||||
|
| 'multi_line_text_field'
|
||||||
|
| 'number_decimal'
|
||||||
|
| 'number_integer'
|
||||||
|
| 'page_reference'
|
||||||
|
| 'product_reference'
|
||||||
|
| 'rating'
|
||||||
|
| 'single_line_text_field'
|
||||||
|
| 'url'
|
||||||
|
| 'variant_reference'
|
||||||
|
| 'volume'
|
||||||
|
| 'weight'
|
||||||
|
| 'list.color'
|
||||||
|
| 'list.date'
|
||||||
|
| 'list.date_time'
|
||||||
|
| 'list.dimension'
|
||||||
|
| 'list.file_reference'
|
||||||
|
| 'list.number_integer'
|
||||||
|
| 'list.number_decimal'
|
||||||
|
| 'list.page_reference'
|
||||||
|
| 'list.product_reference'
|
||||||
|
| 'list.rating'
|
||||||
|
| 'list.single_line_text_field'
|
||||||
|
| 'list.url'
|
||||||
|
| 'list.variant_reference'
|
||||||
|
| 'list.volume'
|
||||||
|
| 'list.weight'
|
||||||
|
|
||||||
|
export type MetafieldType = LiteralUnion<MetafieldTypes>
|
@ -1,7 +1,12 @@
|
|||||||
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(errors: any[] | null, status: number) {
|
||||||
errors = errors ?? [{ message: 'Failed to fetch Shopify API' }]
|
errors = errors ?? [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Failed to fetch Shopify API, most likely related to an unexpected output. E.g: NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN & NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN might be incorect',
|
||||||
|
},
|
||||||
|
]
|
||||||
return new FetcherError({ errors, status })
|
return new FetcherError({ errors, status })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
118
packages/shopify/src/utils/metafields.ts
Normal file
118
packages/shopify/src/utils/metafields.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import type { MetafieldType } from '../types/metafields'
|
||||||
|
|
||||||
|
export const parseJson = (value: string): any => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (e) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitConversion: Record<string, string> = {
|
||||||
|
mm: 'millimeter',
|
||||||
|
cm: 'centimeter',
|
||||||
|
m: 'meter',
|
||||||
|
in: 'inch',
|
||||||
|
ft: 'foot',
|
||||||
|
yd: 'yard',
|
||||||
|
|
||||||
|
ml: 'milliliter',
|
||||||
|
l: 'liter',
|
||||||
|
us_fl_oz: 'fluid-ounce',
|
||||||
|
us_gal: 'gallon',
|
||||||
|
|
||||||
|
kg: 'kilogram',
|
||||||
|
g: 'gram',
|
||||||
|
lb: 'pound',
|
||||||
|
oz: 'ounce',
|
||||||
|
|
||||||
|
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',
|
||||||
|
|
||||||
|
FEET: 'foot',
|
||||||
|
}
|
||||||
|
|
||||||
|
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' ? '✓' : '✕'
|
||||||
|
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" rel="norreferrer">${value}</a>`
|
||||||
|
case 'multi_line_text_field':
|
||||||
|
return value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => `${line}<br/>`)
|
||||||
|
.join('')
|
||||||
|
case 'json':
|
||||||
|
case 'single_line_text_field':
|
||||||
|
case 'product_reference':
|
||||||
|
case 'page_reference':
|
||||||
|
case 'variant_reference':
|
||||||
|
case 'file_reference':
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toLabel = (string: string) =>
|
||||||
|
string
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[_-]+/g, ' ')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ')
|
@ -1,7 +1,8 @@
|
|||||||
import type { Page } from '@vercel/commerce/types/page'
|
import type { Page } from '@vercel/commerce/types/page'
|
||||||
import type { Product } from '@vercel/commerce/types/product'
|
import type { Product, ProductMetafield } from '@vercel/commerce/types/product'
|
||||||
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
||||||
import type { Category } from '@vercel/commerce/types/site'
|
import type { Category } from '@vercel/commerce/types/site'
|
||||||
|
import type { MetafieldType } from '../types/metafields'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Product as ShopifyProduct,
|
Product as ShopifyProduct,
|
||||||
@ -15,9 +16,12 @@ import type {
|
|||||||
Page as ShopifyPage,
|
Page as ShopifyPage,
|
||||||
PageEdge,
|
PageEdge,
|
||||||
Collection,
|
Collection,
|
||||||
|
Maybe,
|
||||||
|
Metafield,
|
||||||
} from '../../schema'
|
} from '../../schema'
|
||||||
|
|
||||||
import { colorMap } from './colors'
|
import { colorMap } from './colors'
|
||||||
|
import { getMetafieldValue, toLabel, parseJson } from './metafields'
|
||||||
|
|
||||||
const money = ({ amount, currencyCode }: MoneyV2) => {
|
const money = ({ amount, currencyCode }: MoneyV2) => {
|
||||||
return {
|
return {
|
||||||
@ -87,7 +91,6 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
|
|||||||
name,
|
name,
|
||||||
values: [value],
|
values: [value],
|
||||||
})
|
})
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -95,7 +98,8 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeProduct({
|
export function normalizeProduct(
|
||||||
|
{
|
||||||
id,
|
id,
|
||||||
title: name,
|
title: name,
|
||||||
vendor,
|
vendor,
|
||||||
@ -108,7 +112,9 @@ export function normalizeProduct({
|
|||||||
options,
|
options,
|
||||||
metafields,
|
metafields,
|
||||||
...rest
|
...rest
|
||||||
}: ShopifyProduct): Product {
|
}: ShopifyProduct,
|
||||||
|
locale?: string
|
||||||
|
): Product {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -123,12 +129,48 @@ 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: normalizeMetafields(metafields, locale),
|
||||||
description: description || '',
|
description: description || '',
|
||||||
...(descriptionHtml && { descriptionHtml }),
|
...(descriptionHtml && { descriptionHtml }),
|
||||||
...rest,
|
...rest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeMetafields(
|
||||||
|
metafields: Maybe<Metafield>[],
|
||||||
|
locale?: string
|
||||||
|
) {
|
||||||
|
const output: Record<string, Record<string, ProductMetafield>> = {}
|
||||||
|
|
||||||
|
if (!metafields) return output
|
||||||
|
|
||||||
|
for (const metafield of metafields) {
|
||||||
|
if (!metafield) continue
|
||||||
|
|
||||||
|
const { key, type, namespace, value, ...rest } = metafield
|
||||||
|
|
||||||
|
const newField = {
|
||||||
|
...rest,
|
||||||
|
key,
|
||||||
|
name: toLabel(key),
|
||||||
|
type,
|
||||||
|
namespace,
|
||||||
|
value,
|
||||||
|
html: getMetafieldValue(type, value, locale),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output[namespace]) {
|
||||||
|
output[namespace] = {
|
||||||
|
[key]: newField,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output[namespace][key] = newField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeCart(checkout: Checkout): Cart {
|
export function normalizeCart(checkout: Checkout): Cart {
|
||||||
return {
|
return {
|
||||||
id: checkout.id,
|
id: checkout.id,
|
||||||
@ -197,3 +239,19 @@ 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,8 @@
|
|||||||
const getProductQuery = /* GraphQL */ `
|
const getProductQuery = /* GraphQL */ `
|
||||||
query getProductBySlug($slug: String!) {
|
query getProductBySlug(
|
||||||
|
$slug: String!
|
||||||
|
$withMetafields: [HasMetafieldsIdentifier!] = []
|
||||||
|
) {
|
||||||
productByHandle(handle: $slug) {
|
productByHandle(handle: $slug) {
|
||||||
id
|
id
|
||||||
handle
|
handle
|
||||||
@ -24,7 +27,7 @@ const getProductQuery = /* GraphQL */ `
|
|||||||
currencyCode
|
currencyCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
variants(first: 250) {
|
variants(first: 25) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
hasPreviousPage
|
hasPreviousPage
|
||||||
@ -51,7 +54,7 @@ const getProductQuery = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
images(first: 250) {
|
images(first: 25) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
hasPreviousPage
|
hasPreviousPage
|
||||||
@ -65,6 +68,13 @@ const getProductQuery = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
metafields(identifiers: $withMetafields) {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
namespace
|
||||||
|
description
|
||||||
|
type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -62,10 +62,16 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
className="pb-4 break-words w-full max-w-xl"
|
className="pb-4 break-words w-full max-w-xl"
|
||||||
html={product.descriptionHtml || product.description}
|
html={product.descriptionHtml || product.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{product.metafields?.reviews?.rating && (
|
||||||
<div className="flex flex-row justify-between items-center">
|
<div className="flex flex-row justify-between items-center">
|
||||||
<Rating value={4} />
|
<Rating value={product.metafields.reviews.rating.value} />
|
||||||
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
<div className="text-accent-6 pr-1 font-medium text-sm">
|
||||||
|
{product.metafields.reviews.count?.value || 2} reviews
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{error && <ErrorMessage error={error} className="my-5" />}
|
{error && <ErrorMessage error={error} className="my-5" />}
|
||||||
{process.env.COMMERCE_CART_ENABLED && (
|
{process.env.COMMERCE_CART_ENABLED && (
|
||||||
@ -84,15 +90,28 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
{product.metafields?.descriptors?.care_guide && (
|
||||||
<Collapse title="Care">
|
<Collapse title="Care">
|
||||||
This is a limited edition production run. Printing starts when the
|
<Text
|
||||||
drop ends.
|
className="leading-0"
|
||||||
|
html={product.metafields.descriptors.care_guide.html}
|
||||||
|
/>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{product.metafields?.my_fields && (
|
||||||
<Collapse title="Details">
|
<Collapse title="Details">
|
||||||
This is a limited edition production run. Printing starts when the
|
{Object.entries(product.metafields.my_fields).map(([_, field]) => (
|
||||||
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
<div
|
||||||
to COVID-19.
|
key={field.key}
|
||||||
|
className="flex gap-2 border-b py-3 border-accent-2 border-dashed last:border-b-0"
|
||||||
|
>
|
||||||
|
<strong className="leading-7">{field.name}:</strong>
|
||||||
|
<Text html={field.html || field.value} className="!mx-0" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -3,11 +3,23 @@ import type {
|
|||||||
GetStaticPropsContext,
|
GetStaticPropsContext,
|
||||||
InferGetStaticPropsType,
|
InferGetStaticPropsType,
|
||||||
} from 'next'
|
} from 'next'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import commerce from '@lib/api/commerce'
|
import commerce from '@lib/api/commerce'
|
||||||
|
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { Layout } from '@components/common'
|
import { Layout } from '@components/common'
|
||||||
import { ProductView } from '@components/product'
|
import { ProductView } from '@components/product'
|
||||||
|
|
||||||
|
// Used by the Shopify Example
|
||||||
|
const withMetafields = [
|
||||||
|
{ namespace: 'reviews', key: 'rating' },
|
||||||
|
{ namespace: 'descriptors', key: 'care_guide' },
|
||||||
|
{ namespace: 'my_fields', key: 'weight' },
|
||||||
|
{ namespace: 'my_fields', key: 'width' },
|
||||||
|
{ namespace: 'my_fields', key: 'length' },
|
||||||
|
{ namespace: 'my_fields', key: 'manufacturer_url' },
|
||||||
|
]
|
||||||
|
|
||||||
export async function getStaticProps({
|
export async function getStaticProps({
|
||||||
params,
|
params,
|
||||||
locale,
|
locale,
|
||||||
@ -18,7 +30,10 @@ export async function getStaticProps({
|
|||||||
const pagesPromise = commerce.getAllPages({ config, preview })
|
const pagesPromise = commerce.getAllPages({ config, preview })
|
||||||
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
||||||
const productPromise = commerce.getProduct({
|
const productPromise = commerce.getProduct({
|
||||||
variables: { slug: params!.slug },
|
variables: {
|
||||||
|
slug: params!.slug,
|
||||||
|
withMetafields,
|
||||||
|
},
|
||||||
config,
|
config,
|
||||||
preview,
|
preview,
|
||||||
})
|
})
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
"@components/*": ["components/*"],
|
"@components/*": ["components/*"],
|
||||||
"@commerce": ["../packages/commerce/src"],
|
"@commerce": ["../packages/commerce/src"],
|
||||||
"@commerce/*": ["../packages/commerce/src/*"],
|
"@commerce/*": ["../packages/commerce/src/*"],
|
||||||
"@framework": ["../packages/local/src"],
|
"@framework": ["../packages/shopify/src"],
|
||||||
"@framework/*": ["../packages/local/src/*"]
|
"@framework/*": ["../packages/shopify/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user