-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: search product control implementations #2547
base: feat/quick-order
Are you sure you want to change the base?
Changes from all commits
37d34d4
7255092
a3177b0
28e054a
09f0b28
5a7bf2b
e02f24c
2c85f5d
5e39413
18c5d5d
8207add
f8573ff
d5f06a0
d774a38
e63111b
ba7344b
8f915ea
e0bb7f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React, { forwardRef, HTMLAttributes } from 'react' | ||
import { Badge, Icon, IconButton, Input, Loader, QuantitySelector } from '../..' | ||
type StatusButtonAddToCartType = 'default' | 'inProgress' | 'completed' | ||
|
||
export interface SearchProductItemControlProps | ||
extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onClick'> { | ||
children: React.ReactNode | ||
availability: boolean | ||
hasVariants: boolean | ||
skuMatrixControl: React.ReactNode | ||
quantity: number | ||
onClick?(e: React.MouseEvent<HTMLButtonElement>): void | ||
onChangeQuantity(value: number): void | ||
} | ||
|
||
const SearchProductItemControl = forwardRef< | ||
HTMLDivElement, | ||
SearchProductItemControlProps | ||
>(function SearchProductItemControl( | ||
{ | ||
availability, | ||
children, | ||
hasVariants, | ||
skuMatrixControl, | ||
quantity, | ||
onClick, | ||
onChangeQuantity, | ||
...otherProps | ||
}, | ||
ref | ||
) { | ||
const [statusAddToCart, setStatusAddToCart] = | ||
React.useState<StatusButtonAddToCartType>('default') | ||
function stopPropagationClick(e: React.MouseEvent) { | ||
e.preventDefault() | ||
e.stopPropagation() | ||
} | ||
function handleAddToCart(event: React.MouseEvent<HTMLButtonElement>) { | ||
if (onClick) { | ||
setStatusAddToCart('inProgress') | ||
|
||
setTimeout(() => { | ||
setStatusAddToCart('completed') | ||
onClick(event) | ||
}, 1000) | ||
|
||
setTimeout(() => { | ||
setStatusAddToCart('default') | ||
onChangeQuantity(1) | ||
}, 2000) | ||
} | ||
} | ||
|
||
const getIcon = React.useCallback(() => { | ||
switch (statusAddToCart) { | ||
case 'inProgress': | ||
return <Loader /> | ||
case 'completed': | ||
return <Icon name="Checked" width={24} height={24} /> | ||
default: | ||
return <Icon name="ShoppingCart" width={24} height={24} /> | ||
} | ||
}, [statusAddToCart]) | ||
|
||
const showSKUMatrixControl = availability && hasVariants | ||
const isMobile = window.innerWidth <= 768 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we can use this value from |
||
|
||
return ( | ||
<div ref={ref} data-fs-search-product-item-control {...otherProps}> | ||
<div data-fs-search-product-item-control-content> | ||
{!availability && ( | ||
<Badge data-fs-search-product-item-control-badge variant="warning"> | ||
Out of Stock | ||
</Badge> | ||
)} | ||
{children} | ||
</div> | ||
{availability && !hasVariants && ( | ||
<div | ||
data-fs-search-product-item-control-actions | ||
role="group" | ||
onClick={stopPropagationClick} | ||
> | ||
{!isMobile && ( | ||
<QuantitySelector | ||
disabled={statusAddToCart !== 'default'} | ||
initial={quantity} | ||
onChange={onChangeQuantity} | ||
/> | ||
)} | ||
|
||
{isMobile && ( | ||
<Input | ||
data-fs-product-item-control-input | ||
type="number" | ||
min={1} | ||
value={quantity} | ||
onChange={(e) => onChangeQuantity(e.target.valueAsNumber)} | ||
/> | ||
)} | ||
|
||
<IconButton | ||
variant="primary" | ||
aria-label="Add product to cart" | ||
onClick={handleAddToCart} | ||
disabled={statusAddToCart === 'inProgress'} | ||
icon={getIcon()} | ||
/> | ||
</div> | ||
)} | ||
|
||
{showSKUMatrixControl && ( | ||
<div onClick={stopPropagationClick}>{skuMatrixControl}</div> | ||
)} | ||
</div> | ||
) | ||
}) | ||
export default SearchProductItemControl |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import { | ||
Button, | ||
SearchProductItem as UISearchProductItem, | ||
SearchProductItemContent as UISearchProductItemContent, | ||
SearchProductItemImage as UISearchProductItemImage, | ||
|
@@ -9,6 +10,8 @@ import { Image } from 'src/components/ui/Image' | |
import { useFormattedPrice } from 'src/sdk/product/useFormattedPrice' | ||
import { useProductLink } from 'src/sdk/product/useProductLink' | ||
import type { ProductSummary_ProductFragment } from '@generated/graphql' | ||
import { useMemo, useState } from 'react' | ||
import { useBuyButton } from 'src/sdk/cart/useBuyButton' | ||
|
||
type SearchProductItemProps = { | ||
/** | ||
|
@@ -19,11 +22,16 @@ type SearchProductItemProps = { | |
* Index to generate product link. | ||
*/ | ||
index: number | ||
/** | ||
* Enable Quick Order. | ||
*/ | ||
quickOrder?: boolean | ||
} | ||
|
||
function SearchProductItem({ | ||
product, | ||
index, | ||
quickOrder, | ||
...otherProps | ||
}: SearchProductItemProps) { | ||
const { | ||
|
@@ -36,13 +44,31 @@ function SearchProductItem({ | |
index, | ||
}) | ||
|
||
const [quantity, setQuantity] = useState<number>(1) | ||
|
||
const { | ||
id, | ||
sku, | ||
gtin, | ||
brand, | ||
isVariantOf, | ||
isVariantOf: { name }, | ||
unitMultiplier, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are unavailable attributes from this fragment, causing the All the attributes being used here should be added in the
|
||
image: [img], | ||
offers: { | ||
lowPrice: spotPrice, | ||
offers: [{ listPrice }], | ||
offers: [ | ||
{ | ||
listPrice, | ||
availability, | ||
price, | ||
listPriceWithTaxes, | ||
seller, | ||
priceWithTaxes, | ||
}, | ||
], | ||
}, | ||
additionalProperty, | ||
} = product | ||
|
||
const linkProps = { | ||
|
@@ -54,6 +80,43 @@ function SearchProductItem({ | |
...baseLinkProps, | ||
} | ||
|
||
const outOfStock = useMemo( | ||
() => availability === 'https://schema.org/OutOfStock', | ||
[availability] | ||
) | ||
|
||
const hasVariants = useMemo( | ||
() => | ||
Boolean( | ||
Object.keys(product.isVariantOf.skuVariants.allVariantsByName).length | ||
), | ||
|
||
[product] | ||
) | ||
|
||
const buyProps = useBuyButton( | ||
{ | ||
id, | ||
price, | ||
priceWithTaxes, | ||
listPrice, | ||
listPriceWithTaxes, | ||
seller, | ||
quantity, | ||
itemOffered: { | ||
sku, | ||
name, | ||
gtin, | ||
image: [img], | ||
brand, | ||
isVariantOf, | ||
additionalProperty, | ||
unitMultiplier, | ||
}, | ||
}, | ||
false | ||
) | ||
|
||
return ( | ||
<UISearchProductItem linkProps={linkProps} {...otherProps}> | ||
<UISearchProductItemImage> | ||
|
@@ -66,6 +129,18 @@ function SearchProductItem({ | |
listPrice: listPrice, | ||
formatter: useFormattedPrice, | ||
}} | ||
quickOrder={{ | ||
enabled: quickOrder, | ||
availability: !outOfStock, | ||
hasVariants, | ||
buyProps, | ||
quantity, | ||
onChangeQuantity: setQuantity, | ||
// FIXME: Use SKU Matrix component | ||
skuMatrixControl: ( | ||
<Button variant="tertiary">Select Multiples</Button> | ||
), | ||
}} | ||
></UISearchProductItemContent> | ||
</UISearchProductItem> | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typescript will complain about the
quickOrder
variable not being used inside theuseCallback
scope. Maybe by creating a function outside of theSearchProductItemContent
scope can solve this, because the conditions at lines 60 and 62 will already prevent the unexpected re-renderings.