Merge branch 'custom-fields' of github.com:vercel/commerce into main

This commit is contained in:
Daniele Pancottini 2022-12-20 17:44:20 +01:00
commit 15faacc7de
24 changed files with 773 additions and 100 deletions

View File

@ -63,6 +63,7 @@
"react-dom": "^18" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"@manifoldco/swagger-to-ts": "^2.1.0",
"@taskr/clear": "^1.1.0", "@taskr/clear": "^1.1.0",
"@taskr/esnext": "^1.1.0", "@taskr/esnext": "^1.1.0",
"@taskr/watch": "^1.1.0", "@taskr/watch": "^1.1.0",
@ -74,6 +75,7 @@
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
"lint-staged": "^12.1.7", "lint-staged": "^12.1.7",
"next": "^12.0.8", "next": "^12.0.8",
"node-fetch": "^2.6.7",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -80,6 +80,15 @@ export const productInfoFragment = /* GraphQL */ `
} }
} }
} }
customFields {
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

@ -5,10 +5,12 @@ import type {
import type { import type {
GetCustomerWishlistOperation, GetCustomerWishlistOperation,
Wishlist, Wishlist,
WishlistItem,
} from '@vercel/commerce/types/wishlist' } from '@vercel/commerce/types/wishlist'
import type { RecursivePartial, RecursiveRequired } from '../utils/types' import type { RecursivePartial } from '../utils/types'
import { BigcommerceConfig, Provider } from '..' import { BigcommerceConfig, Provider } from '..'
import getAllProducts, { ProductEdge } from './get-all-products'
import type { Product } from '@vercel/commerce/types/product'
export default function getCustomerWishlistOperation({ export default function getCustomerWishlistOperation({
commerce, commerce,
@ -47,6 +49,12 @@ export default function getCustomerWishlistOperation({
const wishlist = data[0] const wishlist = data[0]
if (!wishlist) {
return {}
}
const items: WishlistItem[] = []
if (includeProducts && wishlist?.items?.length) { if (includeProducts && wishlist?.items?.length) {
const ids = wishlist.items const ids = wishlist.items
?.map((item) => (item?.productId ? String(item?.productId) : null)) ?.map((item) => (item?.productId ? String(item?.productId) : null))
@ -57,25 +65,36 @@ export default function getCustomerWishlistOperation({
variables: { first: 50, ids }, variables: { first: 50, ids },
config, config,
}) })
// Put the products in an object that we can use to get them by id // Put the products in an object that we can use to get them by id
const productsById = graphqlData.products.reduce<{ const productsById = graphqlData.products.reduce<{
[k: number]: ProductEdge [k: number]: Product
}>((prods, p) => { }>((prods, p) => {
prods[Number(p.id)] = p as any prods[Number(p.id)] = p as any
return prods return prods
}, {}) }, {})
// Populate the wishlist items with the graphql products // Populate the wishlist items with the graphql products
wishlist.items.forEach((item) => { wishlist.items.forEach((item) => {
const product = item && productsById[Number(item.productId)] const product = item && productsById[Number(item.productId)]
if (item && product) { if (item && product) {
// @ts-ignore Fix this type when the wishlist type is properly defined items.push({
item.product = product 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 return getCustomerWishlist

View File

@ -100,7 +100,7 @@ export default function getAllProductPathsOperation({
const variables: GetProductQueryVariables = { const variables: GetProductQueryVariables = {
locale, locale,
hasLocale: !!locale, hasLocale: !!locale,
path: slug ? `/${slug}` : vars.path!, path: `/${slug}`,
} }
const { data } = await config.fetch<GetProductQuery>(query, { variables }) const { data } = await config.fetch<GetProductQuery>(query, { variables })
const product = data.site?.route?.node const product = data.site?.route?.node

View File

@ -1,10 +1,12 @@
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 } from '@vercel/commerce/types/product'
import type { CustomField } from '@vercel/commerce/types/common'
import type { Cart, LineItem } from '@vercel/commerce/types/cart' import type { Cart, LineItem } from '@vercel/commerce/types/cart'
import type { Category, Brand } from '@vercel/commerce/types/site' import type { Category, Brand } from '@vercel/commerce/types/site'
import type { BigcommerceCart, BCCategory, BCBrand } from '../types' import type { BigcommerceCart, BCCategory, BCBrand } from '../types'
import type { ProductNode } from '../api/operations/get-all-products' import type { ProductNode } from '../api/operations/get-all-products'
import type { definitions } from '../api/definitions/store-content' import type { definitions } from '../api/definitions/store-content'
import type { CustomFieldEdge } from '../../schema'
import getSlug from './get-slug' 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 { const {
entityId: id, entityId: id,
productOptions, productOptions,
customFields,
prices, prices,
path, path,
images, images,
@ -52,6 +64,7 @@ export function normalizeProduct(productNode: ProductNode): Product {
}) })
) || [], ) || [],
options: productOptions?.edges?.map(normalizeProductOption) || [], options: productOptions?.edges?.map(normalizeProductOption) || [],
customFields: customFields?.edges?.map(normalizeCustomFieldsValue) || [],
slug: path?.replace(/^\/+|\/+$/g, ''), slug: path?.replace(/^\/+|\/+$/g, ''),
price: { price: {
value: prices?.price.value, value: prices?.price.value,
@ -137,3 +150,15 @@ export function normalizeBrand(brand: BCBrand): Brand {
path: `/${slug}`, path: `/${slug}`,
} }
} }
function normalizeCustomFieldsValue(field: CustomFieldEdge): CustomField {
const {
node: { entityId, name, value },
} = field
return {
id: String(entityId),
name,
value,
}
}

View File

@ -34,3 +34,67 @@ export interface Image {
*/ */
height?: number 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
}
}

View File

@ -1,4 +1,4 @@
import { Image } from './common' import { CustomField, Image, Metafields } from './common'
export interface ProductPrice { export interface ProductPrice {
/** /**
@ -126,6 +126,33 @@ export interface Product {
* List of images associated with the product. * List of images associated with the product.
*/ */
images: Image[] 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: '&#9733;&#9733;&#9733;&#9733;&#9734;',
* type: 'integer',
* name: 'Rating',
* }
* }
*/
metafields?: Metafields
/** /**
* List of the products variants. * List of the products variants.
*/ */
@ -204,7 +231,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 }
@ -219,7 +245,24 @@ export type GetAllProductsOperation = {
} }
} }
export type MetafieldsIdentifiers = 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: {
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
}
} }

View File

@ -1,30 +1,24 @@
import type { OperationContext } from '@vercel/commerce/api/operations' 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 type { Provider, SaleorConfig } from '..'
import { normalizeProduct } from '../../utils'
import * as Query from '../../utils/queries' import * as Query from '../../utils/queries'
type Variables = {
slug: string
}
type ReturnType = {
product: any
}
export default function getProductOperation({ export default function getProductOperation({
commerce, commerce,
}: OperationContext<Provider>) { }: OperationContext<Provider>) {
async function getProduct({ async function getProduct<T extends GetProductOperation>({
query = Query.ProductOneBySlug, query = Query.ProductOneBySlug,
variables, variables,
config: cfg, config: cfg,
}: { }: {
query?: string query?: string
variables: Variables variables: T['variables']
config?: Partial<SaleorConfig> config?: Partial<SaleorConfig>
preview?: boolean preview?: boolean
}): Promise<ReturnType> { }): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(cfg) const { fetch, locale } = commerce.getConfig(cfg)
const { data } = await fetch( const { data } = await fetch(
@ -37,9 +31,9 @@ export default function getProductOperation({
} }
) )
return { return data && data.product
product: data && data.product ? normalizeProduct(data.product) : null, ? { product: normalizeProduct(data.product) }
} : {}
} }
return getProduct return getProduct

View File

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

View File

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

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

View File

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

View 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' ? '&#10003;' : '&#10005;'
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 ? '&#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" 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(' ')

View File

@ -1,7 +1,9 @@
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 } from '@vercel/commerce/types/product'
import type { Metafield } from '@vercel/commerce/types/common'
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,14 +17,12 @@ import type {
Page as ShopifyPage, Page as ShopifyPage,
PageEdge, PageEdge,
Collection, Collection,
MetafieldConnection,
MediaConnection,
Model3d,
Metafield,
Maybe, Maybe,
Metafield as ShopifyMetafield,
} 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 {
@ -92,7 +92,6 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
name, name,
values: [value], values: [value],
}) })
return options return options
}), }),
} }
@ -100,35 +99,23 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
) )
} }
const normalizeProductMedia = ({ edges }: MediaConnection) => { export function normalizeProduct(
return edges {
.filter(({ node }) => Object.keys(node).length !== 0) id,
.map(({ node }) => { title: name,
return { vendor,
sources: (node as Model3d).sources.map(({ format, url }) => { images,
return { variants,
format: format, description,
url: url, descriptionHtml,
} handle,
}), priceRange,
} options,
}) metafields,
} ...rest
}: ShopifyProduct,
export function normalizeProduct({ locale?: string
id, ): Product {
title: name,
vendor,
images,
variants,
description,
descriptionHtml,
handle,
priceRange,
options,
metafields,
...rest
}: ShopifyProduct): Product {
return { return {
id, id,
name, 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 .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<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 { export function normalizeCart(checkout: Checkout): Cart {
return { return {
id: checkout.id, id: checkout.id,
@ -217,3 +240,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(' &#8226; ')
: value
}
return getMetafieldValue(type, value, locale)
}

View File

@ -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,15 +27,7 @@ const getProductQuery = /* GraphQL */ `
currencyCode currencyCode
} }
} }
metafields(first: 30) { variants(first: 25) {
edges {
node {
key
value
}
}
}
variants(first: 250) {
pageInfo { pageInfo {
hasNextPage hasNextPage
hasPreviousPage hasPreviousPage
@ -59,7 +54,7 @@ const getProductQuery = /* GraphQL */ `
} }
} }
} }
images(first: 250) { images(first: 25) {
pageInfo { pageInfo {
hasNextPage hasNextPage
hasPreviousPage hasPreviousPage
@ -88,6 +83,12 @@ const getProductQuery = /* GraphQL */ `
} }
} }
} }
metafields(identifiers: $withMetafields) {
key
value
namespace
description
type
} }
} }
} }

View File

@ -1,3 +1,5 @@
import type { GetProductOperation } from '@vercel/commerce/types/product'
import { normalizeProduct } from '../../utils' import { normalizeProduct } from '../../utils'
import { Product } from '@vercel/commerce/types/product' import { Product } from '@vercel/commerce/types/product'
@ -7,15 +9,15 @@ import { Provider, SwellConfig } from '../'
export default function getProductOperation({ export default function getProductOperation({
commerce, commerce,
}: OperationContext<Provider>) { }: OperationContext<Provider>) {
async function getProduct({ async function getProduct<T extends GetProductOperation>({
variables, variables,
config: cfg, config: cfg,
}: { }: {
query?: string query?: string
variables: { slug: string } variables: T['variables']
config?: Partial<SwellConfig> config?: Partial<SwellConfig>
preview?: boolean preview?: boolean
}): Promise<Product | {} | any> { }): Promise<T['data']> {
const config = commerce.getConfig(cfg) const config = commerce.getConfig(cfg)
const product = await config.fetch('products', 'get', [variables.slug]) const product = await config.fetch('products', 'get', [variables.slug])
@ -24,9 +26,7 @@ export default function getProductOperation({
product.variants = product.variants?.results product.variants = product.variants?.results
} }
return { return product ? { product: normalizeProduct(product) } : {}
product: product ? normalizeProduct(product) : null,
}
} }
return getProduct return getProduct

View File

@ -1,22 +1,26 @@
import { Product } from '@vercel/commerce/types/product' import type {
import { OperationContext } from '@vercel/commerce/api/operations' Product,
import { Provider, VendureConfig } from '../' GetProductOperation,
import { GetProductQuery } from '../../../schema' } 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' import { getProductQuery } from '../../utils/queries/get-product-query'
export default function getProductOperation({ export default function getProductOperation({
commerce, commerce,
}: OperationContext<Provider>) { }: OperationContext<Provider>) {
async function getProduct({ async function getProduct<T extends GetProductOperation>({
query = getProductQuery, query = getProductQuery,
variables, variables,
config: cfg, config: cfg,
}: { }: {
query?: string query?: string
variables: { slug: string } variables: T['variables']
config?: Partial<VendureConfig> config?: Partial<VendureConfig>
preview?: boolean preview?: boolean
}): Promise<Product | {} | any> { }): Promise<T['data']> {
const config = commerce.getConfig(cfg) const config = commerce.getConfig(cfg)
const locale = config.locale const locale = config.locale

226
pnpm-lock.yaml generated
View File

@ -15,6 +15,7 @@ importers:
packages/bigcommerce: packages/bigcommerce:
specifiers: specifiers:
'@cfworker/uuid': ^1.12.4 '@cfworker/uuid': ^1.12.4
'@manifoldco/swagger-to-ts': ^2.1.0
'@taskr/clear': ^1.1.0 '@taskr/clear': ^1.1.0
'@taskr/esnext': ^1.1.0 '@taskr/esnext': ^1.1.0
'@taskr/watch': ^1.1.0 '@taskr/watch': ^1.1.0
@ -33,6 +34,7 @@ importers:
lint-staged: ^12.1.7 lint-staged: ^12.1.7
lodash.debounce: ^4.0.8 lodash.debounce: ^4.0.8
next: ^12.0.8 next: ^12.0.8
node-fetch: ^2.6.7
prettier: ^2.5.1 prettier: ^2.5.1
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
@ -51,6 +53,7 @@ importers:
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
uuidv4: 6.2.13 uuidv4: 6.2.13
devDependencies: devDependencies:
'@manifoldco/swagger-to-ts': 2.1.0
'@taskr/clear': 1.1.0 '@taskr/clear': 1.1.0
'@taskr/esnext': 1.1.0 '@taskr/esnext': 1.1.0
'@taskr/watch': 1.1.0 '@taskr/watch': 1.1.0
@ -62,6 +65,7 @@ importers:
'@types/react': 18.0.20 '@types/react': 18.0.20
lint-staged: 12.5.0 lint-staged: 12.5.0
next: 12.3.0_biqbaboplfbrettd7655fr4n2y next: 12.3.0_biqbaboplfbrettd7655fr4n2y
node-fetch: 2.6.7
prettier: 2.7.1 prettier: 2.7.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
@ -3493,6 +3497,7 @@ packages:
- supports-color - supports-color
dev: false dev: false
<<<<<<< HEAD
/@motionone/animation/10.15.1: /@motionone/animation/10.15.1:
resolution: {integrity: sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==} resolution: {integrity: sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==}
dependencies: dependencies:
@ -3539,6 +3544,19 @@ packages:
hey-listen: 1.0.8 hey-listen: 1.0.8
tslib: 2.4.0 tslib: 2.4.0
dev: false 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: /@next/bundle-analyzer/12.3.0:
resolution: {integrity: sha512-hzRLHIrtwOiGEku9rmG7qZk+OQhnqQOL+ycl2XrjBaztBN/xaqnjoG4+HEf9L7ELN943BR+K/ZlaF2OEgbGm+Q==} resolution: {integrity: sha512-hzRLHIrtwOiGEku9rmG7qZk+OQhnqQOL+ycl2XrjBaztBN/xaqnjoG4+HEf9L7ELN943BR+K/ZlaF2OEgbGm+Q==}
@ -4431,6 +4449,10 @@ packages:
/@types/lodash/4.14.185: /@types/lodash/4.14.185:
resolution: {integrity: sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==} resolution: {integrity: sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==}
/@types/minimist/1.2.2:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
/@types/node-fetch/2.6.2: /@types/node-fetch/2.6.2:
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
dependencies: dependencies:
@ -4446,6 +4468,10 @@ packages:
resolution: {integrity: sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==} resolution: {integrity: sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==}
dev: true dev: true
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
dev: true
/@types/parse-json/4.0.0: /@types/parse-json/4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
@ -4768,6 +4794,12 @@ packages:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
dev: false dev: false
/argparse/1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
sprintf-js: 1.0.3
dev: true
/argparse/2.0.1: /argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true dev: true
@ -4870,6 +4902,11 @@ packages:
es-shim-unscopables: 1.0.0 es-shim-unscopables: 1.0.0
dev: true dev: true
/arrify/1.0.1:
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
engines: {node: '>=0.10.0'}
dev: true
/asap/2.0.6: /asap/2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
dev: true dev: true
@ -5204,6 +5241,15 @@ packages:
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: false 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: /camelcase/5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -5728,6 +5774,14 @@ packages:
supports-color: 9.2.3 supports-color: 9.2.3
dev: true 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: /decamelize/1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -6285,6 +6339,12 @@ packages:
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
dev: true 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: /esquery/1.4.0:
resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
@ -6971,6 +7031,11 @@ packages:
duplexer: 0.1.2 duplexer: 0.1.2
dev: true 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: /has-ansi/2.0.0:
resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -7052,6 +7117,7 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
dev: true dev: true
<<<<<<< HEAD
/hey-listen/1.0.8: /hey-listen/1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
dev: false dev: false
@ -7061,6 +7127,11 @@ packages:
dependencies: dependencies:
react-is: 16.13.1 react-is: 16.13.1
dev: false dev: false
=======
/hosted-git-info/2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true
>>>>>>> 1ba9d3bd6e79da1f0b05df2f1c0c1ca3632b6c16
/http-cache-semantics/4.1.0: /http-cache-semantics/4.1.0:
resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
@ -7512,6 +7583,11 @@ packages:
symbol-observable: 1.2.0 symbol-observable: 1.2.0
dev: true 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: /is-plain-object/2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -7660,6 +7736,14 @@ packages:
/js-tokens/4.0.0: /js-tokens/4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 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: /js-yaml/4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
@ -8154,6 +8238,16 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true 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: /map-visit/1.0.0:
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -8172,6 +8266,23 @@ packages:
resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==} resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==}
dev: true 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: /merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true dev: true
@ -8283,6 +8394,11 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/min-indent/1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
dev: true
/minimatch/3.1.2: /minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies: dependencies:
@ -8295,6 +8411,15 @@ packages:
brace-expansion: 1.1.11 brace-expansion: 1.1.11
dev: true 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: /minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
@ -8513,6 +8638,15 @@ packages:
/node-releases/2.0.6: /node-releases/2.0.6:
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} 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: /normalize-path/2.1.1:
resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9424,6 +9558,11 @@ packages:
/queue-microtask/1.2.3: /queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 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: /quick-lru/5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -9628,6 +9767,25 @@ packages:
pify: 2.3.0 pify: 2.3.0
dev: false 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: /readable-stream/2.3.7:
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
dependencies: dependencies:
@ -9666,6 +9824,14 @@ packages:
dependencies: dependencies:
picomatch: 2.3.1 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: /redis-commands/1.7.0:
resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==}
dev: false dev: false
@ -10123,6 +10289,28 @@ packages:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'} 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: /split-string/3.1.0:
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -10136,6 +10324,10 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
dev: true dev: true
/sprintf-js/1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: true
/ssri/8.0.1: /ssri/8.0.1:
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -10292,6 +10484,13 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true 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: /strip-json-comments/2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -10529,6 +10728,11 @@ packages:
/tr46/0.0.3: /tr46/0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 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: /ts-log/2.2.4:
resolution: {integrity: sha512-DEQrfv6l7IvN2jlzc/VTdZJYsWUnQNCsueYjMkC/iXoEoi5fNan6MjeDqkvhfzbmHgdz9UxDUluX3V5HdjTydQ==} resolution: {integrity: sha512-DEQrfv6l7IvN2jlzc/VTdZJYsWUnQNCsueYjMkC/iXoEoi5fNan6MjeDqkvhfzbmHgdz9UxDUluX3V5HdjTydQ==}
dev: true dev: true
@ -10764,6 +10968,11 @@ packages:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
dev: true dev: true
/type-fest/0.13.1:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
engines: {node: '>=10'}
dev: true
/type-fest/0.20.2: /type-fest/0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -10774,6 +10983,16 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true 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: /typescript/4.7.4:
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
@ -10972,6 +11191,13 @@ packages:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true 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: /value-or-promise/1.0.11:
resolution: {integrity: sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==} resolution: {integrity: sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==}
engines: {node: '>=12'} engines: {node: '>=12'}

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,8 @@ import {
SelectedOptions, SelectedOptions,
} from '../helpers' } from '../helpers'
import ErrorMessage from '@components/ui/ErrorMessage' import ErrorMessage from '@components/ui/ErrorMessage'
import { ProductCustomFields } from '../ProductCustomFields'
import { ProductMetafields } from '../ProductMetafields'
interface ProductSidebarProps { interface ProductSidebarProps {
product: Product product: Product
@ -62,10 +64,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}
/> />
<div className="flex flex-row justify-between items-center">
<Rating value={4} /> {product.metafields?.reviews?.rating && (
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div> <div className="flex flex-row justify-between items-center">
</div> <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> <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 && (
@ -88,11 +96,27 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
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.
</Collapse> </Collapse>
<Collapse title="Details"> <Collapse title="Details">
This is a limited edition production run. Printing starts when the This is a limited edition production run. Printing starts when the
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>
{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>
</div> </div>
) )

View File

@ -3,11 +3,22 @@ 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: 'reviews', key: 'count' },
{ namespace: 'my_fields', key: 'width' },
{ namespace: 'my_fields', key: 'weight' },
{ namespace: 'my_fields', key: 'length' },
]
export async function getStaticProps({ export async function getStaticProps({
params, params,
locale, locale,
@ -18,7 +29,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,
}) })