mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 06:56:59 +00:00
Merge branch 'custom-fields' of github.com:vercel/commerce into main
This commit is contained in:
commit
15faacc7de
@ -63,6 +63,7 @@
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@manifoldco/swagger-to-ts": "^2.1.0",
|
||||
"@taskr/clear": "^1.1.0",
|
||||
"@taskr/esnext": "^1.1.0",
|
||||
"@taskr/watch": "^1.1.0",
|
||||
@ -74,6 +75,7 @@
|
||||
"@types/react": "^18.0.14",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"node-fetch": "^2.6.7",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -80,6 +80,15 @@ export const productInfoFragment = /* GraphQL */ `
|
||||
}
|
||||
}
|
||||
}
|
||||
customFields {
|
||||
edges {
|
||||
node {
|
||||
entityId
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
|
||||
@include(if: $hasLocale) {
|
||||
edges {
|
||||
|
@ -5,10 +5,12 @@ import type {
|
||||
import type {
|
||||
GetCustomerWishlistOperation,
|
||||
Wishlist,
|
||||
WishlistItem,
|
||||
} from '@vercel/commerce/types/wishlist'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import type { RecursivePartial } from '../utils/types'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
||||
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
|
||||
export default function getCustomerWishlistOperation({
|
||||
commerce,
|
||||
@ -47,6 +49,12 @@ export default function getCustomerWishlistOperation({
|
||||
|
||||
const wishlist = data[0]
|
||||
|
||||
if (!wishlist) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const items: WishlistItem[] = []
|
||||
|
||||
if (includeProducts && wishlist?.items?.length) {
|
||||
const ids = wishlist.items
|
||||
?.map((item) => (item?.productId ? String(item?.productId) : null))
|
||||
@ -57,25 +65,36 @@ export default function getCustomerWishlistOperation({
|
||||
variables: { first: 50, ids },
|
||||
config,
|
||||
})
|
||||
|
||||
// Put the products in an object that we can use to get them by id
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: ProductEdge
|
||||
[k: number]: Product
|
||||
}>((prods, p) => {
|
||||
prods[Number(p.id)] = p as any
|
||||
return prods
|
||||
}, {})
|
||||
|
||||
// Populate the wishlist items with the graphql products
|
||||
wishlist.items.forEach((item) => {
|
||||
const product = item && productsById[Number(item.productId)]
|
||||
if (item && product) {
|
||||
// @ts-ignore Fix this type when the wishlist type is properly defined
|
||||
item.product = product
|
||||
items.push({
|
||||
id: String(item.id),
|
||||
productId: String(item.productId),
|
||||
variantId: String(item.variantId),
|
||||
product,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { wishlist: wishlist as RecursiveRequired<typeof wishlist> }
|
||||
return {
|
||||
wishlist: {
|
||||
id: String(wishlist.id),
|
||||
items,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return getCustomerWishlist
|
||||
|
@ -100,7 +100,7 @@ export default function getAllProductPathsOperation({
|
||||
const variables: GetProductQueryVariables = {
|
||||
locale,
|
||||
hasLocale: !!locale,
|
||||
path: slug ? `/${slug}` : vars.path!,
|
||||
path: `/${slug}`,
|
||||
}
|
||||
const { data } = await config.fetch<GetProductQuery>(query, { variables })
|
||||
const product = data.site?.route?.node
|
||||
|
@ -1,10 +1,12 @@
|
||||
import type { Page } from '@vercel/commerce/types/page'
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
import type { CustomField } from '@vercel/commerce/types/common'
|
||||
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
||||
import type { Category, Brand } from '@vercel/commerce/types/site'
|
||||
import type { BigcommerceCart, BCCategory, BCBrand } from '../types'
|
||||
import type { ProductNode } from '../api/operations/get-all-products'
|
||||
import type { definitions } from '../api/definitions/store-content'
|
||||
import type { CustomFieldEdge } from '../../schema'
|
||||
|
||||
import getSlug from './get-slug'
|
||||
|
||||
@ -20,10 +22,20 @@ function normalizeProductOption(productOption: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProduct(productNode: ProductNode): Product {
|
||||
// TODO: change this after schema definition is updated
|
||||
interface ProductNodeWithCustomFields extends ProductNode {
|
||||
customFields: {
|
||||
edges?: CustomFieldEdge[]
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProduct(
|
||||
productNode: ProductNodeWithCustomFields
|
||||
): Product {
|
||||
const {
|
||||
entityId: id,
|
||||
productOptions,
|
||||
customFields,
|
||||
prices,
|
||||
path,
|
||||
images,
|
||||
@ -52,6 +64,7 @@ export function normalizeProduct(productNode: ProductNode): Product {
|
||||
})
|
||||
) || [],
|
||||
options: productOptions?.edges?.map(normalizeProductOption) || [],
|
||||
customFields: customFields?.edges?.map(normalizeCustomFieldsValue) || [],
|
||||
slug: path?.replace(/^\/+|\/+$/g, ''),
|
||||
price: {
|
||||
value: prices?.price.value,
|
||||
@ -137,3 +150,15 @@ export function normalizeBrand(brand: BCBrand): Brand {
|
||||
path: `/${slug}`,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCustomFieldsValue(field: CustomFieldEdge): CustomField {
|
||||
const {
|
||||
node: { entityId, name, value },
|
||||
} = field
|
||||
|
||||
return {
|
||||
id: String(entityId),
|
||||
name,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
@ -34,3 +34,67 @@ export interface Image {
|
||||
*/
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface CustomField {
|
||||
/**
|
||||
* The unique identifier for the custom field.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* The name of the custom field.
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* The value of the custom field.
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Metafield {
|
||||
/**
|
||||
* The unique identifier for the metafield.
|
||||
*/
|
||||
key: string
|
||||
|
||||
/**
|
||||
* The namespace for the metafield, which is a container for a set of metadata.
|
||||
* @example `rating`
|
||||
*/
|
||||
namespace: string
|
||||
|
||||
/**
|
||||
* The value of the metafield, usually a string that can be might parsed into JSON.
|
||||
* @example `{"value": 5, "scale_max": 5}`
|
||||
*/
|
||||
value: any
|
||||
|
||||
/**
|
||||
* The value of the metafield, complete with HTML formatting.
|
||||
*/
|
||||
valueHtml?: string
|
||||
|
||||
/**
|
||||
* The type of the metafield, used to determine how the value should be interpreted.
|
||||
* For example: `string`, `integer`, `boolean`, `json` ...
|
||||
*/
|
||||
type?: string
|
||||
|
||||
/**
|
||||
* The name of the metafield, that can be used as a label.
|
||||
*/
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface Metafields {
|
||||
/**
|
||||
* The namespace for the metafield, which is a container for a set of metadata.
|
||||
* @example `reviews`, `specifications`
|
||||
*/
|
||||
[namespace: string]: {
|
||||
/**
|
||||
* The key of the metafield, which is the name of the metafield.
|
||||
* @example `rating`
|
||||
*/
|
||||
[key: string]: Metafield
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Image } from './common'
|
||||
import { CustomField, Image, Metafields } from './common'
|
||||
|
||||
export interface ProductPrice {
|
||||
/**
|
||||
@ -126,6 +126,33 @@ export interface Product {
|
||||
* List of images associated with the product.
|
||||
*/
|
||||
images: Image[]
|
||||
/**
|
||||
* List of custom fields / properties associated with the product.
|
||||
* @example
|
||||
* customFields: [{
|
||||
* id: '1',
|
||||
* name: 'Warehouse Location',
|
||||
* value: 'Aisle 3, Shelf 5, Bin 6'
|
||||
* }]
|
||||
*/
|
||||
customFields?: CustomField[]
|
||||
/**
|
||||
* Advanced custom fields that can be added to a product. They are used to store additional information about the product, in a structured format, grouped by namespaces.
|
||||
* @example
|
||||
* {
|
||||
* // Namespace, the container for a set of metadata
|
||||
* reviews: {
|
||||
* // Key of the metafield
|
||||
* rating: {
|
||||
* key: 'rating',
|
||||
* value: 4,
|
||||
* valueHtml: '★★★★☆',
|
||||
* type: 'integer',
|
||||
* name: 'Rating',
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
metafields?: Metafields
|
||||
/**
|
||||
* List of the product’s variants.
|
||||
*/
|
||||
@ -204,7 +231,6 @@ export type ProductsSchema = {
|
||||
/**
|
||||
* Product operations
|
||||
*/
|
||||
|
||||
export type GetAllProductPathsOperation = {
|
||||
data: { products: Pick<Product, 'path'>[] }
|
||||
variables: { first?: number }
|
||||
@ -219,7 +245,24 @@ export type GetAllProductsOperation = {
|
||||
}
|
||||
}
|
||||
|
||||
export type MetafieldsIdentifiers = Array<{
|
||||
namespace: string
|
||||
key: string
|
||||
}>
|
||||
|
||||
export type GetProductOperation = {
|
||||
data: { product?: Product }
|
||||
variables: { path: string; slug?: never } | { path?: never; slug: string }
|
||||
variables: {
|
||||
slug: string
|
||||
/**
|
||||
* Metafields identifiers used to fetch the product metafields, represented as an array of objects with the namespace and key.
|
||||
*
|
||||
* @example
|
||||
* withMetafields: [
|
||||
* {namespace: 'reviews', key: 'rating'},
|
||||
* {namespace: 'reviews', key: 'count'},
|
||||
* ]
|
||||
*/
|
||||
withMetafields?: MetafieldsIdentifiers
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,24 @@
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import { normalizeProduct } from '../../utils'
|
||||
import type { GetProductOperation } from '@vercel/commerce/types/product'
|
||||
import type { Provider, SaleorConfig } from '..'
|
||||
|
||||
import { normalizeProduct } from '../../utils'
|
||||
|
||||
import * as Query from '../../utils/queries'
|
||||
|
||||
type Variables = {
|
||||
slug: string
|
||||
}
|
||||
|
||||
type ReturnType = {
|
||||
product: any
|
||||
}
|
||||
|
||||
export default function getProductOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getProduct({
|
||||
async function getProduct<T extends GetProductOperation>({
|
||||
query = Query.ProductOneBySlug,
|
||||
variables,
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables: Variables
|
||||
variables: T['variables']
|
||||
config?: Partial<SaleorConfig>
|
||||
preview?: boolean
|
||||
}): Promise<ReturnType> {
|
||||
}): Promise<T['data']> {
|
||||
const { fetch, locale } = commerce.getConfig(cfg)
|
||||
|
||||
const { data } = await fetch(
|
||||
@ -37,9 +31,9 @@ export default function getProductOperation({
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
product: data && data.product ? normalizeProduct(data.product) : null,
|
||||
}
|
||||
return data && data.product
|
||||
? { product: normalizeProduct(data.product) }
|
||||
: {}
|
||||
}
|
||||
|
||||
return getProduct
|
||||
|
@ -55,7 +55,7 @@ export default function getProductOperation({
|
||||
|
||||
return {
|
||||
...(productByHandle && {
|
||||
product: normalizeProduct(productByHandle as ShopifyProduct),
|
||||
product: normalizeProduct(productByHandle as ShopifyProduct, locale),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -30,14 +30,7 @@ const fetchGraphqlApi: GraphQLFetcher = async (
|
||||
|
||||
return { data, res }
|
||||
} catch (err) {
|
||||
throw getError(
|
||||
[
|
||||
{
|
||||
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
|
||||
)
|
||||
throw getError([err], 500)
|
||||
}
|
||||
}
|
||||
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'
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
|
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: length, value: val } = JSON.parse(value)
|
||||
return Array.from({ length }, (_, 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,9 @@
|
||||
import type { Page } from '@vercel/commerce/types/page'
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
import type { Metafield } from '@vercel/commerce/types/common'
|
||||
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
||||
import type { Category } from '@vercel/commerce/types/site'
|
||||
import type { MetafieldType } from '../types/metafields'
|
||||
|
||||
import type {
|
||||
Product as ShopifyProduct,
|
||||
@ -15,14 +17,12 @@ import type {
|
||||
Page as ShopifyPage,
|
||||
PageEdge,
|
||||
Collection,
|
||||
MetafieldConnection,
|
||||
MediaConnection,
|
||||
Model3d,
|
||||
Metafield,
|
||||
Maybe,
|
||||
Metafield as ShopifyMetafield,
|
||||
} from '../../schema'
|
||||
|
||||
import { colorMap } from './colors'
|
||||
import { getMetafieldValue, toLabel, parseJson } from './metafields'
|
||||
|
||||
const money = ({ amount, currencyCode }: MoneyV2) => {
|
||||
return {
|
||||
@ -92,7 +92,6 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
|
||||
name,
|
||||
values: [value],
|
||||
})
|
||||
|
||||
return options
|
||||
}),
|
||||
}
|
||||
@ -100,35 +99,23 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
|
||||
)
|
||||
}
|
||||
|
||||
const normalizeProductMedia = ({ edges }: MediaConnection) => {
|
||||
return edges
|
||||
.filter(({ node }) => Object.keys(node).length !== 0)
|
||||
.map(({ node }) => {
|
||||
return {
|
||||
sources: (node as Model3d).sources.map(({ format, url }) => {
|
||||
return {
|
||||
format: format,
|
||||
url: url,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeProduct({
|
||||
id,
|
||||
title: name,
|
||||
vendor,
|
||||
images,
|
||||
variants,
|
||||
description,
|
||||
descriptionHtml,
|
||||
handle,
|
||||
priceRange,
|
||||
options,
|
||||
metafields,
|
||||
...rest
|
||||
}: ShopifyProduct): Product {
|
||||
export function normalizeProduct(
|
||||
{
|
||||
id,
|
||||
title: name,
|
||||
vendor,
|
||||
images,
|
||||
variants,
|
||||
description,
|
||||
descriptionHtml,
|
||||
handle,
|
||||
priceRange,
|
||||
options,
|
||||
metafields,
|
||||
...rest
|
||||
}: ShopifyProduct,
|
||||
locale?: string
|
||||
): Product {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
@ -143,12 +130,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
|
||||
.map((o) => normalizeProductOption(o))
|
||||
: [],
|
||||
metafields: normalizeMetafields(metafields, locale),
|
||||
description: description || '',
|
||||
...(descriptionHtml && { descriptionHtml }),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeMetafields(
|
||||
metafields: Maybe<ShopifyMetafield>[],
|
||||
locale?: string
|
||||
) {
|
||||
const output: Record<string, Record<string, Metafield>> = {}
|
||||
|
||||
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,
|
||||
valueHtml: getMetafieldValue(type, value, locale),
|
||||
}
|
||||
|
||||
if (!output[namespace]) {
|
||||
output[namespace] = {
|
||||
[key]: newField,
|
||||
}
|
||||
} else {
|
||||
output[namespace][key] = newField
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export function normalizeCart(checkout: Checkout): Cart {
|
||||
return {
|
||||
id: checkout.id,
|
||||
@ -217,3 +240,19 @@ 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,8 @@
|
||||
const getProductQuery = /* GraphQL */ `
|
||||
query getProductBySlug($slug: String!) {
|
||||
query getProductBySlug(
|
||||
$slug: String!
|
||||
$withMetafields: [HasMetafieldsIdentifier!] = []
|
||||
) {
|
||||
productByHandle(handle: $slug) {
|
||||
id
|
||||
handle
|
||||
@ -24,15 +27,7 @@ const getProductQuery = /* GraphQL */ `
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
metafields(first: 30) {
|
||||
edges {
|
||||
node {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
variants(first: 250) {
|
||||
variants(first: 25) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
@ -59,7 +54,7 @@ const getProductQuery = /* GraphQL */ `
|
||||
}
|
||||
}
|
||||
}
|
||||
images(first: 250) {
|
||||
images(first: 25) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
@ -88,6 +83,12 @@ const getProductQuery = /* GraphQL */ `
|
||||
}
|
||||
}
|
||||
}
|
||||
metafields(identifiers: $withMetafields) {
|
||||
key
|
||||
value
|
||||
namespace
|
||||
description
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { GetProductOperation } from '@vercel/commerce/types/product'
|
||||
|
||||
import { normalizeProduct } from '../../utils'
|
||||
|
||||
import { Product } from '@vercel/commerce/types/product'
|
||||
@ -7,15 +9,15 @@ import { Provider, SwellConfig } from '../'
|
||||
export default function getProductOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getProduct({
|
||||
async function getProduct<T extends GetProductOperation>({
|
||||
variables,
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables: { slug: string }
|
||||
variables: T['variables']
|
||||
config?: Partial<SwellConfig>
|
||||
preview?: boolean
|
||||
}): Promise<Product | {} | any> {
|
||||
}): Promise<T['data']> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
|
||||
const product = await config.fetch('products', 'get', [variables.slug])
|
||||
@ -24,9 +26,7 @@ export default function getProductOperation({
|
||||
product.variants = product.variants?.results
|
||||
}
|
||||
|
||||
return {
|
||||
product: product ? normalizeProduct(product) : null,
|
||||
}
|
||||
return product ? { product: normalizeProduct(product) } : {}
|
||||
}
|
||||
|
||||
return getProduct
|
||||
|
@ -1,22 +1,26 @@
|
||||
import { Product } from '@vercel/commerce/types/product'
|
||||
import { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import { Provider, VendureConfig } from '../'
|
||||
import { GetProductQuery } from '../../../schema'
|
||||
import type {
|
||||
Product,
|
||||
GetProductOperation,
|
||||
} from '@vercel/commerce/types/product'
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import type { Provider, VendureConfig } from '../'
|
||||
|
||||
import type { GetProductQuery } from '../../../schema'
|
||||
import { getProductQuery } from '../../utils/queries/get-product-query'
|
||||
|
||||
export default function getProductOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getProduct({
|
||||
async function getProduct<T extends GetProductOperation>({
|
||||
query = getProductQuery,
|
||||
variables,
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables: { slug: string }
|
||||
variables: T['variables']
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
}): Promise<Product | {} | any> {
|
||||
}): Promise<T['data']> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
|
||||
const locale = config.locale
|
||||
|
226
pnpm-lock.yaml
generated
226
pnpm-lock.yaml
generated
@ -15,6 +15,7 @@ importers:
|
||||
packages/bigcommerce:
|
||||
specifiers:
|
||||
'@cfworker/uuid': ^1.12.4
|
||||
'@manifoldco/swagger-to-ts': ^2.1.0
|
||||
'@taskr/clear': ^1.1.0
|
||||
'@taskr/esnext': ^1.1.0
|
||||
'@taskr/watch': ^1.1.0
|
||||
@ -33,6 +34,7 @@ importers:
|
||||
lint-staged: ^12.1.7
|
||||
lodash.debounce: ^4.0.8
|
||||
next: ^12.0.8
|
||||
node-fetch: ^2.6.7
|
||||
prettier: ^2.5.1
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
@ -51,6 +53,7 @@ importers:
|
||||
lodash.debounce: 4.0.8
|
||||
uuidv4: 6.2.13
|
||||
devDependencies:
|
||||
'@manifoldco/swagger-to-ts': 2.1.0
|
||||
'@taskr/clear': 1.1.0
|
||||
'@taskr/esnext': 1.1.0
|
||||
'@taskr/watch': 1.1.0
|
||||
@ -62,6 +65,7 @@ importers:
|
||||
'@types/react': 18.0.20
|
||||
lint-staged: 12.5.0
|
||||
next: 12.3.0_biqbaboplfbrettd7655fr4n2y
|
||||
node-fetch: 2.6.7
|
||||
prettier: 2.7.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
@ -3493,6 +3497,7 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
<<<<<<< HEAD
|
||||
/@motionone/animation/10.15.1:
|
||||
resolution: {integrity: sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==}
|
||||
dependencies:
|
||||
@ -3539,6 +3544,19 @@ packages:
|
||||
hey-listen: 1.0.8
|
||||
tslib: 2.4.0
|
||||
dev: false
|
||||
=======
|
||||
/@manifoldco/swagger-to-ts/2.1.0:
|
||||
resolution: {integrity: sha512-IH0FAHhwWHR3Gs3rnVHNEscZujGn+K6/2Zu5cWfZre3Vz2tx1SvvJKEbSM89MztfDDRjOpb+6pQD/vqdEoTBVg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
deprecated: This package has changed to openapi-typescript
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
js-yaml: 3.14.1
|
||||
meow: 7.1.1
|
||||
prettier: 2.7.1
|
||||
dev: true
|
||||
>>>>>>> 1ba9d3bd6e79da1f0b05df2f1c0c1ca3632b6c16
|
||||
|
||||
/@next/bundle-analyzer/12.3.0:
|
||||
resolution: {integrity: sha512-hzRLHIrtwOiGEku9rmG7qZk+OQhnqQOL+ycl2XrjBaztBN/xaqnjoG4+HEf9L7ELN943BR+K/ZlaF2OEgbGm+Q==}
|
||||
@ -4431,6 +4449,10 @@ packages:
|
||||
/@types/lodash/4.14.185:
|
||||
resolution: {integrity: sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==}
|
||||
|
||||
/@types/minimist/1.2.2:
|
||||
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
||||
dev: true
|
||||
|
||||
/@types/node-fetch/2.6.2:
|
||||
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
|
||||
dependencies:
|
||||
@ -4446,6 +4468,10 @@ packages:
|
||||
resolution: {integrity: sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==}
|
||||
dev: true
|
||||
|
||||
/@types/normalize-package-data/2.4.1:
|
||||
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
|
||||
dev: true
|
||||
|
||||
/@types/parse-json/4.0.0:
|
||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||
|
||||
@ -4768,6 +4794,12 @@ packages:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
dev: false
|
||||
|
||||
/argparse/1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
dev: true
|
||||
|
||||
/argparse/2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
@ -4870,6 +4902,11 @@ packages:
|
||||
es-shim-unscopables: 1.0.0
|
||||
dev: true
|
||||
|
||||
/arrify/1.0.1:
|
||||
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/asap/2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
dev: true
|
||||
@ -5204,6 +5241,15 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/camelcase-keys/6.2.2:
|
||||
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
map-obj: 4.3.0
|
||||
quick-lru: 4.0.1
|
||||
dev: true
|
||||
|
||||
/camelcase/5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
@ -5728,6 +5774,14 @@ packages:
|
||||
supports-color: 9.2.3
|
||||
dev: true
|
||||
|
||||
/decamelize-keys/1.1.1:
|
||||
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
decamelize: 1.2.0
|
||||
map-obj: 1.0.1
|
||||
dev: true
|
||||
|
||||
/decamelize/1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6285,6 +6339,12 @@ packages:
|
||||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
/esprima/4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/esquery/1.4.0:
|
||||
resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
|
||||
engines: {node: '>=0.10'}
|
||||
@ -6971,6 +7031,11 @@ packages:
|
||||
duplexer: 0.1.2
|
||||
dev: true
|
||||
|
||||
/hard-rejection/2.1.0:
|
||||
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/has-ansi/2.0.0:
|
||||
resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -7052,6 +7117,7 @@ packages:
|
||||
tslib: 2.4.0
|
||||
dev: true
|
||||
|
||||
<<<<<<< HEAD
|
||||
/hey-listen/1.0.8:
|
||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||
dev: false
|
||||
@ -7061,6 +7127,11 @@ packages:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
dev: false
|
||||
=======
|
||||
/hosted-git-info/2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
dev: true
|
||||
>>>>>>> 1ba9d3bd6e79da1f0b05df2f1c0c1ca3632b6c16
|
||||
|
||||
/http-cache-semantics/4.1.0:
|
||||
resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
|
||||
@ -7512,6 +7583,11 @@ packages:
|
||||
symbol-observable: 1.2.0
|
||||
dev: true
|
||||
|
||||
/is-plain-obj/1.1.0:
|
||||
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-plain-object/2.0.4:
|
||||
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -7660,6 +7736,14 @@ packages:
|
||||
/js-tokens/4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
/js-yaml/3.14.1:
|
||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
dev: true
|
||||
|
||||
/js-yaml/4.1.0:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
@ -8154,6 +8238,16 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/map-obj/1.0.1:
|
||||
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/map-obj/4.3.0:
|
||||
resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/map-visit/1.0.0:
|
||||
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -8172,6 +8266,23 @@ packages:
|
||||
resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==}
|
||||
dev: true
|
||||
|
||||
/meow/7.1.1:
|
||||
resolution: {integrity: sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
'@types/minimist': 1.2.2
|
||||
camelcase-keys: 6.2.2
|
||||
decamelize-keys: 1.1.1
|
||||
hard-rejection: 2.1.0
|
||||
minimist-options: 4.1.0
|
||||
normalize-package-data: 2.5.0
|
||||
read-pkg-up: 7.0.1
|
||||
redent: 3.0.0
|
||||
trim-newlines: 3.0.1
|
||||
type-fest: 0.13.1
|
||||
yargs-parser: 18.1.3
|
||||
dev: true
|
||||
|
||||
/merge-stream/2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
dev: true
|
||||
@ -8283,6 +8394,11 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/min-indent/1.0.1:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/minimatch/3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
@ -8295,6 +8411,15 @@ packages:
|
||||
brace-expansion: 1.1.11
|
||||
dev: true
|
||||
|
||||
/minimist-options/4.1.0:
|
||||
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
arrify: 1.0.1
|
||||
is-plain-obj: 1.1.0
|
||||
kind-of: 6.0.3
|
||||
dev: true
|
||||
|
||||
/minimist/1.2.6:
|
||||
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
|
||||
|
||||
@ -8513,6 +8638,15 @@ packages:
|
||||
/node-releases/2.0.6:
|
||||
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
|
||||
|
||||
/normalize-package-data/2.5.0:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
resolve: 1.22.1
|
||||
semver: 5.7.1
|
||||
validate-npm-package-license: 3.0.4
|
||||
dev: true
|
||||
|
||||
/normalize-path/2.1.1:
|
||||
resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -9424,6 +9558,11 @@ packages:
|
||||
/queue-microtask/1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
/quick-lru/4.0.1:
|
||||
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/quick-lru/5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
@ -9628,6 +9767,25 @@ packages:
|
||||
pify: 2.3.0
|
||||
dev: false
|
||||
|
||||
/read-pkg-up/7.0.1:
|
||||
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
read-pkg: 5.2.0
|
||||
type-fest: 0.8.1
|
||||
dev: true
|
||||
|
||||
/read-pkg/5.2.0:
|
||||
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@types/normalize-package-data': 2.4.1
|
||||
normalize-package-data: 2.5.0
|
||||
parse-json: 5.2.0
|
||||
type-fest: 0.6.0
|
||||
dev: true
|
||||
|
||||
/readable-stream/2.3.7:
|
||||
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
|
||||
dependencies:
|
||||
@ -9666,6 +9824,14 @@ packages:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
/redent/3.0.0:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
dev: true
|
||||
|
||||
/redis-commands/1.7.0:
|
||||
resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==}
|
||||
dev: false
|
||||
@ -10123,6 +10289,28 @@ packages:
|
||||
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/spdx-correct/3.1.1:
|
||||
resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
|
||||
dependencies:
|
||||
spdx-expression-parse: 3.0.1
|
||||
spdx-license-ids: 3.0.12
|
||||
dev: true
|
||||
|
||||
/spdx-exceptions/2.3.0:
|
||||
resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
|
||||
dev: true
|
||||
|
||||
/spdx-expression-parse/3.0.1:
|
||||
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
|
||||
dependencies:
|
||||
spdx-exceptions: 2.3.0
|
||||
spdx-license-ids: 3.0.12
|
||||
dev: true
|
||||
|
||||
/spdx-license-ids/3.0.12:
|
||||
resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==}
|
||||
dev: true
|
||||
|
||||
/split-string/3.1.0:
|
||||
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -10136,6 +10324,10 @@ packages:
|
||||
tslib: 2.4.0
|
||||
dev: true
|
||||
|
||||
/sprintf-js/1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
dev: true
|
||||
|
||||
/ssri/8.0.1:
|
||||
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -10292,6 +10484,13 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/strip-indent/3.0.0:
|
||||
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
min-indent: 1.0.1
|
||||
dev: true
|
||||
|
||||
/strip-json-comments/2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -10529,6 +10728,11 @@ packages:
|
||||
/tr46/0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
/trim-newlines/3.0.1:
|
||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/ts-log/2.2.4:
|
||||
resolution: {integrity: sha512-DEQrfv6l7IvN2jlzc/VTdZJYsWUnQNCsueYjMkC/iXoEoi5fNan6MjeDqkvhfzbmHgdz9UxDUluX3V5HdjTydQ==}
|
||||
dev: true
|
||||
@ -10764,6 +10968,11 @@ packages:
|
||||
prelude-ls: 1.2.1
|
||||
dev: true
|
||||
|
||||
/type-fest/0.13.1:
|
||||
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/type-fest/0.20.2:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -10774,6 +10983,16 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/type-fest/0.6.0:
|
||||
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/type-fest/0.8.1:
|
||||
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/typescript/4.7.4:
|
||||
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
@ -10972,6 +11191,13 @@ packages:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
dev: true
|
||||
|
||||
/validate-npm-package-license/3.0.4:
|
||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||
dependencies:
|
||||
spdx-correct: 3.1.1
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/value-or-promise/1.0.11:
|
||||
resolution: {integrity: sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import type { CustomField } from '@commerce/types/common'
|
||||
|
||||
interface Props {
|
||||
customFields: CustomField[]
|
||||
}
|
||||
|
||||
const ProductCustomFields: React.FC<Props> = ({ customFields }) => {
|
||||
return (
|
||||
<>
|
||||
{customFields.map((field) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex gap-2 border-b py-3 border-accent-2 border-dashed last:border-b-0"
|
||||
>
|
||||
<strong className="leading-7">{field.name}:</strong>
|
||||
<span>{field.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 as ProductCustomFields } from './ProductCustomFields'
|
@ -0,0 +1,29 @@
|
||||
import type { FC } from 'react'
|
||||
import type { Metafields } from '@commerce/types/common'
|
||||
import Text from '@components/ui/Text'
|
||||
|
||||
interface Props {
|
||||
metafields: Metafields
|
||||
/**
|
||||
* The namespace of the metafields to display.
|
||||
*/
|
||||
namespace: string
|
||||
}
|
||||
|
||||
const ProductMetafields: FC<Props> = ({ metafields, namespace }) => {
|
||||
return (
|
||||
<>
|
||||
{Object.values(metafields[namespace] ?? {}).map((field) => (
|
||||
<div
|
||||
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.valueHtml || field.value} className="!mx-0" />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductMetafields
|
1
site/components/product/ProductMetafields/index.ts
Normal file
1
site/components/product/ProductMetafields/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as ProductMetafields } from './ProductMetafields'
|
@ -10,6 +10,8 @@ import {
|
||||
SelectedOptions,
|
||||
} from '../helpers'
|
||||
import ErrorMessage from '@components/ui/ErrorMessage'
|
||||
import { ProductCustomFields } from '../ProductCustomFields'
|
||||
import { ProductMetafields } from '../ProductMetafields'
|
||||
|
||||
interface ProductSidebarProps {
|
||||
product: Product
|
||||
@ -62,10 +64,16 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
||||
className="pb-4 break-words w-full max-w-xl"
|
||||
html={product.descriptionHtml || product.description}
|
||||
/>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Rating value={4} />
|
||||
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
||||
</div>
|
||||
|
||||
{product.metafields?.reviews?.rating && (
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Rating value={product.metafields.reviews.rating.value} />
|
||||
<div className="text-accent-6 pr-1 font-medium text-sm">
|
||||
{product.metafields.reviews.count?.value ?? 0} reviews
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{error && <ErrorMessage error={error} className="my-5" />}
|
||||
{process.env.COMMERCE_CART_ENABLED && (
|
||||
@ -88,11 +96,27 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends.
|
||||
</Collapse>
|
||||
|
||||
<Collapse title="Details">
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
||||
to COVID-19.
|
||||
</Collapse>
|
||||
|
||||
{product.customFields && product.customFields?.length > 0 && (
|
||||
<Collapse title="Specifications">
|
||||
<ProductCustomFields customFields={product.customFields} />
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
{product.metafields?.my_fields && (
|
||||
<Collapse title="Specifications">
|
||||
<ProductMetafields
|
||||
metafields={product.metafields}
|
||||
namespace="my_fields"
|
||||
/>
|
||||
</Collapse>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -3,11 +3,22 @@ import type {
|
||||
GetStaticPropsContext,
|
||||
InferGetStaticPropsType,
|
||||
} from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import commerce from '@lib/api/commerce'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { Layout } from '@components/common'
|
||||
import { ProductView } from '@components/product'
|
||||
|
||||
// Used by the Shopify Example
|
||||
const withMetafields = [
|
||||
{ namespace: 'reviews', key: 'rating' },
|
||||
{ namespace: 'reviews', key: 'count' },
|
||||
{ namespace: 'my_fields', key: 'width' },
|
||||
{ namespace: 'my_fields', key: 'weight' },
|
||||
{ namespace: 'my_fields', key: 'length' },
|
||||
]
|
||||
|
||||
export async function getStaticProps({
|
||||
params,
|
||||
locale,
|
||||
@ -18,7 +29,10 @@ export async function getStaticProps({
|
||||
const pagesPromise = commerce.getAllPages({ config, preview })
|
||||
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
||||
const productPromise = commerce.getProduct({
|
||||
variables: { slug: params!.slug },
|
||||
variables: {
|
||||
slug: params!.slug,
|
||||
withMetafields,
|
||||
},
|
||||
config,
|
||||
preview,
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user