mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Updates
This commit is contained in:
parent
79d320501d
commit
3bcf9d1d53
@ -1,3 +1,6 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
import type { CookieAttributes } from 'js-cookie'
|
||||||
import type { FetcherOptions } from '@vercel/commerce/utils/types'
|
import type { FetcherOptions } from '@vercel/commerce/utils/types'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -7,7 +10,6 @@ import type {
|
|||||||
CustomerAccessTokenCreateInput,
|
CustomerAccessTokenCreateInput,
|
||||||
} from '../../schema'
|
} from '../../schema'
|
||||||
|
|
||||||
import { setCustomerToken } from './helpers'
|
|
||||||
import { throwUserErrors } from './throw-user-errors'
|
import { throwUserErrors } from './throw-user-errors'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -15,6 +17,27 @@ import {
|
|||||||
customerAccessTokenCreateMutation,
|
customerAccessTokenCreateMutation,
|
||||||
} from './mutations'
|
} from './mutations'
|
||||||
|
|
||||||
|
import { SHOPIFY_COOKIE_EXPIRE, SHOPIFY_CUSTOMER_TOKEN_COOKIE } from '../const'
|
||||||
|
|
||||||
|
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
|
||||||
|
|
||||||
|
export const setCustomerToken = (
|
||||||
|
token: string | null,
|
||||||
|
options?: CookieAttributes
|
||||||
|
) => {
|
||||||
|
if (!token) {
|
||||||
|
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
|
||||||
|
} else {
|
||||||
|
Cookies.set(
|
||||||
|
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||||
|
token,
|
||||||
|
options ?? {
|
||||||
|
expires: SHOPIFY_COOKIE_EXPIRE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const handleLogin = (data: any) => {
|
export const handleLogin = (data: any) => {
|
||||||
const response = data.customerAccessTokenCreate
|
const response = data.customerAccessTokenCreate
|
||||||
throwUserErrors(response?.customerUserErrors)
|
throwUserErrors(response?.customerUserErrors)
|
65
packages/shopify/src/utils/cart.ts
Normal file
65
packages/shopify/src/utils/cart.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { FetcherOptions } from '@vercel/commerce/utils/types'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CartLineInput,
|
||||||
|
CartCreateMutation,
|
||||||
|
CartDetailsFragment,
|
||||||
|
CartCreateMutationVariables,
|
||||||
|
} from '../../schema'
|
||||||
|
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import { throwUserErrors } from './throw-user-errors'
|
||||||
|
import { cartCreateMutation } from './mutations/cart-mutations'
|
||||||
|
|
||||||
|
import {
|
||||||
|
SHOPIFY_CART_ID_COOKIE,
|
||||||
|
SHOPIFY_CART_URL_COOKIE,
|
||||||
|
SHOPIFY_COOKIE_EXPIRE,
|
||||||
|
} from '../const'
|
||||||
|
|
||||||
|
export const setCartUrlCookie = (cartUrl: string) => {
|
||||||
|
if (cartUrl) {
|
||||||
|
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
|
||||||
|
if (oldCookie !== cartUrl) {
|
||||||
|
Cookies.set(SHOPIFY_CART_URL_COOKIE, cartUrl, {
|
||||||
|
expires: SHOPIFY_COOKIE_EXPIRE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCartId = (id?: string) => {
|
||||||
|
return id || Cookies.get(SHOPIFY_CART_ID_COOKIE)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cartCreate = async (
|
||||||
|
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
|
||||||
|
lines?: Array<CartLineInput> | CartLineInput
|
||||||
|
): Promise<CartDetailsFragment | null | undefined> => {
|
||||||
|
const { cartCreate } = await fetch<
|
||||||
|
CartCreateMutation,
|
||||||
|
CartCreateMutationVariables
|
||||||
|
>({
|
||||||
|
query: cartCreateMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
lines,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const cart = cartCreate?.cart
|
||||||
|
|
||||||
|
throwUserErrors(cartCreate?.userErrors)
|
||||||
|
|
||||||
|
if (cart?.id) {
|
||||||
|
const options = {
|
||||||
|
expires: SHOPIFY_COOKIE_EXPIRE,
|
||||||
|
}
|
||||||
|
Cookies.set(SHOPIFY_CART_ID_COOKIE, cart.id, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCartUrlCookie(cart?.checkoutUrl)
|
||||||
|
|
||||||
|
return cart
|
||||||
|
}
|
132
packages/shopify/src/utils/collections.ts
Normal file
132
packages/shopify/src/utils/collections.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import type {
|
||||||
|
CollectionEdge,
|
||||||
|
GetAllProductVendorsQuery,
|
||||||
|
GetAllProductVendorsQueryVariables,
|
||||||
|
} from '../../schema'
|
||||||
|
|
||||||
|
import type { Category } from '../types/site'
|
||||||
|
import type { SearchProductsBody } from '../types/product'
|
||||||
|
|
||||||
|
import { ShopifyConfig } from '../api'
|
||||||
|
import { normalizeCategory } from './normalize'
|
||||||
|
import { getAllProductVendors, getSiteCollectionsQuery } from './queries'
|
||||||
|
|
||||||
|
export const getCategories = async ({
|
||||||
|
fetch,
|
||||||
|
locale,
|
||||||
|
}: ShopifyConfig): Promise<Category[]> => {
|
||||||
|
const { data } = await fetch(
|
||||||
|
getSiteCollectionsQuery,
|
||||||
|
{
|
||||||
|
variables: {
|
||||||
|
first: 250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(locale && {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': locale,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
data.collections?.edges?.map(({ node }: CollectionEdge) =>
|
||||||
|
normalizeCategory(node)
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Brand = {
|
||||||
|
entityId: string
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BrandEdge = {
|
||||||
|
node: Brand
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Brands = BrandEdge[]
|
||||||
|
|
||||||
|
export const getBrands = async (
|
||||||
|
config: ShopifyConfig
|
||||||
|
): Promise<BrandEdge[]> => {
|
||||||
|
const { data } = await config.fetch<
|
||||||
|
GetAllProductVendorsQuery,
|
||||||
|
GetAllProductVendorsQueryVariables
|
||||||
|
>(getAllProductVendors, {
|
||||||
|
variables: {
|
||||||
|
first: 250,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let vendorsStrings = data.products.edges.map(({ node: { vendor } }) => vendor)
|
||||||
|
|
||||||
|
return [...new Set(vendorsStrings)].map((v) => {
|
||||||
|
const id = v.replace(/\s+/g, '-').toLowerCase()
|
||||||
|
return {
|
||||||
|
node: {
|
||||||
|
entityId: id,
|
||||||
|
name: v,
|
||||||
|
path: `brands/${id}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSortVariables = (
|
||||||
|
sort?: string,
|
||||||
|
isCategory: boolean = false
|
||||||
|
) => {
|
||||||
|
switch (sort) {
|
||||||
|
case 'price-asc':
|
||||||
|
return {
|
||||||
|
sortKey: 'PRICE',
|
||||||
|
reverse: false,
|
||||||
|
}
|
||||||
|
case 'price-desc':
|
||||||
|
return {
|
||||||
|
sortKey: 'PRICE',
|
||||||
|
reverse: true,
|
||||||
|
}
|
||||||
|
case 'trending-desc':
|
||||||
|
return {
|
||||||
|
sortKey: 'BEST_SELLING',
|
||||||
|
reverse: false,
|
||||||
|
}
|
||||||
|
case 'latest-desc':
|
||||||
|
return {
|
||||||
|
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
|
||||||
|
reverse: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSearchVariables = ({
|
||||||
|
brandId,
|
||||||
|
search,
|
||||||
|
categoryId,
|
||||||
|
sort,
|
||||||
|
locale,
|
||||||
|
}: SearchProductsBody) => {
|
||||||
|
let query = ''
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
query += `product_type:${search} OR title:${search} OR tag:${search} `
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brandId) {
|
||||||
|
query += `${search ? 'AND ' : ''}vendor:${brandId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
categoryId,
|
||||||
|
query,
|
||||||
|
...getSortVariables(sort, !!categoryId),
|
||||||
|
...(locale && {
|
||||||
|
locale,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
@ -1,303 +0,0 @@
|
|||||||
import Cookies, { CookieAttributes } from 'js-cookie'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CartLineInput,
|
|
||||||
CollectionEdge,
|
|
||||||
CartCreateMutation,
|
|
||||||
CartDetailsFragment,
|
|
||||||
CartCreateMutationVariables,
|
|
||||||
GetAllProductVendorsQuery,
|
|
||||||
GetAllProductVendorsQueryVariables,
|
|
||||||
} from '../../schema'
|
|
||||||
|
|
||||||
import type { Category } from '../types/site'
|
|
||||||
import type { MetafieldType, SearchProductsBody } from '../types/product'
|
|
||||||
import type { FetcherOptions } from '@vercel/commerce/utils/types'
|
|
||||||
|
|
||||||
import {
|
|
||||||
SHOPIFY_CART_URL_COOKIE,
|
|
||||||
SHOPIFY_COOKIE_EXPIRE,
|
|
||||||
SHOPIFY_CART_ID_COOKIE,
|
|
||||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
|
||||||
} from '../const'
|
|
||||||
|
|
||||||
import { ShopifyConfig } from '../api'
|
|
||||||
import { normalizeCategory } from './normalize'
|
|
||||||
import { throwUserErrors } from './throw-user-errors'
|
|
||||||
import { cartCreateMutation } from './mutations/cart-mutations'
|
|
||||||
import { getAllProductVendors, getSiteCollectionsQuery } from './queries'
|
|
||||||
|
|
||||||
export const cartCreate = async (
|
|
||||||
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
|
|
||||||
lines?: Array<CartLineInput> | CartLineInput
|
|
||||||
): Promise<CartDetailsFragment | null | undefined> => {
|
|
||||||
const { cartCreate } = await fetch<
|
|
||||||
CartCreateMutation,
|
|
||||||
CartCreateMutationVariables
|
|
||||||
>({
|
|
||||||
query: cartCreateMutation,
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
lines,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const cart = cartCreate?.cart
|
|
||||||
|
|
||||||
throwUserErrors(cartCreate?.userErrors)
|
|
||||||
|
|
||||||
if (cart?.id) {
|
|
||||||
const options = {
|
|
||||||
expires: SHOPIFY_COOKIE_EXPIRE,
|
|
||||||
}
|
|
||||||
Cookies.set(SHOPIFY_CART_ID_COOKIE, cart.id, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
setCartUrlCookie(cart?.checkoutUrl)
|
|
||||||
|
|
||||||
return cart
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCategories = async ({
|
|
||||||
fetch,
|
|
||||||
locale,
|
|
||||||
}: ShopifyConfig): Promise<Category[]> => {
|
|
||||||
const { data } = await fetch(
|
|
||||||
getSiteCollectionsQuery,
|
|
||||||
{
|
|
||||||
variables: {
|
|
||||||
first: 250,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...(locale && {
|
|
||||||
headers: {
|
|
||||||
'Accept-Language': locale,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
data.collections?.edges?.map(({ node }: CollectionEdge) =>
|
|
||||||
normalizeCategory(node)
|
|
||||||
) ?? []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Brand = {
|
|
||||||
entityId: string
|
|
||||||
name: string
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BrandEdge = {
|
|
||||||
node: Brand
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Brands = BrandEdge[]
|
|
||||||
|
|
||||||
export const getBrands = async (
|
|
||||||
config: ShopifyConfig
|
|
||||||
): Promise<BrandEdge[]> => {
|
|
||||||
const { data } = await config.fetch<
|
|
||||||
GetAllProductVendorsQuery,
|
|
||||||
GetAllProductVendorsQueryVariables
|
|
||||||
>(getAllProductVendors, {
|
|
||||||
variables: {
|
|
||||||
first: 250,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let vendorsStrings = data.products.edges.map(({ node: { vendor } }) => vendor)
|
|
||||||
|
|
||||||
return [...new Set(vendorsStrings)].map((v) => {
|
|
||||||
const id = v.replace(/\s+/g, '-').toLowerCase()
|
|
||||||
return {
|
|
||||||
node: {
|
|
||||||
entityId: id,
|
|
||||||
name: v,
|
|
||||||
path: `brands/${id}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setCartUrlCookie = (cartUrl: string) => {
|
|
||||||
if (cartUrl) {
|
|
||||||
const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE)
|
|
||||||
if (oldCookie !== cartUrl) {
|
|
||||||
Cookies.set(SHOPIFY_CART_URL_COOKIE, cartUrl, {
|
|
||||||
expires: SHOPIFY_COOKIE_EXPIRE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCartId = (id?: string) => {
|
|
||||||
return id || Cookies.get(SHOPIFY_CART_ID_COOKIE)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSortVariables = (
|
|
||||||
sort?: string,
|
|
||||||
isCategory: boolean = false
|
|
||||||
) => {
|
|
||||||
switch (sort) {
|
|
||||||
case 'price-asc':
|
|
||||||
return {
|
|
||||||
sortKey: 'PRICE',
|
|
||||||
reverse: false,
|
|
||||||
}
|
|
||||||
case 'price-desc':
|
|
||||||
return {
|
|
||||||
sortKey: 'PRICE',
|
|
||||||
reverse: true,
|
|
||||||
}
|
|
||||||
case 'trending-desc':
|
|
||||||
return {
|
|
||||||
sortKey: 'BEST_SELLING',
|
|
||||||
reverse: false,
|
|
||||||
}
|
|
||||||
case 'latest-desc':
|
|
||||||
return {
|
|
||||||
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
|
|
||||||
reverse: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSearchVariables = ({
|
|
||||||
brandId,
|
|
||||||
search,
|
|
||||||
categoryId,
|
|
||||||
sort,
|
|
||||||
locale,
|
|
||||||
}: SearchProductsBody) => {
|
|
||||||
let query = ''
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
query += `product_type:${search} OR title:${search} OR tag:${search} `
|
|
||||||
}
|
|
||||||
|
|
||||||
if (brandId) {
|
|
||||||
query += `${search ? 'AND ' : ''}vendor:${brandId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
categoryId,
|
|
||||||
query,
|
|
||||||
...getSortVariables(sort, !!categoryId),
|
|
||||||
...(locale && {
|
|
||||||
locale,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
|
|
||||||
|
|
||||||
export const setCustomerToken = (
|
|
||||||
token: string | null,
|
|
||||||
options?: CookieAttributes
|
|
||||||
) => {
|
|
||||||
if (!token) {
|
|
||||||
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
|
|
||||||
} else {
|
|
||||||
Cookies.set(
|
|
||||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
|
||||||
token,
|
|
||||||
options ?? {
|
|
||||||
expires: SHOPIFY_COOKIE_EXPIRE,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseJson = (value: string): any => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(value)
|
|
||||||
} catch (e) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unitConversion: Record<string, string> = {
|
|
||||||
MILLIMETERS: 'millimeter',
|
|
||||||
CENTIMETERS: 'centimeter',
|
|
||||||
METERS: 'meter',
|
|
||||||
|
|
||||||
MILLILITERS: 'milliliter',
|
|
||||||
LITERS: 'liter',
|
|
||||||
FLUID_OUNCES: 'fluid-ounce',
|
|
||||||
IMPERIAL_FLUID_OUNCES: 'fluid-ounce',
|
|
||||||
GALLONS: 'gallon',
|
|
||||||
|
|
||||||
KILOGRAMS: 'kilogram',
|
|
||||||
GRAMS: 'gram',
|
|
||||||
OUNCES: 'ounce',
|
|
||||||
POUNDS: 'pound',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMeasurment = (input: string, locale: string = 'en-US') => {
|
|
||||||
try {
|
|
||||||
let { unit, value } = JSON.parse(input)
|
|
||||||
|
|
||||||
return new Intl.NumberFormat(locale, {
|
|
||||||
unit: unitConversion[unit],
|
|
||||||
style: 'unit',
|
|
||||||
}).format(parseFloat(value))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMetafieldValue = (
|
|
||||||
type: MetafieldType,
|
|
||||||
value: string,
|
|
||||||
locale: string = 'en-US'
|
|
||||||
) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'boolean':
|
|
||||||
return value === 'true' ? '✓' : 'No'
|
|
||||||
case 'number_integer':
|
|
||||||
return parseInt(value).toLocaleString(locale)
|
|
||||||
case 'number_decimal':
|
|
||||||
return parseFloat(value).toLocaleString(locale)
|
|
||||||
case 'date':
|
|
||||||
return Intl.DateTimeFormat(locale, {
|
|
||||||
dateStyle: 'medium',
|
|
||||||
}).format(new Date(value))
|
|
||||||
case 'date_time':
|
|
||||||
return Intl.DateTimeFormat(locale, {
|
|
||||||
dateStyle: 'medium',
|
|
||||||
timeStyle: 'long',
|
|
||||||
}).format(new Date(value))
|
|
||||||
case 'dimension':
|
|
||||||
case 'volume':
|
|
||||||
case 'weight':
|
|
||||||
return getMeasurment(value, locale)
|
|
||||||
case 'rating':
|
|
||||||
const { scale_max, value: val } = JSON.parse(value)
|
|
||||||
return Array.from({ length: scale_max }, (_, i) =>
|
|
||||||
i <= val - 1 ? '★' : '☆'
|
|
||||||
).join('')
|
|
||||||
case 'color':
|
|
||||||
return `<figure style="background-color: ${value}; width: 1rem; height:1rem; display:block; margin-top: 2px; border-radius: 100%;"/>`
|
|
||||||
case 'url':
|
|
||||||
return `<a href="${value}" target="_blank">${value}</a>`
|
|
||||||
case 'multi_line_text_field':
|
|
||||||
return value
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => `<p>${line}</p>`)
|
|
||||||
.join('')
|
|
||||||
case 'json':
|
|
||||||
case 'single_line_text_field':
|
|
||||||
case 'product_reference':
|
|
||||||
case 'page_reference':
|
|
||||||
case 'variant_reference':
|
|
||||||
case 'file_reference':
|
|
||||||
default:
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,9 +5,12 @@ export {
|
|||||||
handleLogin,
|
handleLogin,
|
||||||
handleAutomaticLogin,
|
handleAutomaticLogin,
|
||||||
handleAccountActivation,
|
handleAccountActivation,
|
||||||
} from './handle-login'
|
} from './auth'
|
||||||
|
|
||||||
export * from './helpers'
|
export * from './auth'
|
||||||
|
export * from './cart'
|
||||||
|
export * from './metafields'
|
||||||
|
export * from './collections'
|
||||||
export * from './queries'
|
export * from './queries'
|
||||||
export * from './mutations'
|
export * from './mutations'
|
||||||
export * from './normalize'
|
export * from './normalize'
|
||||||
|
90
packages/shopify/src/utils/metafields.ts
Normal file
90
packages/shopify/src/utils/metafields.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { MetafieldType } from '../types/product'
|
||||||
|
|
||||||
|
export const parseJson = (value: string): any => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (e) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitConversion: Record<string, string> = {
|
||||||
|
MILLIMETERS: 'millimeter',
|
||||||
|
CENTIMETERS: 'centimeter',
|
||||||
|
METERS: 'meter',
|
||||||
|
|
||||||
|
MILLILITERS: 'milliliter',
|
||||||
|
LITERS: 'liter',
|
||||||
|
FLUID_OUNCES: 'fluid-ounce',
|
||||||
|
IMPERIAL_FLUID_OUNCES: 'fluid-ounce',
|
||||||
|
GALLONS: 'gallon',
|
||||||
|
|
||||||
|
KILOGRAMS: 'kilogram',
|
||||||
|
GRAMS: 'gram',
|
||||||
|
OUNCES: 'ounce',
|
||||||
|
POUNDS: 'pound',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMeasurment = (input: string, locale: string = 'en-US') => {
|
||||||
|
try {
|
||||||
|
let { unit, value } = JSON.parse(input)
|
||||||
|
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
unit: unitConversion[unit],
|
||||||
|
style: 'unit',
|
||||||
|
}).format(parseFloat(value))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetafieldValue = (
|
||||||
|
type: MetafieldType,
|
||||||
|
value: string,
|
||||||
|
locale: string = 'en-US'
|
||||||
|
) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'boolean':
|
||||||
|
return value === 'true' ? '✓' : 'No'
|
||||||
|
case 'number_integer':
|
||||||
|
return parseInt(value).toLocaleString(locale)
|
||||||
|
case 'number_decimal':
|
||||||
|
return parseFloat(value).toLocaleString(locale)
|
||||||
|
case 'date':
|
||||||
|
return Intl.DateTimeFormat(locale, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
}).format(new Date(value))
|
||||||
|
case 'date_time':
|
||||||
|
return Intl.DateTimeFormat(locale, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'long',
|
||||||
|
}).format(new Date(value))
|
||||||
|
case 'dimension':
|
||||||
|
case 'volume':
|
||||||
|
case 'weight':
|
||||||
|
return getMeasurment(value, locale)
|
||||||
|
case 'rating':
|
||||||
|
const { scale_max, value: val } = JSON.parse(value)
|
||||||
|
return Array.from({ length: scale_max }, (_, i) =>
|
||||||
|
i <= val - 1 ? '★' : '☆'
|
||||||
|
).join('')
|
||||||
|
case 'color':
|
||||||
|
return `<figure style="background-color: ${value}; width: 1rem; height:1rem; display:block; margin-top: 2px; border-radius: 100%;"/>`
|
||||||
|
case 'url':
|
||||||
|
return `<a href="${value}" target="_blank">${value}</a>`
|
||||||
|
case 'multi_line_text_field':
|
||||||
|
return value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => `<p>${line}</p>`)
|
||||||
|
.join('')
|
||||||
|
case 'json':
|
||||||
|
case 'single_line_text_field':
|
||||||
|
case 'product_reference':
|
||||||
|
case 'page_reference':
|
||||||
|
case 'variant_reference':
|
||||||
|
case 'file_reference':
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ import humanizeString from 'humanize-string'
|
|||||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||||
|
|
||||||
import { colorMap } from './colors'
|
import { colorMap } from './colors'
|
||||||
import { getMetafieldValue, parseJson } from './helpers'
|
import { getMetafieldValue, parseJson } from './metafields'
|
||||||
|
|
||||||
type MoneyProps = MoneyV2 & { retailPrice?: string | number }
|
type MoneyProps = MoneyV2 & { retailPrice?: string | number }
|
||||||
|
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
"wishlist": false,
|
"wishlist": false,
|
||||||
"customerAuth": false,
|
"customerAuth": false,
|
||||||
"customCheckout": false,
|
"customCheckout": false,
|
||||||
"metafields": false
|
"customFields": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import cn from 'clsx'
|
import cn from 'clsx'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { WishlistButton } from '@components/wishlist'
|
|
||||||
import ProductSidebar from '../ProductSidebar'
|
|
||||||
import ProductTag from '../ProductTag'
|
|
||||||
|
|
||||||
import { useProduct } from '../context'
|
import { useProduct } from '../context'
|
||||||
|
import { WishlistButton } from '@components/wishlist'
|
||||||
|
|
||||||
|
import ProductTag from '../ProductTag'
|
||||||
|
import ProductSlider from '../ProductSlider'
|
||||||
|
import ProductSidebar from '../ProductSidebar'
|
||||||
|
|
||||||
import s from './ProductDetails.module.css'
|
import s from './ProductDetails.module.css'
|
||||||
import ProductSlider from '../ProductSlider'
|
|
||||||
|
|
||||||
const ProductDetails = () => {
|
const ProductDetails = () => {
|
||||||
const { product, variant, price } = useProduct()
|
const { product, variant, price } = useProduct()
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { FC } from 'react'
|
|
||||||
|
import type { FC } from 'react'
|
||||||
import type { Product } from '@commerce/types/product'
|
import type { Product } from '@commerce/types/product'
|
||||||
|
|
||||||
import { ProductCard } from '@components/product'
|
|
||||||
import { Container, Text } from '@components/ui'
|
|
||||||
import { ProductProvider } from '../context'
|
|
||||||
import ProductDetails from '../ProductDetails/ProductDetails'
|
|
||||||
import { SEO } from '@components/common'
|
import { SEO } from '@components/common'
|
||||||
|
import { ProductProvider } from '../context'
|
||||||
|
import { Container, Text } from '@components/ui'
|
||||||
|
import { ProductCard } from '@components/product'
|
||||||
|
|
||||||
|
import ProductDetails from '../ProductDetails/ProductDetails'
|
||||||
|
|
||||||
interface ProductViewProps {
|
interface ProductViewProps {
|
||||||
product: Product
|
product: Product
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
import { Product, ProductImage, ProductVariant } from '@commerce/types/product'
|
import { useMemo, useState, useEffect, useContext, createContext } from 'react'
|
||||||
|
|
||||||
import {
|
import type { FC, ReactNode, Dispatch, SetStateAction } from 'react'
|
||||||
getProductVariant,
|
|
||||||
selectDefaultOptionFromProduct,
|
|
||||||
SelectedOptions,
|
|
||||||
} from './helpers'
|
|
||||||
|
|
||||||
import React, { FC, useMemo, useState, ReactNode, useEffect } from 'react'
|
import type {
|
||||||
|
Product,
|
||||||
|
ProductImage,
|
||||||
|
ProductVariant,
|
||||||
|
} from '@commerce/types/product'
|
||||||
|
|
||||||
|
import type { SelectedOptions } from './helpers'
|
||||||
|
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
|
import { getProductVariant, selectDefaultOptionFromProduct } from './helpers'
|
||||||
|
|
||||||
export interface ProductContextValue {
|
export interface ProductContextValue {
|
||||||
product: Product
|
product: Product
|
||||||
imageIndex: number | null
|
imageIndex: number | null
|
||||||
setImageIndex: React.Dispatch<React.SetStateAction<number | null>>
|
setImageIndex: Dispatch<SetStateAction<number | null>>
|
||||||
price: string
|
price: string
|
||||||
variant: ProductVariant
|
variant: ProductVariant
|
||||||
selectedOptions: SelectedOptions
|
selectedOptions: SelectedOptions
|
||||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
setSelectedOptions: Dispatch<SetStateAction<SelectedOptions>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProductContext = React.createContext<ProductContextValue | null>(
|
export const ProductContext = createContext<ProductContextValue | null>(null)
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
ProductContext.displayName = 'ProductContext'
|
ProductContext.displayName = 'ProductContext'
|
||||||
|
|
||||||
@ -55,11 +56,11 @@ export const ProductProvider: FC<ProductProviderProps> = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const idx = product.images.findIndex((image: ProductImage) => {
|
const index = product.images.findIndex((image: ProductImage) => {
|
||||||
return image.url === variant?.image?.url
|
return image.url === variant?.image?.url
|
||||||
})
|
})
|
||||||
if (idx !== -1) {
|
if (index !== -1) {
|
||||||
setImageIndex(idx)
|
setImageIndex(index)
|
||||||
}
|
}
|
||||||
}, [variant, product])
|
}, [variant, product])
|
||||||
|
|
||||||
@ -82,7 +83,7 @@ export const ProductProvider: FC<ProductProviderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useProduct = () => {
|
export const useProduct = () => {
|
||||||
const context = React.useContext(ProductContext) as ProductContextValue
|
const context = useContext(ProductContext) as ProductContextValue
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error(`useProduct must be used within a ProductProvider`)
|
throw new Error(`useProduct must be used within a ProductProvider`)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user