Skip to content
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

Open
wants to merge 18 commits into
base: feat/quick-order
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { forwardRef } from 'react'
import React, { forwardRef, useCallback } from 'react'
import { ProductPrice } from '../..'
import SearchProductItemControl from './SearchProductItemControl'

import type { PriceDefinition } from '../../typings/PriceDefinition'

Expand All @@ -12,23 +13,63 @@ export interface SearchProductItemContentProps {
* Specifies product's prices.
*/
price: PriceDefinition
/**
* Quick order settings.
*/
quickOrder?: {
enabled: boolean
availability: boolean
hasVariants: boolean
skuMatrixControl: React.ReactNode
quantity: number
onChangeQuantity(value: number): void
buyProps?: {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
'data-testid': string
'data-sku': string
'data-seller': string
}
}
}

const SearchProductItemContent = forwardRef<
HTMLElement,
SearchProductItemContentProps
>(function SearchProductItemContent({ price, title, ...otherProps }, ref) {
>(function SearchProductItemContent(
{ price, title, quickOrder, ...otherProps },
ref
) {
const renderProductItemContent = useCallback(() => {
return (
<>
<p data-fs-search-product-item-title>{title}</p>
{price.value !== 0 && (
<ProductPrice
data-fs-search-product-item-prices
listPrice={price.listPrice}
value={price.value}
formatter={price.formatter}
/>
)}
</>
)
}, [quickOrder?.enabled])

Copy link
Contributor

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 the useCallback scope. Maybe by creating a function outside of the SearchProductItemContent scope can solve this, because the conditions at lines 60 and 62 will already prevent the unexpected re-renderings.

return (
<section ref={ref} data-fs-search-product-item-content {...otherProps}>
<p data-fs-search-product-item-title>{title}</p>
{!quickOrder?.enabled && renderProductItemContent()}

{price.value !== 0 && (
<ProductPrice
data-fs-search-product-item-prices
listPrice={price.listPrice}
value={price.value}
formatter={price.formatter}
/>
{quickOrder?.enabled && (
<SearchProductItemControl
availability={quickOrder.availability}
hasVariants={quickOrder.hasVariants}
skuMatrixControl={quickOrder.skuMatrixControl}
quantity={quickOrder.quantity}
onChangeQuantity={quickOrder.onChangeQuantity}
{...quickOrder.buyProps}
>
{renderProductItemContent()}
</SearchProductItemControl>
)}
</section>
)
Expand Down
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
Copy link
Contributor

@lucasfp13 lucasfp13 Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we can use this value from useScreenResize hook instead.


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,
Expand All @@ -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 = {
/**
Expand All @@ -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 {
Expand All @@ -36,13 +44,31 @@ function SearchProductItem({
index,
})

const [quantity, setQuantity] = useState<number>(1)

const {
id,
sku,
gtin,
brand,
isVariantOf,
isVariantOf: { name },
unitMultiplier,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are unavailable attributes from this fragment, causing the Property does not exist on type 'ProductSummary_ProductFragment' error.

All the attributes being used here should be added in the ProductSummary_product fragment and generate the GraphQL typings again:

  1. Add the missing attributes to the ProductSummary_product fragment;
  2. Run yarn generate:codegen on the @faststore/core package

image: [img],
offers: {
lowPrice: spotPrice,
offers: [{ listPrice }],
offers: [
{
listPrice,
availability,
price,
listPriceWithTaxes,
seller,
priceWithTaxes,
},
],
},
additionalProperty,
} = product

const linkProps = {
Expand All @@ -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>
Expand All @@ -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>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
@import "@faststore/ui/src/components/atoms/Badge/styles.scss";
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
@import "@faststore/ui/src/components/atoms/Icon/styles.scss";
@import "@faststore/ui/src/components/atoms/Loader/styles.scss";
@import "@faststore/ui/src/components/atoms/Input/styles.scss";
@import "@faststore/ui/src/components/atoms/Link/styles.scss";
@import "@faststore/ui/src/components/atoms/List/styles.scss";
@import "@faststore/ui/src/components/atoms/Logo/styles.scss";
@import "@faststore/ui/src/components/atoms/Price/styles.scss";
@import "@faststore/ui/src/components/molecules/LinkButton/styles.scss";
@import "@faststore/ui/src/components/molecules/QuantitySelector/styles.scss";
@import "@faststore/ui/src/components/molecules/NavbarLinks/styles.scss";
@import "@faststore/ui/src/components/molecules/ProductPrice/styles.scss";
@import "@faststore/ui/src/components/molecules/SearchAutoComplete/styles.scss";
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/sdk/cart/useBuyButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useUI } from '@faststore/ui'
import { useSession } from '../session'
import { cartStore } from './index'

export const useBuyButton = (item: CartItem | null) => {
export const useBuyButton = (item: CartItem | null, shouldOpenCart = true) => {
const { openCart } = useUI()
const {
currency: { code },
Expand Down Expand Up @@ -49,7 +49,10 @@ export const useBuyButton = (item: CartItem | null) => {
})

cartStore.addItem(item)
openCart()

if (shouldOpenCart) {
openCart()
}
},
[code, item, openCart]
)
Expand Down
Loading
Loading