mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Change image & price on variant change
This commit is contained in:
parent
f8a1fb3d7f
commit
0b41b4e618
@ -14,3 +14,8 @@ export type Image = {
|
|||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SEO = {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { SEO } from './common'
|
||||||
|
|
||||||
export type ProductImage = {
|
export type ProductImage = {
|
||||||
url: string
|
url: string
|
||||||
alt?: string
|
alt?: string
|
||||||
@ -29,6 +31,10 @@ export type ProductVariant = {
|
|||||||
id: string | number
|
id: string | number
|
||||||
options: ProductOption[]
|
options: ProductOption[]
|
||||||
availableForSale?: boolean
|
availableForSale?: boolean
|
||||||
|
// Product variant price
|
||||||
|
price?: ProductPrice
|
||||||
|
// Product variant image
|
||||||
|
image?: ProductImage
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
@ -44,6 +50,7 @@ export type Product = {
|
|||||||
price: ProductPrice
|
price: ProductPrice
|
||||||
options: ProductOption[]
|
options: ProductOption[]
|
||||||
vendor?: string
|
vendor?: string
|
||||||
|
seo?: SEO
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchProductsBody = {
|
export type SearchProductsBody = {
|
||||||
|
@ -108,10 +108,7 @@ const ProductCard: FC<Props> = ({
|
|||||||
variant={product.variants[0] as any}
|
variant={product.variants[0] as any}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ProductTag
|
<ProductTag name={product.name} price={price} />
|
||||||
name={product.name}
|
|
||||||
price={`${price} ${product.price?.currencyCode}`}
|
|
||||||
/>
|
|
||||||
<div className={s.imageContainer}>
|
<div className={s.imageContainer}>
|
||||||
{product?.images && (
|
{product?.images && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
49
site/components/product/ProductDetails/ProductDetails.tsx
Normal file
49
site/components/product/ProductDetails/ProductDetails.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import cn from 'clsx'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { WishlistButton } from '@components/wishlist'
|
||||||
|
import ProductSidebar from '../ProductSidebar'
|
||||||
|
import ProductTag from '../ProductTag'
|
||||||
|
|
||||||
|
import { useProduct } from '../product-context'
|
||||||
|
|
||||||
|
import s from './ProductDetails.module.css'
|
||||||
|
import ProductSlider from '../ProductSlider'
|
||||||
|
|
||||||
|
const ProductDetails = () => {
|
||||||
|
const { product, variant, price } = useProduct()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(s.root, 'fit')}>
|
||||||
|
<div className={cn(s.main, 'fit')}>
|
||||||
|
<ProductTag name={product.name} price={price} fontSize={32} />
|
||||||
|
<div className={s.sliderContainer}>
|
||||||
|
<ProductSlider key={product.id}>
|
||||||
|
{product.images.map((image, i) => (
|
||||||
|
<div key={image.url} className={s.imageContainer}>
|
||||||
|
<Image
|
||||||
|
className={s.img}
|
||||||
|
src={image.url!}
|
||||||
|
alt={image.alt || 'Product Image'}
|
||||||
|
width={600}
|
||||||
|
height={600}
|
||||||
|
priority={i === 0}
|
||||||
|
quality="85"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ProductSlider>
|
||||||
|
</div>
|
||||||
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
|
<WishlistButton
|
||||||
|
className={s.wishlistButton}
|
||||||
|
productId={product.id}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ProductSidebar key={product.id} className={s.sidebar} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductDetails
|
@ -1,22 +1,13 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Swatch } from '@components/product'
|
import { Swatch } from '@components/product'
|
||||||
import type { ProductOption } from '@commerce/types/product'
|
import { useProduct } from '../product-context'
|
||||||
import { SelectedOptions } from '../helpers'
|
|
||||||
|
|
||||||
interface ProductOptionsProps {
|
const ProductOptions: React.FC = () => {
|
||||||
options: ProductOption[]
|
const { product, selectedOptions, setSelectedOptions } = useProduct()
|
||||||
selectedOptions: SelectedOptions
|
|
||||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
|
||||||
options,
|
|
||||||
selectedOptions,
|
|
||||||
setSelectedOptions,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{options.map((opt) => (
|
{product.options.map((opt) => (
|
||||||
<div className="pb-4" key={opt.displayName}>
|
<div className="pb-4" key={opt.displayName}>
|
||||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||||
{opt.displayName}
|
{opt.displayName}
|
||||||
|
@ -1,31 +1,22 @@
|
|||||||
import s from './ProductSidebar.module.css'
|
import s from './ProductSidebar.module.css'
|
||||||
import { useAddItem } from '@framework/cart'
|
import { useAddItem } from '@framework/cart'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { ProductOptions } from '@components/product'
|
import { ProductOptions } from '@components/product'
|
||||||
import type { Product } from '@commerce/types/product'
|
|
||||||
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
||||||
import {
|
|
||||||
getProductVariant,
|
import { useProduct } from '../product-context'
|
||||||
selectDefaultOptionFromProduct,
|
|
||||||
SelectedOptions,
|
|
||||||
} from '../helpers'
|
|
||||||
|
|
||||||
interface ProductSidebarProps {
|
interface ProductSidebarProps {
|
||||||
product: Product
|
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
|
|
||||||
|
const { product, variant } = useProduct()
|
||||||
const { openSidebar, setSidebarView } = useUI()
|
const { openSidebar, setSidebarView } = useUI()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
|
||||||
}, [product])
|
|
||||||
|
|
||||||
const variant = getProductVariant(product, selectedOptions)
|
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@ -43,11 +34,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<ProductOptions
|
<ProductOptions />
|
||||||
options={product.options}
|
|
||||||
selectedOptions={selectedOptions}
|
|
||||||
setSelectedOptions={setSelectedOptions}
|
|
||||||
/>
|
|
||||||
<Text
|
<Text
|
||||||
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}
|
||||||
|
@ -10,6 +10,7 @@ import cn from 'clsx'
|
|||||||
import { a } from '@react-spring/web'
|
import { a } from '@react-spring/web'
|
||||||
import s from './ProductSlider.module.css'
|
import s from './ProductSlider.module.css'
|
||||||
import ProductSliderControl from '../ProductSliderControl'
|
import ProductSliderControl from '../ProductSliderControl'
|
||||||
|
import { useProduct } from '../product-context'
|
||||||
|
|
||||||
interface ProductSliderProps {
|
interface ProductSliderProps {
|
||||||
children: React.ReactNode[]
|
children: React.ReactNode[]
|
||||||
@ -20,7 +21,8 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const [currentSlide, setCurrentSlide] = useState(0)
|
const { imageIndex, resetImageIndex } = useProduct()
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(imageIndex ?? 0)
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
const sliderContainerRef = useRef<HTMLDivElement>(null)
|
const sliderContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const thumbsContainerRef = useRef<HTMLDivElement>(null)
|
const thumbsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
@ -29,10 +31,12 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
loop: true,
|
loop: true,
|
||||||
slides: { perView: 1 },
|
slides: { perView: 1 },
|
||||||
created: () => setIsMounted(true),
|
created: () => setIsMounted(true),
|
||||||
|
dragStarted: () => {
|
||||||
|
resetImageIndex()
|
||||||
|
},
|
||||||
slideChanged(s) {
|
slideChanged(s) {
|
||||||
const slideNumber = s.track.details.rel
|
const slideNumber = s.track.details.rel
|
||||||
setCurrentSlide(slideNumber)
|
setCurrentSlide(slideNumber)
|
||||||
|
|
||||||
if (thumbsContainerRef.current) {
|
if (thumbsContainerRef.current) {
|
||||||
const $el = document.getElementById(`thumb-${slideNumber}`)
|
const $el = document.getElementById(`thumb-${slideNumber}`)
|
||||||
if (slideNumber >= 3) {
|
if (slideNumber >= 3) {
|
||||||
@ -74,8 +78,23 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onPrev = React.useCallback(() => slider.current?.prev(), [slider])
|
useEffect(() => {
|
||||||
const onNext = React.useCallback(() => slider.current?.next(), [slider])
|
if (imageIndex && imageIndex !== currentSlide) {
|
||||||
|
slider.current?.moveToIdx(imageIndex, undefined, {
|
||||||
|
duration: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [imageIndex, currentSlide, slider])
|
||||||
|
|
||||||
|
const onPrev = React.useCallback(() => {
|
||||||
|
resetImageIndex()
|
||||||
|
slider.current?.prev()
|
||||||
|
}, [resetImageIndex, slider])
|
||||||
|
|
||||||
|
const onNext = React.useCallback(() => {
|
||||||
|
resetImageIndex()
|
||||||
|
slider.current?.next()
|
||||||
|
}, [resetImageIndex, slider])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(s.root, className)} ref={sliderContainerRef}>
|
<div className={cn(s.root, className)} ref={sliderContainerRef}>
|
||||||
@ -114,6 +133,7 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
}),
|
}),
|
||||||
id: `thumb-${idx}`,
|
id: `thumb-${idx}`,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
resetImageIndex()
|
||||||
slider.current?.moveToIdx(idx)
|
slider.current?.moveToIdx(idx)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
.relatedProductsGrid {
|
||||||
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
|
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,71 +1,27 @@
|
|||||||
import cn from 'clsx'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import type { Product } from '@commerce/types/product'
|
import type { Product } from '@commerce/types/product'
|
||||||
import usePrice from '@framework/product/use-price'
|
|
||||||
import { WishlistButton } from '@components/wishlist'
|
import { ProductCard } from '@components/product'
|
||||||
import { ProductSlider, ProductCard } from '@components/product'
|
|
||||||
import { Container, Text } from '@components/ui'
|
import { Container, Text } from '@components/ui'
|
||||||
|
import { ProductProvider } from '../product-context'
|
||||||
|
import ProductDetails from '../ProductDetails/ProductDetails'
|
||||||
import { SEO } from '@components/common'
|
import { SEO } from '@components/common'
|
||||||
import ProductSidebar from '../ProductSidebar'
|
|
||||||
import ProductTag from '../ProductTag'
|
|
||||||
interface ProductViewProps {
|
interface ProductViewProps {
|
||||||
product: Product
|
product: Product
|
||||||
relatedProducts: Product[]
|
relatedProducts: Product[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
||||||
const { price } = usePrice({
|
|
||||||
amount: product.price.value,
|
|
||||||
baseAmount: product.price.retailPrice,
|
|
||||||
currencyCode: product.price.currencyCode!,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container className="max-w-none w-full" clean>
|
<Container className="max-w-none w-full" clean>
|
||||||
<div className={cn(s.root, 'fit')}>
|
|
||||||
<div className={cn(s.main, 'fit')}>
|
|
||||||
<ProductTag
|
|
||||||
name={product.name}
|
|
||||||
price={`${price} ${product.price?.currencyCode}`}
|
|
||||||
fontSize={32}
|
|
||||||
/>
|
|
||||||
<div className={s.sliderContainer}>
|
|
||||||
<ProductSlider key={product.id}>
|
|
||||||
{product.images.map((image, i) => (
|
|
||||||
<div key={image.url} className={s.imageContainer}>
|
|
||||||
<Image
|
|
||||||
className={s.img}
|
|
||||||
src={image.url!}
|
|
||||||
alt={image.alt || 'Product Image'}
|
|
||||||
width={600}
|
|
||||||
height={600}
|
|
||||||
priority={i === 0}
|
|
||||||
quality="85"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ProductSlider>
|
|
||||||
</div>
|
|
||||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
|
||||||
<WishlistButton
|
|
||||||
className={s.wishlistButton}
|
|
||||||
productId={product.id}
|
|
||||||
variant={product.variants[0]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ProductSidebar
|
|
||||||
key={product.id}
|
|
||||||
product={product}
|
|
||||||
className={s.sidebar}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr className="mt-7 border-accent-2" />
|
<hr className="mt-7 border-accent-2" />
|
||||||
<section className="py-12 px-6 mb-10">
|
<section className="py-12 px-6 mb-10">
|
||||||
|
<ProductProvider product={product}>
|
||||||
|
<ProductDetails />
|
||||||
|
</ProductProvider>
|
||||||
<Text variant="sectionHeading">Related Products</Text>
|
<Text variant="sectionHeading">Related Products</Text>
|
||||||
<div className={s.relatedProductsGrid}>
|
<div className={s.relatedProductsGrid}>
|
||||||
{relatedProducts.map((p) => (
|
{relatedProducts.map((p) => (
|
||||||
@ -90,12 +46,12 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
|||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
<SEO
|
<SEO
|
||||||
title={product.name}
|
title={product.seo?.title || product.name}
|
||||||
description={product.description}
|
description={product.seo?.description || product.description}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
type: 'website',
|
type: 'website',
|
||||||
title: product.name,
|
title: product.seo?.title || product.name,
|
||||||
description: product.description,
|
description: product.seo?.description || product.description,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: product.images[0]?.url!,
|
url: product.images[0]?.url!,
|
||||||
|
95
site/components/product/product-context.tsx
Normal file
95
site/components/product/product-context.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Product, ProductImage, ProductVariant } from '@commerce/types/product'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getProductVariant,
|
||||||
|
selectDefaultOptionFromProduct,
|
||||||
|
SelectedOptions,
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import usePrice from '@framework/product/use-price'
|
||||||
|
|
||||||
|
export interface ProductContextValue {
|
||||||
|
product: Product
|
||||||
|
imageIndex: number | null
|
||||||
|
price: string
|
||||||
|
variant: ProductVariant
|
||||||
|
selectedOptions: SelectedOptions
|
||||||
|
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||||
|
resetImageIndex: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductContext = React.createContext<ProductContextValue | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
ProductContext.displayName = 'ProductContext'
|
||||||
|
|
||||||
|
type ProductProviderProps = {
|
||||||
|
product: Product
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductProvider: FC<ProductProviderProps> = ({
|
||||||
|
product,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
|
||||||
|
const [imageIndex, setImageIndex] = useState<number | null>(null)
|
||||||
|
const resetImageIndex = useCallback(() => setImageIndex(null), [])
|
||||||
|
|
||||||
|
const variant = useMemo(() => {
|
||||||
|
const v = getProductVariant(product, selectedOptions)
|
||||||
|
return v || product.variants[0]
|
||||||
|
}, [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!,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||||
|
}, [product])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const idx = product.images.findIndex(
|
||||||
|
(image: ProductImage) => image.url === variant?.image?.url
|
||||||
|
)
|
||||||
|
if (idx) {
|
||||||
|
setImageIndex(idx)
|
||||||
|
}
|
||||||
|
}, [variant, product])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProductContext.Provider
|
||||||
|
value={{
|
||||||
|
price,
|
||||||
|
product,
|
||||||
|
variant,
|
||||||
|
imageIndex,
|
||||||
|
resetImageIndex,
|
||||||
|
selectedOptions,
|
||||||
|
setSelectedOptions,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ProductContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProduct = () => {
|
||||||
|
const context = React.useContext(ProductContext) as ProductContextValue
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(`useProduct must be used within a ProductProvider`)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user