diff --git a/packages/bigcommerce/src/lib/normalize.ts b/packages/bigcommerce/src/lib/normalize.ts
index e83394c91..ee3715d89 100644
--- a/packages/bigcommerce/src/lib/normalize.ts
+++ b/packages/bigcommerce/src/lib/normalize.ts
@@ -8,6 +8,17 @@ import type { definitions } from '../api/definitions/store-content'
import getSlug from './get-slug'
+const normalizePrice = (prices: ProductNode['prices']) => ({
+ value: prices?.price.value || 0,
+ currencyCode: prices?.price.currencyCode || 'USD',
+ ...(prices?.salePrice?.value && {
+ salePrice: prices.salePrice.value,
+ }),
+ ...(prices?.retailPrice?.value && {
+ retailPrice: prices.retailPrice.value,
+ }),
+})
+
function normalizeProductOption(productOption: any) {
const {
node: { entityId, values: { edges = [] } = {}, ...rest },
@@ -20,43 +31,64 @@ function normalizeProductOption(productOption: any) {
}
}
+const normalizeImages = (productNode: ProductNode) => {
+ const output =
+ productNode.images.edges?.map(
+ ({ node: { urlOriginal, altText, ...rest } }: any) => ({
+ url: urlOriginal,
+ alt: altText,
+ ...rest,
+ })
+ ) || []
+
+ /**
+ * Add the variants images to the product images, because the variants images are not included in the product images
+ */
+ productNode.variants.edges?.forEach(({ node: variant }: any) => {
+ if (variant.defaultImage?.urlOriginal) {
+ output.push({
+ url: variant.defaultImage.urlOriginal,
+ alt: variant.defaultImage.altText,
+ })
+ }
+ })
+
+ return output
+}
+
+const normalizeVariants = (variants: any) =>
+ variants.edges?.map(
+ ({
+ node: { entityId, productOptions, prices, defaultImage, ...rest },
+ }: any) => ({
+ id: String(entityId),
+ ...(defaultImage && {
+ image: {
+ url: defaultImage.urlOriginal,
+ alt: defaultImage.altText,
+ },
+ }),
+ ...(prices && { price: normalizePrice(prices) }),
+ options: productOptions?.edges
+ ? productOptions.edges.map(normalizeProductOption)
+ : [],
+ ...rest,
+ })
+ ) || []
+
export function normalizeProduct(productNode: ProductNode): Product {
- const {
- entityId: id,
- productOptions,
- prices,
- path,
- images,
- variants,
- } = productNode
+ const { entityId: id, productOptions, prices, path, variants } = productNode
return {
id: String(id),
name: productNode.name,
description: productNode.description,
- images:
- images.edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
- url: urlOriginal,
- alt: altText,
- ...rest,
- })) || [],
+ images: normalizeImages(productNode),
path: `/${getSlug(path)}`,
- variants:
- variants.edges?.map(
- ({ node: { entityId, productOptions, ...rest } }: any) => ({
- id: String(entityId),
- options: productOptions?.edges
- ? productOptions.edges.map(normalizeProductOption)
- : [],
- ...rest,
- })
- ) || [],
+ variants: normalizeVariants(variants),
options: productOptions?.edges?.map(normalizeProductOption) || [],
slug: path?.replace(/^\/+|\/+$/g, ''),
- price: {
- value: prices?.price.value,
- currencyCode: prices?.price.currencyCode,
- },
+ price: normalizePrice(prices),
}
}
diff --git a/packages/commerce/src/api/utils/index.ts b/packages/commerce/src/api/utils/index.ts
index 3fd50d00e..082029d0c 100644
--- a/packages/commerce/src/api/utils/index.ts
+++ b/packages/commerce/src/api/utils/index.ts
@@ -44,7 +44,9 @@ export const transformRequest = (req: NextApiRequest, path: string) => {
body = JSON.stringify(req.body)
}
- return new NextRequest(`https://${req.headers.host}/api/commerce/${path}`, {
+ const url = new URL(req.url || '/', `https://${req.headers.host}`)
+
+ return new NextRequest(url, {
headers,
method: req.method,
body,
diff --git a/packages/shopify/src/utils/normalize.ts b/packages/shopify/src/utils/normalize.ts
index 066daff33..177a9a18f 100644
--- a/packages/shopify/src/utils/normalize.ts
+++ b/packages/shopify/src/utils/normalize.ts
@@ -19,7 +19,9 @@ import type {
import { colorMap } from './colors'
-const money = ({ amount, currencyCode }: MoneyV2) => {
+type MoneyProps = MoneyV2 & { retailPrice?: string | number }
+
+const money = ({ amount, currencyCode }: MoneyProps) => {
return {
value: +amount,
currencyCode,
@@ -67,6 +69,7 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
selectedOptions,
sku,
title,
+ image,
priceV2,
compareAtPriceV2,
requiresShipping,
@@ -77,7 +80,8 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
id,
name: title,
sku,
- price: +priceV2.amount,
+ image,
+ price: money({ ...priceV2, retailPrice: compareAtPriceV2?.amount }),
listPrice: +compareAtPriceV2?.amount,
requiresShipping,
availableForSale,
diff --git a/packages/shopify/src/utils/queries/get-product-query.ts b/packages/shopify/src/utils/queries/get-product-query.ts
index 1c4458432..7dfeb79e1 100644
--- a/packages/shopify/src/utils/queries/get-product-query.ts
+++ b/packages/shopify/src/utils/queries/get-product-query.ts
@@ -34,6 +34,13 @@ const getProductQuery = /* GraphQL */ `
id
title
sku
+ image {
+ id
+ altText
+ url
+ width
+ height
+ }
availableForSale
requiresShipping
selectedOptions {
diff --git a/site/components/product/ProductCard/ProductCard.module.css b/site/components/product/ProductCard/ProductCard.module.css
index d5d441fea..a34b338be 100644
--- a/site/components/product/ProductCard/ProductCard.module.css
+++ b/site/components/product/ProductCard/ProductCard.module.css
@@ -7,7 +7,7 @@
.root:hover {
& .productImage {
- transform: scale(1.2625);
+ transform: scale(1.1);
}
& .header .name span,
@@ -47,23 +47,25 @@
}
.header .name {
- @apply pt-0 max-w-full w-full leading-extra-loose
+ @apply pt-1 max-w-full w-full leading-extra-loose
transition-colors ease-in-out duration-500;
- font-size: 2rem;
+ font-size: 1rem;
+ line-height: 1;
letter-spacing: 0.4px;
}
.header .name span {
- @apply py-4 px-6 bg-primary text-primary font-bold
+ @apply py-2 px-3 bg-primary text-primary font-bold
transition-colors ease-in-out duration-500;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
+ line-height: 1 !important;
}
.header .price {
- @apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
+ @apply pt-1.5 pb-2 px-3 text-sm bg-primary text-accent-9
font-semibold inline-block tracking-wide
transition-colors ease-in-out duration-500;
}
@@ -77,8 +79,7 @@
}
.imageContainer .productImage {
- @apply transform transition-transform duration-500
- object-cover scale-120;
+ @apply transform transition-transform duration-500 object-cover;
}
.root .wishlistButton {
@@ -87,7 +88,7 @@
/* Variant Simple */
.simple .header .name {
- @apply pt-2 text-lg leading-10 -mt-1;
+ @apply pt-1 text-base leading-6;
}
.simple .header .price {
@@ -101,11 +102,11 @@
}
.slim .header {
- @apply absolute inset-0 flex items-center justify-end mr-8 z-20;
+ @apply absolute inset-0 flex text-center items-center justify-end mr-8 z-20;
}
.slim span {
- @apply bg-accent-9 text-accent-0 inline-block p-3
+ @apply bg-accent-9 text-accent-0 inline-block px-3 py-2.5
font-bold text-xl break-words;
}
diff --git a/site/components/product/ProductDetails/ProductDetails.module.css b/site/components/product/ProductDetails/ProductDetails.module.css
new file mode 100644
index 000000000..988c78440
--- /dev/null
+++ b/site/components/product/ProductDetails/ProductDetails.module.css
@@ -0,0 +1,56 @@
+.root {
+ @apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
+ min-height: auto;
+}
+
+.main {
+ @apply relative px-0 pb-0 box-border flex flex-col col-span-1;
+ min-height: 500px;
+}
+
+.sidebar {
+ @apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
+}
+
+.sliderContainer {
+ @apply flex flex-1 overflow-x-hidden bg-violet items-center justify-center;
+}
+
+.imageContainer {
+ @apply text-center h-full relative flex-shrink-0;
+}
+
+.imageContainer > img {
+ @apply mx-auto;
+}
+
+.sliderContainer .img {
+ @apply w-full h-full max-h-full object-cover transition duration-500 ease-in-out;
+}
+
+.button {
+ width: 100%;
+}
+
+.wishlistButton {
+ @apply absolute z-30 top-0 right-0;
+}
+
+@screen lg {
+ .root {
+ @apply grid-cols-12;
+ }
+
+ .main {
+ @apply mx-0 col-span-8;
+ min-height: 782px;
+ }
+
+ .sidebar {
+ @apply col-span-4 py-6;
+ }
+
+ .imageContainer {
+ max-height: 600px;
+ }
+}
diff --git a/site/components/product/ProductDetails/ProductDetails.tsx b/site/components/product/ProductDetails/ProductDetails.tsx
new file mode 100644
index 000000000..336e4f1b5
--- /dev/null
+++ b/site/components/product/ProductDetails/ProductDetails.tsx
@@ -0,0 +1,47 @@
+import cn from 'clsx'
+import { useProduct } from '../context'
+import { WishlistButton } from '@components/wishlist'
+import Image from 'next/image'
+import ProductTag from '../ProductTag'
+import ProductSlider from '../ProductSlider'
+import ProductSidebar from '../ProductSidebar'
+
+import s from './ProductDetails.module.css'
+
+const ProductDetails = () => {
+ const { product, variant, price } = useProduct()
+
+ return (
+
+
+
+
+
+ {product.images.map((image, i) => (
+
+
+
+ ))}
+
+
+ {process.env.COMMERCE_WISHLIST_ENABLED && variant && (
+
+ )}
+
+
+
+ )
+}
+
+export default ProductDetails
diff --git a/site/components/product/ProductDetails/index.ts b/site/components/product/ProductDetails/index.ts
new file mode 100644
index 000000000..cf6d80139
--- /dev/null
+++ b/site/components/product/ProductDetails/index.ts
@@ -0,0 +1 @@
+export { default } from './ProductDetails'
diff --git a/site/components/product/ProductOptions/ProductOptions.tsx b/site/components/product/ProductOptions/ProductOptions.tsx
index 15c229edb..35998a279 100644
--- a/site/components/product/ProductOptions/ProductOptions.tsx
+++ b/site/components/product/ProductOptions/ProductOptions.tsx
@@ -1,27 +1,18 @@
import { memo } from 'react'
import { Swatch } from '@components/product'
-import type { ProductOption } from '@commerce/types/product'
-import { SelectedOptions } from '../helpers'
+import { useProduct } from '../context'
-interface ProductOptionsProps {
- options: ProductOption[]
- selectedOptions: SelectedOptions
- setSelectedOptions: React.Dispatch>
-}
+const ProductOptions: React.FC = () => {
+ const { product, selectedOptions, setSelectedOptions } = useProduct()
-const ProductOptions: React.FC = ({
- options,
- selectedOptions,
- setSelectedOptions,
-}) => {
return (
- {options.map((opt) => (
+ {product.options.map((opt) => (
{opt.displayName}
-
+
{opt.values.map((v, i: number) => {
const active = selectedOptions[opt.displayName.toLowerCase()]
return (
diff --git a/site/components/product/ProductSidebar/ProductSidebar.tsx b/site/components/product/ProductSidebar/ProductSidebar.tsx
index 67c3dab9f..0624e7e99 100644
--- a/site/components/product/ProductSidebar/ProductSidebar.tsx
+++ b/site/components/product/ProductSidebar/ProductSidebar.tsx
@@ -1,63 +1,49 @@
import s from './ProductSidebar.module.css'
import { useAddItem } from '@framework/cart'
-import { FC, useEffect, useState } from 'react'
+import { FC, useState } from 'react'
import { ProductOptions } from '@components/product'
-import type { Product } from '@commerce/types/product'
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
-import {
- getProductVariant,
- selectDefaultOptionFromProduct,
- SelectedOptions,
-} from '../helpers'
-import ErrorMessage from '@components/ui/ErrorMessage'
+import { useProduct } from '../context'
interface ProductSidebarProps {
- product: Product
className?: string
}
-const ProductSidebar: FC
= ({ product, className }) => {
+const ProductSidebar: FC = ({ className }) => {
const addItem = useAddItem()
+
+ const { product, variant, price } = useProduct()
const { openSidebar, setSidebarView } = useUI()
const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
- const [selectedOptions, setSelectedOptions] = useState({})
+ const [error, setError] = useState(null)
- useEffect(() => {
- selectDefaultOptionFromProduct(product, setSelectedOptions)
- }, [product])
-
- const variant = getProductVariant(product, selectedOptions)
const addToCart = async () => {
setLoading(true)
setError(null)
try {
- await addItem({
- productId: String(product.id),
- variantId: String(variant ? variant.id : product.variants[0]?.id),
- })
- setSidebarView('CART_VIEW')
- openSidebar()
+ if (variant) {
+ await addItem({
+ productId: String(product.id),
+ variantId: String(variant.id),
+ })
+ setSidebarView('CART_VIEW')
+ openSidebar()
+ } else {
+ throw new Error('The variant selected is not available')
+ }
setLoading(false)
} catch (err) {
+ console.error(err)
setLoading(false)
if (err instanceof Error) {
- console.error(err)
- setError({
- ...err,
- message: 'Could not add item to cart. Please try again.',
- })
+ setError(err.message)
}
}
}
return (
-
+
= ({ product, className }) => {
36 reviews
- {error &&
}
{process.env.COMMERCE_CART_ENABLED && (
)}
+
+ {error && (
+
{error}
+ )}
diff --git a/site/components/product/ProductSlider/ProductSlider.module.css b/site/components/product/ProductSlider/ProductSlider.module.css
index b95bffdd0..f80f574a2 100644
--- a/site/components/product/ProductSlider/ProductSlider.module.css
+++ b/site/components/product/ProductSlider/ProductSlider.module.css
@@ -1,29 +1,24 @@
.root {
- @apply relative w-full h-full select-none;
+ @apply flex flex-col relative select-none w-full;
overflow: hidden;
}
.slider {
- @apply relative h-full transition-opacity duration-150;
- opacity: 0;
-}
-
-.slider.show {
- opacity: 1;
+ @apply flex-1;
}
.thumb {
- @apply overflow-hidden inline-block cursor-pointer h-full;
+ @apply overflow-hidden inline-block cursor-pointer h-full transition duration-300 md:hover:scale-105;
width: 125px;
width: calc(100% / 3);
}
.thumb.selected {
- @apply bg-white;
+ @apply bg-white/30;
}
.thumb img {
- height: 85% !important;
+ @apply w-full h-full max-h-full object-cover;
}
.album {
@@ -44,10 +39,6 @@
}
@screen md {
- .thumb:hover {
- transform: scale(1.02);
- }
-
.album {
height: 182px;
}
diff --git a/site/components/product/ProductSlider/ProductSlider.tsx b/site/components/product/ProductSlider/ProductSlider.tsx
index bd0e2db87..46f02ecbd 100644
--- a/site/components/product/ProductSlider/ProductSlider.tsx
+++ b/site/components/product/ProductSlider/ProductSlider.tsx
@@ -10,6 +10,8 @@ import cn from 'clsx'
import { a } from '@react-spring/web'
import s from './ProductSlider.module.css'
import ProductSliderControl from '../ProductSliderControl'
+import { useProduct } from '../context'
+import { Image as ProductImage } from '@commerce/types/common'
interface ProductSliderProps {
children?: React.ReactNode[]
@@ -20,19 +22,18 @@ const ProductSlider: React.FC = ({
children,
className = '',
}) => {
+ const { product, variant } = useProduct()
const [currentSlide, setCurrentSlide] = useState(0)
- const [isMounted, setIsMounted] = useState(false)
+
const sliderContainerRef = useRef(null)
const thumbsContainerRef = useRef(null)
const [ref, slider] = useKeenSlider({
loop: true,
slides: { perView: 1 },
- created: () => setIsMounted(true),
slideChanged(s) {
const slideNumber = s.track.details.rel
setCurrentSlide(slideNumber)
-
if (thumbsContainerRef.current) {
const $el = document.getElementById(`thumb-${slideNumber}`)
if (slideNumber >= 3) {
@@ -44,6 +45,18 @@ const ProductSlider: React.FC = ({
},
})
+ useEffect(() => {
+ const index = product.images.findIndex((image: ProductImage) => {
+ return image.url === variant?.image?.url
+ })
+
+ if (index !== -1) {
+ slider.current?.moveToIdx(index, false, {
+ duration: 0,
+ })
+ }
+ }, [variant, product, slider])
+
// Stop the history navigation gesture on touch devices
useEffect(() => {
const preventNavigation = (event: TouchEvent) => {
@@ -74,15 +87,17 @@ const ProductSlider: React.FC = ({
}
}, [])
- const onPrev = React.useCallback(() => slider.current?.prev(), [slider])
- const onNext = React.useCallback(() => slider.current?.next(), [slider])
+ const onPrev = React.useCallback(() => {
+ slider.current?.prev()
+ }, [slider])
+
+ const onNext = React.useCallback(() => {
+ slider.current?.next()
+ }, [slider])
return (
-
+
{slider &&
}
{Children.map(children, (child) => {
// Add the keen-slider__slide className to children
@@ -109,6 +124,8 @@ const ProductSlider: React.FC
= ({
...child,
props: {
...child.props,
+ width: 132,
+ height: 82,
className: cn(child.props.className, s.thumb, {
[s.selected]: currentSlide === idx,
}),
diff --git a/site/components/product/ProductTag/ProductTag.module.css b/site/components/product/ProductTag/ProductTag.module.css
index c36b43aa6..8c735506d 100644
--- a/site/components/product/ProductTag/ProductTag.module.css
+++ b/site/components/product/ProductTag/ProductTag.module.css
@@ -4,10 +4,10 @@
}
.root .name {
- @apply pt-0 max-w-full w-full leading-extra-loose;
+ @apply pt-0 max-w-full w-full;
font-size: 2rem;
letter-spacing: 0.4px;
- line-height: 2.1em;
+ line-height: 1.5em;
}
.root .name span {
diff --git a/site/components/product/ProductView/ProductView.module.css b/site/components/product/ProductView/ProductView.module.css
index e68c76f21..c040a31a4 100644
--- a/site/components/product/ProductView/ProductView.module.css
+++ b/site/components/product/ProductView/ProductView.module.css
@@ -1,59 +1,3 @@
-.root {
- @apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
- min-height: auto;
-}
-
-.main {
- @apply relative px-0 pb-0 box-border flex flex-col col-span-1;
- min-height: 500px;
-}
-
-.sidebar {
- @apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
-}
-
-.sliderContainer {
- @apply flex items-center justify-center overflow-x-hidden bg-violet;
-}
-
-.imageContainer {
- @apply text-center h-full relative;
-}
-
-.imageContainer > span {
- height: 100% !important;
-}
-
-.sliderContainer .img {
- @apply w-full h-full max-h-full object-cover;
-}
-
-.button {
- width: 100%;
-}
-
-.wishlistButton {
- @apply absolute z-30 top-0 right-0;
-}
-
.relatedProductsGrid {
- @apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
-}
-
-@screen lg {
- .root {
- @apply grid-cols-12;
- }
-
- .main {
- @apply mx-0 col-span-8;
- }
-
- .sidebar {
- @apply col-span-4 py-6;
- }
-
- .imageContainer {
- max-height: 600px;
- }
+ @apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7 m-0;
}
diff --git a/site/components/product/ProductView/ProductView.tsx b/site/components/product/ProductView/ProductView.tsx
index 31cbcd577..b1167a01d 100644
--- a/site/components/product/ProductView/ProductView.tsx
+++ b/site/components/product/ProductView/ProductView.tsx
@@ -1,88 +1,44 @@
-import cn from 'clsx'
-import Image from 'next/image'
import s from './ProductView.module.css'
-import { FC } from 'react'
+
+import type { FC } from 'react'
import type { Product } from '@commerce/types/product'
-import usePrice from '@framework/product/use-price'
-import { WishlistButton } from '@components/wishlist'
-import { ProductSlider, ProductCard } from '@components/product'
-import { Container, Text } from '@components/ui'
+
import { SEO } from '@components/common'
-import ProductSidebar from '../ProductSidebar'
-import ProductTag from '../ProductTag'
+import { ProductProvider } from '../context'
+import { Container, Text } from '@components/ui'
+import { ProductCard } from '@components/product'
+
+import ProductDetails from '../ProductDetails/ProductDetails'
+
interface ProductViewProps {
product: Product
relatedProducts: Product[]
}
const ProductView: FC = ({ product, relatedProducts }) => {
- const { price } = usePrice({
- amount: product.price.value,
- baseAmount: product.price.retailPrice,
- currencyCode: product.price.currencyCode!,
- })
-
return (
<>
-
-
-
-
-
- {product.images.map((image, i) => (
-
-
-
- ))}
-
-
- {process.env.COMMERCE_WISHLIST_ENABLED && (
-
- )}
-
+
+
+
-
-
+
Related Products
{relatedProducts.map((p) => (
))}
@@ -99,7 +55,7 @@ const ProductView: FC
= ({ product, relatedProducts }) => {
images: [
{
url: product.images[0]?.url!,
- width: '800',
+ width: '600',
height: '600',
alt: product.name,
},
diff --git a/site/components/product/Swatch/Swatch.module.css b/site/components/product/Swatch/Swatch.module.css
index 79a69e548..619ac8331 100644
--- a/site/components/product/Swatch/Swatch.module.css
+++ b/site/components/product/Swatch/Swatch.module.css
@@ -1,9 +1,9 @@
.swatch {
box-sizing: border-box;
composes: root from '@components/ui/Button/Button.module.css';
- @apply h-10 w-10 bg-primary text-primary rounded-full mr-3 inline-flex
+ @apply h-10 w-10 bg-primary text-primary rounded-full inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out
- p-0 shadow-none border-accent-3 border box-border select-none;
+ p-0 shadow-none border-accent-3 border box-border select-none !mr-0;
margin-right: calc(0.75rem - 1px);
overflow: hidden;
width: 48px;
diff --git a/site/components/product/Swatch/Swatch.tsx b/site/components/product/Swatch/Swatch.tsx
index 865f43398..5ac8c2ae9 100644
--- a/site/components/product/Swatch/Swatch.tsx
+++ b/site/components/product/Swatch/Swatch.tsx
@@ -18,7 +18,7 @@ const Swatch: React.FC & SwatchProps> = ({
className,
color = '',
label = null,
- variant = 'size',
+ variant,
...props
}) => {
variant = variant?.toLowerCase()
diff --git a/site/components/product/context.tsx b/site/components/product/context.tsx
new file mode 100644
index 000000000..0ad91620f
--- /dev/null
+++ b/site/components/product/context.tsx
@@ -0,0 +1,71 @@
+import { useMemo, useState, useEffect, useContext, createContext } from 'react'
+
+import type { FC, ReactNode, Dispatch, SetStateAction } from 'react'
+import type { SelectedOptions } from './helpers'
+import type { Product, ProductVariant } from '@commerce/types/product'
+
+import usePrice from '@framework/product/use-price'
+import { getProductVariant, selectDefaultOptionFromProduct } from './helpers'
+
+export interface ProductContextValue {
+ product: Product
+ price: string
+ variant?: ProductVariant
+ selectedOptions: SelectedOptions
+ setSelectedOptions: Dispatch>
+}
+
+export const ProductContext = createContext(null)
+
+ProductContext.displayName = 'ProductContext'
+
+type ProductProviderProps = {
+ product: Product
+ children?: ReactNode
+}
+
+export const ProductProvider: FC = ({
+ product,
+ children,
+}) => {
+ const [selectedOptions, setSelectedOptions] = useState({})
+
+ useEffect(
+ () => selectDefaultOptionFromProduct(product, setSelectedOptions),
+ [product]
+ )
+
+ const variant = useMemo(
+ () => getProductVariant(product, selectedOptions),
+ [product, selectedOptions]
+ )
+
+ const { price } = usePrice({
+ amount: variant?.price?.value || product.price.value,
+ baseAmount: variant?.price?.retailPrice || product.price.retailPrice,
+ currencyCode: variant?.price?.currencyCode || product.price.currencyCode!,
+ })
+
+ const value = useMemo(
+ () => ({
+ price,
+ product,
+ variant,
+ selectedOptions,
+ setSelectedOptions,
+ }),
+ [price, product, selectedOptions, variant]
+ )
+
+ return (
+ {children}
+ )
+}
+
+export const useProduct = () => {
+ const context = useContext(ProductContext) as ProductContextValue
+ if (context === undefined) {
+ throw new Error(`useProduct must be used within a ProductProvider`)
+ }
+ return context
+}
diff --git a/site/components/product/helpers.ts b/site/components/product/helpers.ts
index 77e385bb8..8447b4f88 100644
--- a/site/components/product/helpers.ts
+++ b/site/components/product/helpers.ts
@@ -1,4 +1,4 @@
-import type { Product } from '@commerce/types/product'
+import type { Product } from '@vercel/commerce/types/product'
export type SelectedOptions = Record
import { Dispatch, SetStateAction } from 'react'
@@ -22,11 +22,19 @@ export function selectDefaultOptionFromProduct(
product: Product,
updater: Dispatch>
) {
- // Selects the default option
- product.variants[0]?.options?.forEach((v) => {
- updater((choices) => ({
- ...choices,
- [v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
- }))
- })
+ // Get the first available option or the first option
+ const variant =
+ product.variants.find((variant) => variant.availableForSale) ||
+ product.variants[0]
+
+ // Reset the selectedOptions and set the default option from the available variant
+ const newValue: SelectedOptions = {}
+
+ if (variant) {
+ for (const c of variant.options) {
+ newValue[c.displayName.toLowerCase()] = c.values[0].label.toLowerCase()
+ }
+ }
+
+ updater(newValue)
}
diff --git a/site/components/search.tsx b/site/components/search.tsx
index 92146628c..5b4386d55 100644
--- a/site/components/search.tsx
+++ b/site/components/search.tsx
@@ -273,7 +273,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
{/* Products */}
{(q || activeCategory || activeBrand) && (
-
+
{data ? (
<>
)}
{data ? (
-
+
{data.products.map((product: Product) => (
) : (
-
+
{rangeMap(12, (i) => (
diff --git a/site/pages/product/[slug].tsx b/site/pages/product/[slug].tsx
index 9d8d153e4..bef23ef40 100644
--- a/site/pages/product/[slug].tsx
+++ b/site/pages/product/[slug].tsx
@@ -14,37 +14,44 @@ export async function getStaticProps({
locales,
preview,
}: GetStaticPropsContext<{ slug: string }>) {
- const config = { locale, locales }
- const pagesPromise = commerce.getAllPages({ config, preview })
- const siteInfoPromise = commerce.getSiteInfo({ config, preview })
- const productPromise = commerce.getProduct({
- variables: { slug: params!.slug },
- config,
- preview,
- })
- const allProductsPromise = commerce.getAllProducts({
- variables: { first: 4 },
- config,
- preview,
- })
+ try {
+ const config = { locale, locales }
+ const pagesPromise = commerce.getAllPages({ config, preview })
+ const siteInfoPromise = commerce.getSiteInfo({ config, preview })
+ const productPromise = commerce.getProduct({
+ variables: { slug: params!.slug },
+ config,
+ preview,
+ })
+ const allProductsPromise = commerce.getAllProducts({
+ variables: { first: 4 },
+ config,
+ preview,
+ })
- const { pages } = await pagesPromise
- const { categories } = await siteInfoPromise
- const { product } = await productPromise
- const { products: relatedProducts } = await allProductsPromise
+ const { pages } = await pagesPromise
+ const { categories } = await siteInfoPromise
+ const { product } = await productPromise
+ const { products: relatedProducts } = await allProductsPromise
- if (!product) {
- throw new Error(`Product with slug '${params!.slug}' not found`)
- }
+ if (!product) {
+ throw new Error(`Product with slug '${params!.slug}' not found`)
+ }
- return {
- props: {
- pages,
- product,
- relatedProducts,
- categories,
- },
- revalidate: 200,
+ return {
+ props: {
+ pages,
+ product,
+ relatedProducts,
+ categories,
+ },
+ revalidate: 200,
+ }
+ } catch (error) {
+ console.log(error)
+ return {
+ notFound: true,
+ }
}
}
diff --git a/site/tsconfig.json b/site/tsconfig.json
index 7c91afd6f..2de809a44 100644
--- a/site/tsconfig.json
+++ b/site/tsconfig.json
@@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["../packages/commerce/src"],
"@commerce/*": ["../packages/commerce/src/*"],
- "@framework": ["../packages/local/src"],
- "@framework/*": ["../packages/local/src/*"]
+ "@framework": ["../packages/shopify/src"],
+ "@framework/*": ["../packages/shopify/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],