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
24 changed files with 773 additions and 100 deletions

View File

@@ -55,7 +55,7 @@ export default function getProductOperation({
return {
...(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 }
} 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

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

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

View File

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