forked from crowetic/commerce
Select variant in product page
This commit is contained in:
parent
3ee325da26
commit
2f37fa46a9
@ -4,7 +4,6 @@ import Image from 'next/image'
|
|||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { Heart } from '@components/icons'
|
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
import { Button, Container } from '@components/ui'
|
import { Button, Container } from '@components/ui'
|
||||||
@ -12,7 +11,11 @@ import { HTMLContent } from '@components/core'
|
|||||||
|
|
||||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
import { getProductOptions } from '../helpers'
|
import {
|
||||||
|
getCurrentVariant,
|
||||||
|
getProductOptions,
|
||||||
|
ProductOptions,
|
||||||
|
} from '../helpers'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -21,16 +24,17 @@ interface Props {
|
|||||||
product: ProductNode
|
product: ProductNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<Props> = ({ product, className }) => {
|
const ProductView: FC<Props> = ({ product }) => {
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
const { openSidebar } = useUI()
|
const { openSidebar } = useUI()
|
||||||
const options = getProductOptions(product)
|
const options = getProductOptions(product)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [choices, setChoices] = useState<ProductOptions>({
|
||||||
const [choices, setChoices] = useState<Record<string, any>>({
|
|
||||||
size: null,
|
size: null,
|
||||||
color: null,
|
color: null,
|
||||||
})
|
})
|
||||||
|
const variant =
|
||||||
|
getCurrentVariant(product, choices) || product.variants.edges?.[0]
|
||||||
|
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -102,7 +106,7 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
||||||
<div className="flex flex-row py-4">
|
<div className="flex flex-row py-4">
|
||||||
{opt.values.map((v: any, i: number) => {
|
{opt.values.map((v: any, i: number) => {
|
||||||
const active = choices[opt.displayName]
|
const active = (choices as any)[opt.displayName]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Swatch
|
<Swatch
|
||||||
@ -137,6 +141,7 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
className={s.button}
|
className={s.button}
|
||||||
onClick={addToCart}
|
onClick={addToCart}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
disabled={!variant}
|
||||||
>
|
>
|
||||||
Add to Cart
|
Add to Cart
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,10 +1,51 @@
|
|||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
|
|
||||||
|
export type SelectedOptions = {
|
||||||
|
size: string | null
|
||||||
|
color: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductOption = {
|
||||||
|
displayName: string
|
||||||
|
values: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the available options of a product
|
||||||
export function getProductOptions(product: ProductNode) {
|
export function getProductOptions(product: ProductNode) {
|
||||||
const options = product.productOptions.edges?.map(({ node }: any) => ({
|
const options = product.productOptions.edges?.reduce<ProductOption[]>(
|
||||||
displayName: node.displayName.toLowerCase(),
|
(arr, edge) => {
|
||||||
values: node.values.edges?.map(({ node }: any) => node),
|
if (edge?.node.__typename === 'MultipleChoiceOption') {
|
||||||
}))
|
arr.push({
|
||||||
|
displayName: edge.node.displayName.toLowerCase(),
|
||||||
|
values: edge.node.values.edges?.map((edge) => edge?.node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finds a variant in the product that matches the selected options
|
||||||
|
export function getCurrentVariant(product: ProductNode, opts: SelectedOptions) {
|
||||||
|
const variant = product.variants.edges?.find((edge) => {
|
||||||
|
const { node } = edge ?? {}
|
||||||
|
|
||||||
|
return Object.entries(opts).every(([key, value]) =>
|
||||||
|
node?.productOptions.edges?.find((edge) => {
|
||||||
|
if (
|
||||||
|
edge?.node.__typename === 'MultipleChoiceOption' &&
|
||||||
|
edge.node.displayName.toLowerCase() === key
|
||||||
|
) {
|
||||||
|
return edge.node.values.edges?.find(
|
||||||
|
(valueEdge) => valueEdge?.node.label === value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return variant
|
||||||
|
}
|
||||||
|
@ -1,3 +1,20 @@
|
|||||||
|
export const productPrices = /* GraphQL */ `
|
||||||
|
fragment productPrices on Prices {
|
||||||
|
price {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
salePrice {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
retailPrice {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const swatchOptionFragment = /* GraphQL */ `
|
export const swatchOptionFragment = /* GraphQL */ `
|
||||||
fragment swatchOption on SwatchOptionValue {
|
fragment swatchOption on SwatchOptionValue {
|
||||||
isDefault
|
isDefault
|
||||||
@ -7,7 +24,6 @@ export const swatchOptionFragment = /* GraphQL */ `
|
|||||||
|
|
||||||
export const multipleChoiceOptionFragment = /* GraphQL */ `
|
export const multipleChoiceOptionFragment = /* GraphQL */ `
|
||||||
fragment multipleChoiceOption on MultipleChoiceOption {
|
fragment multipleChoiceOption on MultipleChoiceOption {
|
||||||
entityId
|
|
||||||
values {
|
values {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@ -31,18 +47,7 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
description
|
description
|
||||||
prices {
|
prices {
|
||||||
price {
|
...productPrices
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
salePrice {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
retailPrice {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
images {
|
images {
|
||||||
edges {
|
edges {
|
||||||
@ -68,6 +73,7 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
productOptions {
|
productOptions {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
__typename
|
||||||
entityId
|
entityId
|
||||||
displayName
|
displayName
|
||||||
...multipleChoiceOption
|
...multipleChoiceOption
|
||||||
@ -85,6 +91,7 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${productPrices}
|
||||||
${multipleChoiceOptionFragment}
|
${multipleChoiceOptionFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -16,6 +16,38 @@ export const getProductQuery = /* GraphQL */ `
|
|||||||
__typename
|
__typename
|
||||||
... on Product {
|
... on Product {
|
||||||
...productInfo
|
...productInfo
|
||||||
|
variants {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
entityId
|
||||||
|
defaultImage {
|
||||||
|
urlOriginal
|
||||||
|
altText
|
||||||
|
isDefault
|
||||||
|
}
|
||||||
|
prices {
|
||||||
|
...productPrices
|
||||||
|
}
|
||||||
|
inventory {
|
||||||
|
aggregated {
|
||||||
|
availableToSell
|
||||||
|
warningLevel
|
||||||
|
}
|
||||||
|
isInStock
|
||||||
|
}
|
||||||
|
productOptions {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
__typename
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
...multipleChoiceOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
118
lib/bigcommerce/schema.d.ts
vendored
118
lib/bigcommerce/schema.d.ts
vendored
@ -1684,6 +1684,16 @@ export type CategoryTreeItemFragment = {
|
|||||||
'entityId' | 'name' | 'path' | 'description' | 'productCount'
|
'entityId' | 'name' | 'path' | 'description' | 'productCount'
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export type ProductPricesFragment = { __typename?: 'Prices' } & {
|
||||||
|
price: { __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
||||||
|
salePrice?: Maybe<
|
||||||
|
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
||||||
|
>
|
||||||
|
retailPrice?: Maybe<
|
||||||
|
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
export type SwatchOptionFragment = { __typename?: 'SwatchOptionValue' } & Pick<
|
export type SwatchOptionFragment = { __typename?: 'SwatchOptionValue' } & Pick<
|
||||||
SwatchOptionValue,
|
SwatchOptionValue,
|
||||||
'isDefault' | 'hexColors'
|
'isDefault' | 'hexColors'
|
||||||
@ -1723,17 +1733,7 @@ export type ProductInfoFragment = { __typename?: 'Product' } & Pick<
|
|||||||
'entityId' | 'name' | 'path' | 'description'
|
'entityId' | 'name' | 'path' | 'description'
|
||||||
> & {
|
> & {
|
||||||
brand?: Maybe<{ __typename?: 'Brand' } & Pick<Brand, 'entityId'>>
|
brand?: Maybe<{ __typename?: 'Brand' } & Pick<Brand, 'entityId'>>
|
||||||
prices?: Maybe<
|
prices?: Maybe<{ __typename?: 'Prices' } & ProductPricesFragment>
|
||||||
{ __typename?: 'Prices' } & {
|
|
||||||
price: { __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
|
||||||
salePrice?: Maybe<
|
|
||||||
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
|
||||||
>
|
|
||||||
retailPrice?: Maybe<
|
|
||||||
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
|
||||||
>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
images: { __typename?: 'ImageConnection' } & {
|
images: { __typename?: 'ImageConnection' } & {
|
||||||
edges?: Maybe<
|
edges?: Maybe<
|
||||||
Array<
|
Array<
|
||||||
@ -1904,7 +1904,101 @@ export type GetProductQuery = { __typename?: 'Query' } & {
|
|||||||
node?: Maybe<
|
node?: Maybe<
|
||||||
| { __typename: 'Brand' }
|
| { __typename: 'Brand' }
|
||||||
| { __typename: 'Category' }
|
| { __typename: 'Category' }
|
||||||
| ({ __typename: 'Product' } & ProductInfoFragment)
|
| ({ __typename: 'Product' } & {
|
||||||
|
variants: { __typename?: 'VariantConnection' } & {
|
||||||
|
edges?: Maybe<
|
||||||
|
Array<
|
||||||
|
Maybe<
|
||||||
|
{ __typename?: 'VariantEdge' } & {
|
||||||
|
node: { __typename?: 'Variant' } & Pick<
|
||||||
|
Variant,
|
||||||
|
'entityId'
|
||||||
|
> & {
|
||||||
|
defaultImage?: Maybe<
|
||||||
|
{ __typename?: 'Image' } & Pick<
|
||||||
|
Image,
|
||||||
|
'urlOriginal' | 'altText' | 'isDefault'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
prices?: Maybe<
|
||||||
|
{ __typename?: 'Prices' } & ProductPricesFragment
|
||||||
|
>
|
||||||
|
inventory?: Maybe<
|
||||||
|
{ __typename?: 'VariantInventory' } & Pick<
|
||||||
|
VariantInventory,
|
||||||
|
'isInStock'
|
||||||
|
> & {
|
||||||
|
aggregated?: Maybe<
|
||||||
|
{ __typename?: 'Aggregated' } & Pick<
|
||||||
|
Aggregated,
|
||||||
|
'availableToSell' | 'warningLevel'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
productOptions: {
|
||||||
|
__typename?: 'ProductOptionConnection'
|
||||||
|
} & {
|
||||||
|
edges?: Maybe<
|
||||||
|
Array<
|
||||||
|
Maybe<
|
||||||
|
{ __typename?: 'ProductOptionEdge' } & {
|
||||||
|
node:
|
||||||
|
| ({
|
||||||
|
__typename?: 'CheckboxOption'
|
||||||
|
} & Pick<
|
||||||
|
CheckboxOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
>)
|
||||||
|
| ({
|
||||||
|
__typename?: 'DateFieldOption'
|
||||||
|
} & Pick<
|
||||||
|
DateFieldOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
>)
|
||||||
|
| ({
|
||||||
|
__typename?: 'FileUploadFieldOption'
|
||||||
|
} & Pick<
|
||||||
|
FileUploadFieldOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
>)
|
||||||
|
| ({
|
||||||
|
__typename?: 'MultiLineTextFieldOption'
|
||||||
|
} & Pick<
|
||||||
|
MultiLineTextFieldOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
>)
|
||||||
|
| ({
|
||||||
|
__typename?: 'MultipleChoiceOption'
|
||||||
|
} & Pick<
|
||||||
|
MultipleChoiceOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
> &
|
||||||
|
MultipleChoiceOptionFragment)
|
||||||
|
| ({
|
||||||
|
__typename?: 'NumberFieldOption'
|
||||||
|
} & Pick<
|
||||||
|
NumberFieldOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
>)
|
||||||
|
| ({
|
||||||
|
__typename?: 'TextFieldOption'
|
||||||
|
} & Pick<
|
||||||
|
TextFieldOption,
|
||||||
|
'entityId' | 'displayName'
|
||||||
|
>)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
} & ProductInfoFragment)
|
||||||
| { __typename: 'Variant' }
|
| { __typename: 'Variant' }
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user