@hanzo/commerce 6.4.7 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ import {
16
16
  Barcode
17
17
  } from "lucide-react"
18
18
 
19
- export const Icons = {
19
+ export default {
20
20
  shoppingCart: ShoppingCart,
21
21
  menu: Menu,
22
22
  chevronLeft: ChevronLeft,
@@ -1,16 +1,19 @@
1
1
  'use client'
2
- import React from 'react'
2
+ import React, { useEffect, useRef } from 'react'
3
+ import { reaction, type IReactionDisposer } from 'mobx'
3
4
  import { observer } from 'mobx-react-lite'
4
5
 
5
- import { Button, toast, type ButtonSizes, type ButtonVariants } from '@hanzo/ui/primitives'
6
- import { cn } from '@hanzo/ui/util'
6
+ import { Button, buttonVariants } from '@hanzo/ui/primitives'
7
+ import { cn, type VariantProps } from '@hanzo/ui/util'
7
8
 
8
- import { Icons } from '../Icons'
9
- import type { LineItem } from '../../types'
10
- import { sendFBEvent, sendGAEvent } from '../../util/analytics'
9
+ import Icons from './Icons'
10
+ import type { LineItem } from '../types'
11
+ import { sendFBEvent, sendGAEvent } from '../util/analytics'
12
+ import { useCommerceUI } from '..'
11
13
 
12
14
  const AddToCartWidget: React.FC<{
13
15
  item: LineItem
16
+ registerAdd?: boolean
14
17
  disabled?: boolean
15
18
  className?: string
16
19
  buttonClx?: string
@@ -19,12 +22,36 @@ const AddToCartWidget: React.FC<{
19
22
  }> = observer(({
20
23
  item,
21
24
  variant='primary',
25
+ registerAdd=true,
22
26
  disabled=false,
23
27
  className='',
24
28
  buttonClx='',
25
29
  onQuantityChanged
26
30
  }) => {
27
31
 
32
+ const ui = useCommerceUI()
33
+
34
+ const reactionDisposer = useRef<IReactionDisposer | undefined>(undefined)
35
+
36
+ useEffect(() => {
37
+ // Only tell the micro-drawer
38
+ // if we're not part of the cart ui,
39
+ // or part of the main drawer.
40
+ if (registerAdd && variant !== 'minimal') {
41
+ reactionDisposer.current = reaction(
42
+ () => (item.quantity),
43
+ (quantity: number, previous: number) => {
44
+ ui.itemQuantityChanged(item, quantity, previous)
45
+ }
46
+ )
47
+ }
48
+ return () => {
49
+ if (reactionDisposer.current) {
50
+ reactionDisposer.current()
51
+ }
52
+ }
53
+ }, [])
54
+
28
55
  const ROUNDED_VAL = 'lg'
29
56
  // no need to safelist, since its used widely
30
57
  const ROUNDED_CLX = ` rounded-${ROUNDED_VAL} `
@@ -149,7 +176,7 @@ const AddToCartWidget: React.FC<{
149
176
  <Button
150
177
  aria-label={'Add a ' + item.title + ' to cart'}
151
178
  size={ghost ? 'xs' : 'default'}
152
- variant={variant as ButtonVariants}
179
+ variant={variant === 'minimal' ? 'ghost' : (variant as VariantProps<typeof buttonVariants>['variant'])}
153
180
  rounded={ROUNDED_VAL}
154
181
  className={cn(buttonClx, className)}
155
182
  onClick={inc}
@@ -1,31 +1,35 @@
1
1
  'use client'
2
2
  import React, {type PropsWithChildren} from 'react'
3
3
 
4
- import { type ButtonVariants, type ButtonSizes, Button } from '@hanzo/ui/primitives'
4
+ import { Button, buttonVariants } from '@hanzo/ui/primitives'
5
+ import { type VariantProps } from '@hanzo/ui/util'
5
6
 
6
- import BuyTriggerWrapper from './buy-trigger-wrapper'
7
7
  import { cn } from '@hanzo/ui/util'
8
+ import { useCommerceUI } from '../..'
8
9
 
9
- const BuyButton: React.FC<PropsWithChildren & {
10
- skuPath: string
11
- variant? : ButtonVariants
12
- size?: ButtonSizes
13
- /* rounded?: ButtonRoundedValue // TODO: wait for version bump*/
14
- className?: string
15
- mobile?: boolean
16
- }> = ({
10
+ const BuyButton: React.FC<
11
+ PropsWithChildren &
12
+ VariantProps<typeof buttonVariants> &
13
+ {
14
+ skuPath: string
15
+ className?: string
16
+ }
17
+ > = ({
17
18
  skuPath,
18
- variant,
19
- size,
20
19
  children,
21
20
  className='',
22
- mobile=false
23
- }) => (
24
- <BuyTriggerWrapper skuPath={skuPath} mobile={mobile}
25
- trigger={ <Button size={size} variant={variant} className={cn(className, '')}>{children}</Button> }
26
- />
27
- )
21
+ ...rest
22
+ }) => {
28
23
 
24
+ const ui = useCommerceUI()
25
+ const handleClick = () => { ui.showBuyOptions(skuPath) }
26
+
27
+ return (
28
+ <Button onClick={handleClick} {...rest} className={cn(className, '')}>
29
+ {children}
30
+ </Button>
31
+ )
32
+ }
29
33
 
30
34
 
31
35
  export default BuyButton
@@ -19,7 +19,7 @@ import * as pathUtils from '../../service/path-utils'
19
19
  import { getFacetValuesMutator, ObsStringMutator } from '../../util'
20
20
 
21
21
  import NodeTabs from '../node-tabs'
22
- import AddToCartWidget from './add-to-cart-widget'
22
+ import AddToCartWidget from '../add-to-cart-widget'
23
23
 
24
24
  const BuyCard: React.FC<{
25
25
  skuPath: string
@@ -215,7 +215,7 @@ const BuyCard: React.FC<{
215
215
  mobile={mobile}
216
216
  mutator={allVariants ?
217
217
  {
218
- get: () => (inst.current!.currentFamTokenMutator.s),
218
+ get: () => (inst.current!.currentFamTokenMutator.get()),
219
219
  set: setFamilyPath
220
220
  }
221
221
  :
@@ -248,6 +248,7 @@ const BuyCard: React.FC<{
248
248
  {(cmmc.currentItem) && (
249
249
  <AddToCartWidget
250
250
  item={cmmc.currentItem}
251
+ registerAdd={false}
251
252
  onQuantityChanged={onQuantityChanged}
252
253
  className={cn('min-w-[160px] mx-auto mt-4', (scroll ? 'shrink-0' : ''), addWidgetClx)}
253
254
  />
@@ -8,7 +8,6 @@ import React, {
8
8
  import { observer } from 'mobx-react-lite'
9
9
 
10
10
  import { cn } from '@hanzo/ui/util'
11
- import { Button } from '@hanzo/ui/primitives'
12
11
 
13
12
  import type {
14
13
  ItemSelectorProps,
@@ -29,7 +28,7 @@ import { CarouselItemSelector, ButtonItemSelector } from '../item-selector'
29
28
  import SingleFamilySelector from './single-family-selector'
30
29
  import { FamilyCarousel, AllVariantsCarousel } from './multi-family'
31
30
 
32
- import AddToCartWidget from './add-to-cart-widget'
31
+ import AddToCartWidget from '../add-to-cart-widget'
33
32
 
34
33
  const SCROLL = {
35
34
  scrollAfter: 5,
@@ -52,15 +51,19 @@ const sortItems = (items: LineItem[], sort: 'asc' | 'desc' | 'none'): LineItem[]
52
51
 
53
52
  const CarouselBuyCard: React.FC<{
54
53
  skuPath: string
54
+ checkoutButton: React.ReactNode
55
55
  clx?: string
56
+ selectorClx?: string
57
+ addBtnClx?: string
56
58
  mobile?: boolean
57
- handleCheckout: () => void
58
59
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
59
60
  }> = ({
60
61
  skuPath,
62
+ checkoutButton,
61
63
  clx='',
64
+ selectorClx='',
65
+ addBtnClx='',
62
66
  mobile=false,
63
- handleCheckout,
64
67
  onQuantityChanged,
65
68
  }) => {
66
69
 
@@ -92,6 +95,12 @@ const CarouselBuyCard: React.FC<{
92
95
 
93
96
  useEffect(() => {
94
97
 
98
+ if (!skuPath || skuPath.length === 0 ) {
99
+ // The component is being hidden (w an amination)
100
+ // keep things the same so no layout jump
101
+ return
102
+ }
103
+
95
104
  const peek = cmmc.peek(skuPath)
96
105
  if (typeof peek === 'string') {
97
106
  throw new Error(peek)
@@ -194,26 +203,18 @@ const CarouselBuyCard: React.FC<{
194
203
  <div className={clx}>
195
204
  <AddToCartWidget
196
205
  item={cmmc.currentItem}
206
+ registerAdd={true}
197
207
  onQuantityChanged={onQuantityChanged}
198
208
  variant={cmmc.cartEmpty ? 'primary' : 'outline'}
199
- className='min-w-[160px] w-full sm:max-w-[320px]'
209
+ className={addBtnClx}
200
210
  />
201
- {!cmmc.cartEmpty && (
202
- <Button
203
- onClick={handleCheckout}
204
- variant='primary'
205
- rounded='lg'
206
- className='min-w-[160px] w-full sm:max-w-[320px]'
207
- >
208
- Checkout
209
- </Button>
210
- )}
211
+ {!cmmc.cartEmpty && checkoutButton}
211
212
  </div>
212
213
  ) : null))
213
214
 
214
215
  return (
215
216
  <div className={cn(
216
- 'px-4 md:px-6 pt-3 pb-4 flex flex-col gap-1 items-center min-h-[40vh]',
217
+ 'px-4 md:px-6 pt-3 pb-4 flex flex-col gap-1 items-center',
217
218
  clx,
218
219
  r.current?.single?.scrollable ? SCROLL.scrollHeightClx : 'h-auto'
219
220
  )}>
@@ -222,13 +223,14 @@ const CarouselBuyCard: React.FC<{
222
223
  {...r.current.single}
223
224
  mediaConstraint={MEDIA_CONSTRAINT}
224
225
  mobile={mobile}
226
+ clx={selectorClx}
225
227
  />
226
228
  ) : (r.current?.multi && r.current.families && /* safegaurd for first render, etc. */ (
227
229
  <MultiFamilyUI
228
230
  {...r.current.multi}
229
231
  families={r.current.families}
230
232
  parent={r.current.node}
231
- clx='max-w-[475px]'
233
+ clx={selectorClx}
232
234
  />
233
235
  ))}
234
236
  <Buttons clx={cn(
@@ -23,7 +23,7 @@ import {
23
23
  accessMultiSelectorOptions
24
24
  } from '../../../util'
25
25
 
26
- import QuantityIndicator from '../../quantity-indicator'
26
+ import QuantityIndicator from '../../item-selector/quantity-indicator'
27
27
  import { ButtonItemSelector, useCommerce } from '../../..'
28
28
 
29
29
  const debugBorder = (c: 'r' | 'g' | 'b', disable: boolean = true): string => {
@@ -208,8 +208,8 @@ const AllVariantsCarousel: React.FC<MultiFamilySelectorProps> = ({
208
208
  const { showItemSwatches } = accessMultiSelectorOptions(selectorOptions)
209
209
  if (
210
210
  !showItemSwatches ||
211
- !cmmc.currentFamily ||
212
- cmmc.currentFamily.products.length === 1
211
+ !cmmc.currentFamily
212
+ //|| cmmc.currentFamily.products.length === 1
213
213
  ) {
214
214
  return null
215
215
  }
@@ -242,8 +242,10 @@ const AllVariantsCarousel: React.FC<MultiFamilySelectorProps> = ({
242
242
  </CarouselItem>
243
243
  ))}
244
244
  </CarouselContent>
245
- <CarouselPrevious className='left-1'/>
246
- <CarouselNext className='right-1'/>
245
+ {r.current.items.length > 1 && (<>
246
+ <CarouselPrevious className='left-1'/>
247
+ <CarouselNext className='right-1'/>
248
+ </>)}
247
249
  </Carousel>
248
250
  )}
249
251
  <ItemInfo labelClx='!text-base font-medium'/>
@@ -8,7 +8,7 @@ import { Image } from '@hanzo/ui/primitives'
8
8
 
9
9
  import type { LineItem } from '../../../types'
10
10
  import { formatCurrencyValue } from '../../../util'
11
- import AddToCartWidget from '../../buy/add-to-cart-widget'
11
+ import AddToCartWidget from '../../add-to-cart-widget'
12
12
  import { useCommerce } from '../../../context'
13
13
 
14
14
  const DEF_IMG_SIZE=40
@@ -61,7 +61,7 @@ const CartLineItem: React.FC<{
61
61
  </div>
62
62
  <div className='flex flex-row items-center justify-between w-full'>
63
63
  <div className='flex flex-row items-center'>
64
- <AddToCartWidget variant='minimal' item={item} buttonClx='!h-8 md:!h-6' />
64
+ <AddToCartWidget variant='minimal' registerAdd={false} item={item} buttonClx='!h-8 md:!h-6' />
65
65
  {item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatCurrencyValue(item.price)}</span>)}
66
66
  </div>
67
67
  <div className='flex flex-row gap-1 items-center justify-end'>
@@ -4,8 +4,8 @@ import {
4
4
  FormField,
5
5
  FormItem,
6
6
  FormMessage,
7
- } from '@hanzo/ui/primitives/form'
8
- import { Input } from '@hanzo/ui/primitives'
7
+ Input
8
+ } from '@hanzo/ui/primitives'
9
9
 
10
10
  import type { ContactFormType } from '../../../types'
11
11
 
@@ -1,17 +1,13 @@
1
- export { default as AddToCartWidget } from './buy/add-to-cart-widget'
2
- export { default as BuyTriggerWrapper } from './buy/buy-trigger-wrapper'
1
+ export { default as AddToCartWidget } from './add-to-cart-widget'
3
2
  export { default as BuyButton } from './buy/buy-button'
4
- export { default as BuyCard } from './buy/buy-card'
5
- export { default as BuyDrawer } from './buy/buy-drawer'
3
+ export { default as CarouselBuyCard } from './buy/carousel-buy-card'
6
4
 
7
5
  export { default as CartAccordian } from './cart/cart-accordian'
8
6
  export { default as CartPanel } from './cart/cart-panel'
9
7
 
10
- export { default as NodeTabs } from './node-tabs'
8
+ export { default as Icons } from './Icons'
11
9
  export { default as PaymentStepForm } from './checkout/payment-step-form'
12
- export { default as ShippingStepForm } from './checkout/shipping-step-form'
13
-
14
10
  export { default as ProductCard } from './item/product-card'
15
- export { Icons } from './Icons'
11
+ export { default as ShippingStepForm } from './checkout/shipping-step-form'
16
12
 
17
13
  export * from './item-selector'
@@ -14,7 +14,7 @@ import { cn } from '@hanzo/ui/util'
14
14
  import { formatCurrencyValue } from '../../util'
15
15
  import type { LineItem } from '../../types'
16
16
 
17
- import AddToCartWidget from '../buy/add-to-cart-widget'
17
+ import AddToCartWidget from '../add-to-cart-widget'
18
18
 
19
19
  interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
20
20
  item: LineItem
@@ -39,7 +39,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
39
39
  </CardTitle>
40
40
  </CardContent>
41
41
  <CardFooter className='p-4 flex flex-row justify-center'>
42
- <AddToCartWidget item={item}/>
42
+ <AddToCartWidget item={item} />
43
43
  </CardFooter>
44
44
  </Card>
45
45
  )
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import React, { useEffect, useRef } from 'react'
2
+ import React from 'react'
3
3
  import { observer } from 'mobx-react-lite'
4
4
 
5
5
  import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
@@ -16,7 +16,7 @@ import type { Dimensions } from '@hanzo/ui/types'
16
16
  import type { ItemSelectorProps, LineItem } from '../../types'
17
17
  import { accessItemOptions, formatCurrencyValue } from '../../util'
18
18
 
19
- import QuantityIndicator from '../quantity-indicator'
19
+ import QuantityIndicator from './quantity-indicator'
20
20
 
21
21
  const DEFAULT_CONSTRAINT = {h: 36, w: 72} // // Apple suggest 42px for clickability
22
22
 
@@ -78,6 +78,7 @@ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
78
78
  showFamilyInOption,
79
79
  buttonType,
80
80
  horizButtons,
81
+ showButtonIfOnlyOne
81
82
  } = accessItemOptions(options)
82
83
 
83
84
  const showImage = buttonType !== 'text'
@@ -148,7 +149,7 @@ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
148
149
  )
149
150
  })
150
151
 
151
- return items.length > 1 ? (
152
+ return showButtonIfOnlyOne || items.length > 1 ? (
152
153
  <RadioGroup
153
154
  className={cn(
154
155
  (scrollable ? 'shrink min-h-0 gap-0' : (mobile ? 'gap-3' : 'gap-1')),
@@ -21,7 +21,7 @@ import {
21
21
  import type { ItemSelectorProps, LineItem } from '../../../types'
22
22
  import { formatCurrencyValue, accessItemOptions } from '../../../util'
23
23
 
24
- import QuantityIndicator from '../../quantity-indicator'
24
+ import QuantityIndicator from '../quantity-indicator'
25
25
  import ItemCarouselSlider from './slider'
26
26
 
27
27
  const DEFAULT_CONSTRAINT = {w: 250, h: 250}
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite'
5
5
  import { type LucideProps } from 'lucide-react'
6
6
 
7
7
  import { cn } from '@hanzo/ui/util'
8
- import type { LineItem } from '../types'
8
+ import type { LineItem } from '../../types'
9
9
 
10
10
  // Generalize this.
11
11
  const BagIcon: React.FC<LucideProps> = (props: LucideProps) => (
@@ -4,54 +4,94 @@ import {
4
4
  makeObservable,
5
5
  observable,
6
6
  } from 'mobx'
7
+ import type { CommerceService, LineItem, ObsLineItemRef } from '../types'
7
8
 
8
- interface CommerceUI {
9
+
10
+ interface CommerceUI extends ObsLineItemRef {
9
11
  showBuyOptions: (skuPath: string) => void
10
12
  hideBuyOptions: () => void
13
+ get buyOptionsSkuPath(): string | undefined
11
14
 
12
- get buyOptionsShowing(): boolean
13
-
14
- get skuPath(): string | undefined
15
- clearSkuPath: () => void
15
+ itemQuantityChanged(item: LineItem, val: number, prevVal: number): void
16
16
  }
17
17
 
18
18
  class CommerceUIStore implements CommerceUI {
19
19
 
20
- _skuPath: string | undefined = undefined
21
- _optionsShowing: boolean = false
20
+ static readonly TIMEOUT = 1500
21
+ _buyOptionsSkuPath: string | undefined = undefined
22
+ _paused: boolean = false
23
+ _activeItem: LineItem | undefined = undefined
24
+ _lastActivity: number | undefined = undefined
25
+ _service: CommerceService
22
26
 
23
- constructor() {
27
+ constructor(s: CommerceService) {
28
+ this._service = s
24
29
  makeObservable(this, {
25
- _skuPath: observable,
26
- _optionsShowing: observable,
30
+ _buyOptionsSkuPath: observable,
31
+ _activeItem: observable.shallow,
27
32
  showBuyOptions: action,
28
33
  hideBuyOptions: action,
29
- buyOptionsShowing: computed,
30
- skuPath: computed,
31
- clearSkuPath: action
34
+ buyOptionsSkuPath: computed,
35
+ itemQuantityChanged: action,
36
+ tick: action,
37
+ item: computed
32
38
  })
33
39
  }
34
40
 
35
41
  showBuyOptions = (skuPath: string): void => {
36
- this._skuPath = skuPath
37
- this._optionsShowing = true
42
+ this._service.setCurrentItem(undefined)
43
+ this._buyOptionsSkuPath = skuPath
44
+ this._paused = true
38
45
  }
39
46
 
40
47
  hideBuyOptions = (): void => {
41
- this._optionsShowing = false
48
+ this._buyOptionsSkuPath = undefined
49
+ this._paused = false
50
+ if (this._lastActivity) {
51
+ this._lastActivity = Date.now()
52
+ }
42
53
  }
43
54
 
44
- get buyOptionsShowing(): boolean {
45
- return this._optionsShowing
55
+ get buyOptionsSkuPath(): string | undefined {
56
+ return this._buyOptionsSkuPath
46
57
  }
47
58
 
48
- get skuPath(): string | undefined {
49
- return this._skuPath
59
+ tick = () => {
60
+ if (
61
+ !this._paused
62
+ &&
63
+ this._lastActivity
64
+ &&
65
+ (Date.now() - this._lastActivity >= CommerceUIStore.TIMEOUT)
66
+ ) {
67
+ this._activeItem = undefined
68
+ this._lastActivity = undefined
69
+ }
70
+ }
71
+
72
+ itemQuantityChanged = (item: LineItem, val: number, oldVal: number, ): void => {
73
+
74
+ if (val === 0) {
75
+ if (this._activeItem?.sku === item.sku) {
76
+ this._activeItem = undefined
77
+ this._lastActivity = undefined
78
+ }
79
+ // otherwise ignore
80
+ }
81
+ else if (val < oldVal) {
82
+ if (this._activeItem?.sku === item.sku) {
83
+ this._lastActivity = Date.now()
84
+ }
85
+ // otherwise ignore
86
+ }
87
+ else {
88
+ this._activeItem = item
89
+ this._lastActivity = Date.now()
90
+ }
50
91
  }
51
92
 
52
- clearSkuPath = (): void => {
53
- this._skuPath = undefined
54
- this._optionsShowing = false
93
+ get item(): LineItem | undefined {
94
+ return this._activeItem
55
95
  }
56
96
  }
57
97
 
package/context/index.tsx CHANGED
@@ -3,7 +3,8 @@ import React, {
3
3
  createContext,
4
4
  useContext,
5
5
  useRef,
6
- type PropsWithChildren
6
+ type PropsWithChildren,
7
+ useEffect
7
8
  } from 'react'
8
9
 
9
10
  // https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
@@ -36,21 +37,32 @@ const CommerceProvider: React.FC<PropsWithChildren & {
36
37
  rootNode: CategoryNode
37
38
  options?: ServiceOptions
38
39
  uiSpecs?: Record<string, SelectionUISpecifier>
40
+ DEBUG_NO_TICK?: boolean
39
41
  }> = ({
40
42
  children,
41
43
  families,
42
44
  rootNode,
43
45
  options,
44
- uiSpecs
46
+ uiSpecs,
47
+ DEBUG_NO_TICK=false
45
48
  }) => {
46
49
 
50
+ useEffect(() => {
51
+ if (DEBUG_NO_TICK) return
52
+ const intervalId = setInterval(() => {
53
+ (valueRef.current.ui as CommerceUIStore).tick()
54
+ }, 250)
55
+ return () => { clearInterval(intervalId) }
56
+ }, [])
57
+
47
58
  // TODO: Inject Promo fixture here from siteDef
48
- const serviceRef = useRef<CommerceContextValue>({
49
- service: getInstance(families, rootNode, options, uiSpecs),
50
- ui: new CommerceUIStore()
51
- })
59
+ const service = getInstance(families, rootNode, options, uiSpecs)
60
+ const ui = new CommerceUIStore(service)
61
+
62
+ const valueRef = useRef<CommerceContextValue>({service, ui})
63
+
52
64
  return (
53
- <CommerceContext.Provider value={serviceRef.current}>
65
+ <CommerceContext.Provider value={valueRef.current}>
54
66
  {children}
55
67
  </CommerceContext.Provider>
56
68
  )
@@ -60,4 +72,5 @@ export {
60
72
  useCommerce,
61
73
  useCommerceUI,
62
74
  CommerceProvider
63
- }
75
+ }
76
+
package/index.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export * from './context'
2
2
  export * from './components'
3
+ // Impl-dependent, so leave w impl
3
4
  export type { StandaloneServiceOptions as ServiceOptions } from './service/impls/standalone/standalone-service'
4
5
  export {
5
6
  useSyncSkuParamWithCurrentItem,
6
7
  getFacetValuesMutator,
7
8
  formatCurrencyValue,
8
- ProductMediaAccessor
9
+ ProductMediaAccessor,
10
+ LineItemRef
9
11
  } from './util'
10
12
 
11
13
  export * from './util/selection-ui-specifiers'
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "6.4.7",
3
+ "version": "7.0.1",
4
4
  "description": "e-commerce framework.",
5
- "type": "module",
6
5
  "publishConfig": {
7
6
  "registry": "https://registry.npmjs.org/",
8
7
  "access": "public",
@@ -23,9 +22,7 @@
23
22
  "scripts": {
24
23
  "lat": "npm show @hanzo/commerce version",
25
24
  "pub": "npm publish",
26
- "build": "tsc",
27
- "tc": "tsc",
28
- "clean": "rm -rf dist && rm -rf node_modules"
25
+ "tc": "tsc"
29
26
  },
30
27
  "exports": {
31
28
  ".": "./index.ts",
@@ -34,14 +31,14 @@
34
31
  "./debug": "./service/debug.ts"
35
32
  },
36
33
  "dependencies": {
37
- "ethers": "^6.11.1",
38
- "next-usequerystate": "^1.17.0",
34
+ "ethers": "^6.12.0",
35
+ "next-usequerystate": "^1.17.1",
39
36
  "react-square-web-payments-sdk": "^3.2.1",
40
- "square": "^35.0.0"
37
+ "square": "^35.1.0"
41
38
  },
42
39
  "peerDependencies": {
43
40
  "@hanzo/auth": "^2.4.5",
44
- "@hanzo/ui": "^3.6.4",
41
+ "@hanzo/ui": "^3.7.0",
45
42
  "@hookform/resolvers": "^3.3.4",
46
43
  "@radix-ui/react-radio-group": "^1.1.3",
47
44
  "firebase": "^10.8.0",
@@ -52,13 +49,13 @@
52
49
  "next": "14.1.3",
53
50
  "react": "^18.2.0",
54
51
  "react-dom": "^18.2.0",
55
- "react-hook-form": "7.50.1",
52
+ "react-hook-form": "^7.51.3",
56
53
  "zod": "3.21.4"
57
54
  },
58
55
  "devDependencies": {
59
- "@types/react": "^18.2.64",
60
- "@types/react-dom": "^18.2.18",
56
+ "@types/react": "^18.2.79",
57
+ "@types/react-dom": "^18.2.25",
61
58
  "cross-fetch": "^4.0.0",
62
- "typescript": "^5.3.3"
59
+ "typescript": "^5.4.5"
63
60
  }
64
61
  }
package/tsconfig.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "extends": "../tsconfig.hanzo-modules.base.json",
3
3
  "include": [
4
4
  "**/*.ts",
5
- "**/*.tsx",
5
+ "**/*.tsx", "types/string-mutator.ts",
6
6
  ],
7
7
  "exclude": [
8
8
  "node_modules",
@@ -55,6 +55,9 @@ type ItemSelectorOptions = {
55
55
  * default: false */
56
56
  showSlider?: boolean
57
57
 
58
+ /** default true */
59
+ showButtonIfOnlyOne?: boolean
60
+
58
61
  /**
59
62
  * Sort by cost.
60
63
  * If it's a carousel selector and 'showSlider' is true,
package/util/index.ts CHANGED
@@ -28,7 +28,6 @@ export function formatCurrencyValue(price: number): string {
28
28
  return (str.endsWith('.00')) ? str.replace('.00', '') : str
29
29
  }
30
30
 
31
-
32
31
  export const getFacetValuesMutator = (level: number, cmmc: CommerceService): StringMutator => {
33
32
 
34
33
  const setLevel = (value: string, level: number ): void => {
@@ -57,7 +56,6 @@ export const getFacetValuesMutator = (level: number, cmmc: CommerceService): Str
57
56
  } satisfies StringMutator
58
57
  }
59
58
 
60
-
61
59
  export { default as useSyncSkuParamWithCurrentItem } from './use-sync-sku-param-w-current-item'
62
60
  export { default as processSquareCardPayment } from './square-payment'
63
61
  export { default as ObsStringMutator } from './obs-string-mutator'
@@ -68,4 +66,6 @@ export * from './selection-ui-specifiers'
68
66
  export { getErrorMessage } from './error'
69
67
 
70
68
  export { default as accessItemOptions } from './item-selector-options-accessor'
71
- export { default as accessMultiSelectorOptions } from './multi-family-selector-options-accessor'
69
+ export { default as accessMultiSelectorOptions } from './multi-family-selector-options-accessor'
70
+
71
+ export { default as LineItemRef} from './line-item-ref'
@@ -15,6 +15,8 @@ export default (options: ItemSelectorOptions | undefined = {}): Required<ItemSel
15
15
  const horizButtons = 'horizButtons' in options ? options.horizButtons! : false
16
16
  const showSlider = 'showSlider' in options ? options.showSlider! : true
17
17
 
18
+ const showButtonIfOnlyOne = 'showButtonIfOnlyOne' in options ? options.showButtonIfOnlyOne! : true
19
+
18
20
  const sort = 'sort' in options ? options.sort! : 'none'
19
21
 
20
22
  return {
@@ -27,6 +29,7 @@ export default (options: ItemSelectorOptions | undefined = {}): Required<ItemSel
27
29
  buttonType,
28
30
  horizButtons,
29
31
  showSlider,
32
+ showButtonIfOnlyOne,
30
33
  sort,
31
34
  }
32
35
  }
@@ -0,0 +1,23 @@
1
+ import { observable, action, computed, makeObservable } from 'mobx'
2
+
3
+ import type { LineItem, ObsLineItemRef } from '../types'
4
+
5
+ class LineItemRef implements ObsLineItemRef {
6
+
7
+ _item: LineItem | undefined = undefined
8
+
9
+ constructor() {
10
+
11
+ makeObservable(this, {
12
+ _item: observable,
13
+ item: computed,
14
+ set: action
15
+ })
16
+ }
17
+
18
+ get item(): LineItem | undefined { return this._item }
19
+
20
+ set = (v: LineItem | undefined): void => { this._item = v }
21
+ }
22
+
23
+ export default LineItemRef
@@ -1,22 +1,22 @@
1
+ import {makeObservable, observable, action} from 'mobx'
1
2
 
2
- import { makeObservable, observable, action } from 'mobx'
3
3
  import type { StringMutator } from '../types'
4
4
 
5
5
  class ObsStringMutator implements StringMutator {
6
6
 
7
- s: string | null
8
- constructor(_s: string) {
9
- this.s = _s
7
+ _s: string | null
8
+
9
+ constructor(s: string) {
10
+ this._s = s
10
11
  makeObservable(this, {
11
- s: observable,
12
+ _s: observable,
12
13
  set: action,
13
- //get: computed
14
+ //get: computed .// no need
14
15
  })
15
16
  }
16
17
 
17
- set(v: string | null): void { this.s = v }
18
- get(): string | null { return this.s }
18
+ set(v: string | null): void { this._s = v }
19
+ get(): string | null { return this._s }
19
20
  }
20
21
 
21
22
  export default ObsStringMutator
22
-
@@ -1,109 +0,0 @@
1
- 'use client'
2
- import React from 'react'
3
- import { observer } from 'mobx-react-lite'
4
-
5
- import { cn } from '@hanzo/ui/util'
6
- import { Skeleton } from '@hanzo/ui/primitives'
7
-
8
- import type { ItemSelector, LineItem } from '../../types'
9
-
10
- import AddToCartWidget from './add-to-cart-widget'
11
- import { ButtonItemSelector } from '../item-selector'
12
- import { formatCurrencyValue } from '../../util'
13
-
14
- const SelectFamilyItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & ItemSelector & {
15
- isLoading?: boolean
16
- mobile?: boolean
17
- title?: string
18
- onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
19
- }> = /* NOT observer */({
20
- items,
21
- selectedItemRef: selItemRef,
22
- selectSku,
23
- className,
24
- isLoading = false,
25
- mobile = false,
26
- title,
27
- onQuantityChanged,
28
- ...props
29
- }) => {
30
-
31
- const soleOption = items.length === 1
32
-
33
- const SelectProductComp: React.FC<{ className?: string }> = ({
34
- className = ''
35
- }) => {
36
-
37
- const mobilePicker = (mobile || items.length > 6)
38
- if (soleOption) {
39
- const item = items[0] as LineItem
40
- return (
41
- <div className={cn('flex flex-col justify-center items-center ' + (mobilePicker ? 'h-[180px] ' : 'h-auto min-h-24'), className)}>
42
- <p className='text-lg text-center font-semibold'>{item.optionLabel + ', ' + formatCurrencyValue(item.price)}</p>
43
- </div>
44
- )
45
- }
46
-
47
- return (
48
- <div /* id='CV_AVAIL_AMOUNTS' */ className={cn(
49
- 'sm:w-pr-80 sm:mx-auto md:w-full flex flex-col justify-start items-center',
50
- className
51
- )}>
52
- {title && (<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } /> )}
53
- <ButtonItemSelector
54
- items={items}
55
- selectedItemRef={selItemRef}
56
- selectSku={selectSku}
57
- clx='mt-2'
58
- scrollable={false}
59
- itemClx='flex flex-row gap-2.5 items-center'
60
- />
61
- </div>
62
- )
63
- }
64
-
65
- const AddToCartComp: React.FC<{ className?: string }> = observer(({ className = '' }) => (
66
- // TODO disable if nothing selected
67
- (selItemRef.item && !isLoading) && (
68
- <AddToCartWidget
69
- item={selItemRef.item}
70
- onQuantityChanged={onQuantityChanged}
71
- className={cn('lg:min-w-[160px] lg:mx-auto', className)}
72
- />
73
- )
74
- ))
75
-
76
- const TitleArea: React.FC<{ className?: string }> = observer(({ className = '' }) => (
77
-
78
- isLoading ? (<Skeleton className={'h-8 w-full ' + className} />) : (
79
-
80
- <div className={cn('text-center flex flex-col justify-start items-center', className)}>
81
- <p className='font-heading text-center'>{title}</p>
82
- </div>
83
-
84
- )))
85
-
86
- return mobile ? (
87
- <div /* id='CV_OUTER' */
88
- className={cn(
89
- 'w-full flex flex-col justify-between items-center gap-5 py-pr-6',
90
- className
91
- )}
92
- {...props}
93
- >
94
- {title && (<TitleArea className='grow pt-3 mb-0' />)}
95
- <SelectProductComp className='w-pr-65' />
96
- <AddToCartComp className='w-pr-65' />
97
- </div>
98
- ) : (
99
- <div className={cn('', className)} {...props}>
100
- {title && (<TitleArea className='' />)}
101
- <div className='flex flex-col justify-start items-center gap-4'>
102
- <SelectProductComp />
103
- <AddToCartComp className='' />
104
- </div>
105
- </div>
106
- )
107
- }
108
-
109
- export default SelectFamilyItemCard
@@ -1,192 +0,0 @@
1
- 'use client'
2
- import React from 'react'
3
- import Image from 'next/image'
4
- import { observer } from 'mobx-react-lite'
5
-
6
- import { cn } from '@hanzo/ui/util'
7
- import { Skeleton } from '@hanzo/ui/primitives'
8
-
9
- import type { Family, ObsLineItemRef, LineItem } from '../../types'
10
- import { formatCurrencyValue } from '../../util'
11
- import { Icons } from '../Icons'
12
-
13
- import AddToCartWidget from './add-to-cart-widget'
14
- import { ButtonItemSelector } from '../item-selector'
15
-
16
- const SelectFamilyItemPanel: React.FC<
17
- React.HTMLAttributes<HTMLDivElement> &
18
- {
19
- family: Family
20
- selectedItemRef: ObsLineItemRef
21
- selectSku: (sku: string) => void
22
- showQuantity?: boolean
23
- isLoading?: boolean
24
- mobile?: boolean
25
- }
26
- > = /* NOT observer */ ({
27
- family,
28
- selectedItemRef,
29
- selectSku,
30
- className,
31
- showQuantity=true,
32
- isLoading = false,
33
- mobile = false,
34
- ...props
35
- }) => {
36
-
37
- const soleOption = family.products.length === 1
38
-
39
- const FamilyImage: React.FC<{ className?: string }> = ({ className = '' }) => {
40
-
41
- if (isLoading) {
42
- // deliberately not Skeleton to have a better overall pulse effect.
43
- return <div className={cn(
44
- 'bg-level-1 rounded-xl aspect-square w-full min-h-1 ', // +
45
- //' min-h-[100px] sm:min-h-[200px] lg:aspect-auto 2xl:w-auto 2xl:aspect-square',
46
- className)} />
47
- }
48
-
49
- return family.img ? (
50
- // TODO: Why so many div's?
51
- <div className={cn('flex flex-col justify-start', className)}>
52
- <div className={cn('w-full border rounded-xl p-6 ')}>
53
- <div className={cn('w-full aspect-square relative')}>
54
- <Image
55
- src={family.img.src}
56
- fill
57
- sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, (max-width: 1200px) 50vw, 20vw"
58
- alt={family.title}
59
- className=''
60
- loading='lazy'
61
- style={{ objectFit: 'contain' }} />
62
- </div>
63
- </div>
64
- </div>
65
- ) : (
66
- <div
67
- aria-label='Placeholder'
68
- role='img'
69
- aria-roledescription='placeholder'
70
- className={cn('w-full flex items-center justify-center aspect-square' , className)}
71
- >
72
- <Icons.barcode className='h-9 w-9 text-muted' aria-hidden='true' />
73
- </div>
74
- )
75
- }
76
-
77
- const AvailableAmounts: React.FC<{ className?: string }> = observer(({ className = '' }) => {
78
-
79
- if (soleOption) return null
80
- const mobilePicker = (mobile && family.products.length > 8)
81
-
82
- return isLoading ? (
83
- <Skeleton className={'min-h-[120px] w-pr-60 mx-auto ' + className} />
84
- ) : (
85
- <div /* id='CV_AVAIL_AMOUNTS' */ className={cn(
86
- 'sm:w-pr-80 sm:mx-auto md:w-full flex flex-col justify-start items-center',
87
- (mobilePicker ? 'gap-4' : 'gap-8'),
88
- className
89
- )}>
90
- <div className='w-full flex flex-col justify-start items-center'>
91
- <h6 className='text-center font-semibold'>Available options</h6>
92
- <div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } />
93
- </div>
94
- <ButtonItemSelector
95
- items={family.products as LineItem[]}
96
- selectedItemRef={selectedItemRef}
97
- selectSku={selectSku}
98
- options={{ showQuantity }}
99
- scrollable={false}
100
- clx='block columns-2 gap-4'
101
- itemClx='flex flex-row gap-2 items-center mb-2.5'
102
- />
103
- </div>
104
- )
105
- })
106
-
107
- const AddToCartArea: React.FC<{ className?: string }> = observer(({ className = '' }) => (
108
- (selectedItemRef.item && !isLoading) ? (
109
- <AddToCartWidget item={selectedItemRef.item} className={className}/>
110
- ) : (
111
- <div className={cn('h-6 w-12 invisible', className)} />
112
- )
113
- ))
114
-
115
- const TitleArea: React.FC<{ className?: string }> = observer(({ className = '' }) => (
116
-
117
- isLoading ? (<Skeleton className={'h-12 w-pr-80 mx-auto ' + className} />) : (
118
-
119
- <div className={cn('flex flex-col justify-start items-center', className)}>
120
- <h3 className='text-base md:text-lg lg:text-2xl font-heading text-center'>
121
- {family.parentTitle && (
122
- <span>{family.parentTitle}<br className='md:hidden' /><span className='xs:hidden md:inline '>,&nbsp;</span></span>
123
- )}
124
- <span>{family.title}</span>
125
- </h3>
126
- {selectedItemRef.item?.sku ? (
127
- <h6 className='text-center font-semibold'>
128
- {(soleOption ? '' : (selectedItemRef.item.optionLabel + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
129
- </h6>
130
- ) : ''}
131
- </div>
132
- )))
133
-
134
- const Desc: React.FC<{ className?: string }> = ({ className = '' }) => (
135
- isLoading ? (
136
- <Skeleton className={'min-h-20 w-full grow mx-auto ' + className} />
137
- ) : (
138
- <p className={cn('text-base lg:text-lg mb-6 xs:mb-0', className)}>{family.desc}</p>
139
- )
140
- )
141
-
142
- return mobile ? (
143
- <div /* id='CV_OUTER' */
144
- className={cn(
145
- 'w-full h-[calc(100svh-96px)] max-h-[700px] flex flex-col justify-between ' +
146
- 'items-stretch gap-[4vh] mt-[2vh] pb-[6vh]',
147
- className
148
- )}
149
- {...props}
150
- >
151
- <div /* id='CV_TITLE_AND_IMAGE_ROW' */ className='flex flex-row justify-between items-start w-full'>
152
- {isLoading ? ( <Skeleton className={'min-h-30 w-full '} /> ) : (<>
153
- <FamilyImage className='w-pr-33' />
154
- <TitleArea className='grow pt-3 mb-0' />
155
- </>)}
156
- </div>
157
- <Desc className='' />
158
- <AvailableAmounts className='mb-[3vh]' />
159
- <AddToCartArea className='w-pr-70 mx-auto' />
160
- </div>
161
- ) : (
162
- <div /* id='CV_OUTERMOST' */>
163
- <div /* id='CV_OUTER' */ className={cn('w-full flex flex-row justify-between items-stretch gap-6 sm:gap-4', className)} {...props}>
164
- <div /* id='CV_IMAGE_COL_SEP' */ className={'relative ' + (!isLoading ? 'md:hidden sm:min-w-[185px] xl:flex xl:w-pr-40' : '')}>
165
- <FamilyImage className='w-full' />
166
- </div>
167
- <div /* id='CV_CONTENT_COL */ className={'flex flex-col xl:w-pr-60 justify-center ' + (soleOption ? '' : 'gap-6') }>
168
- <div /* id='CV_MD_IMAGE_AND_TITLE */ className='hidden md:flex xl:hidden flex-row gap-x-3'>
169
- <FamilyImage className='min-w-pr-30' />
170
- <TitleArea className='grow' />
171
- </div>
172
-
173
- <div /* id='CV_CONTENT' */ className={'flex flex-col gap-2.5 ' + (isLoading ? 'justify-between h-full' : '')}>
174
- <TitleArea className='md:hidden xl:flex' />
175
- <Desc className='' />
176
- </div>
177
- <div /* id='CV_CTA_AREA_BIG' */ className='hidden lg:flex flex-col justify-start items-center gap-6'>
178
- <AvailableAmounts />
179
- <AddToCartArea className='' />
180
- </div>
181
- </div>
182
- </div>
183
- <div /* id='CV_CTA_AREA_COMPACT' */ className='lg:hidden flex p-4 flex-col justify-start items-center gap-6'>
184
- <AvailableAmounts />
185
- <AddToCartArea className='' />
186
- </div>
187
- </div>
188
- )
189
- }
190
-
191
-
192
- export default SelectFamilyItemPanel
@@ -1,83 +0,0 @@
1
- 'use client'
2
- import React, { useState, type ReactNode } from 'react'
3
- import { useRouter } from 'next/navigation'
4
-
5
- import { X as LucideX} from 'lucide-react'
6
-
7
- import {
8
- Drawer,
9
- DrawerTrigger,
10
- DrawerContent,
11
- Button,
12
- } from '@hanzo/ui/primitives'
13
-
14
- import { cn } from '@hanzo/ui/util'
15
-
16
- //import BuyCard from './buy-card'
17
-
18
- import CarouselBuyCard from './carousel-buy-card'
19
-
20
- const BuyDrawer: React.FC<{
21
- skuPath: string
22
- trigger: ReactNode
23
- triggerClx?: string
24
- drawerClx?: string
25
- cardClx?: string
26
- mobile?: boolean
27
- }> = ({
28
- skuPath,
29
- trigger,
30
- triggerClx='',
31
- drawerClx='',
32
- cardClx='',
33
- mobile=false
34
- }) => {
35
-
36
- const [open, setOpen] = useState<boolean>(false)
37
- const router = useRouter()
38
-
39
- return (
40
- <Drawer open={open} onOpenChange={setOpen} >
41
- <DrawerTrigger asChild className={triggerClx}>
42
- {trigger}
43
- </DrawerTrigger>
44
- <DrawerContent
45
- className={cn('rounded-t-xl mt-6 pb-12 h-auto min-h-[35vh] pt-6 md:max-w-[550px] md:mx-auto lg:max-w-[50vw]', drawerClx)}
46
- >
47
- <CarouselBuyCard
48
- skuPath={skuPath}
49
- handleCheckout={() => {router.push('/checkout')}}
50
- mobile={mobile}
51
- clx={cn('w-full', cardClx)}
52
- />
53
-
54
- <Button
55
- variant='ghost'
56
- size='icon'
57
- onClick={() => {setOpen(false)}}
58
- className={'absolute top-4 right-4 w-8 h-8 group rounded-full p-1 hidden md:flex items-center'}
59
- >
60
- <LucideX className='w-6 h-6 text-muted group-hover:text-foreground'/>
61
- </Button>
62
-
63
- </DrawerContent>
64
- </Drawer>
65
- )
66
- }
67
-
68
- export default BuyDrawer
69
-
70
- /*
71
- <BuyCard
72
- skuPath={skuPath}
73
- scrollAfter={spec.selector === 'carousel' ? 999 : undefined}
74
- mobile={mobile}
75
- onQuantityChanged={onQuantityChanged}
76
- clx={cn('w-full', cardClx)}
77
- selector={selector}
78
- selectorProps={{soleItemClx:'mb-3'}}
79
- showItemMedia={spec.selector !== 'carousel'}
80
- familyTabAs='label'
81
- allVariants={spec.allVariants}
82
- />
83
- */
@@ -1,20 +0,0 @@
1
- 'use client'
2
- import React from 'react'
3
-
4
- import BuyDrawer from './buy-drawer'
5
-
6
- const BuyTriggerWrapper: React.FC<{
7
- skuPath: string
8
- trigger: React.ReactNode
9
- mobileTrigger?: React.ReactNode
10
- mobile?: boolean
11
- }> = ({
12
- skuPath,
13
- trigger,
14
- mobileTrigger,
15
- mobile
16
- }) => (
17
- <BuyDrawer skuPath={skuPath} trigger={trigger} mobile={mobile}/>
18
- )
19
-
20
- export default BuyTriggerWrapper
@@ -1,27 +0,0 @@
1
- {/*
2
- <NodeTabs
3
- className={cn(
4
- 'grid gap-0 align-stretch justify-normal ' + `grid-cols-${inst.current.parentNode.subNodes!.length}`,
5
- 'border-b-2 rounded-lg border-level-3 mb-4 -mr-2 -ml-2 max-w-[460px] h-10', // height is needed for iPhone bug
6
- (scroll ? 'shrink-0' : ''),
7
- famWidgetClx
8
- )}
9
- mobile={mobile}
10
- mutator={allVariants ?
11
- {
12
- get: () => (inst.current!.currentFamily.s),
13
- set: setFamilyPath
14
- }
15
- :
16
- getFacetValuesMutator(inst.current.level + 1, cmmc)
17
- }
18
- itemClx='flex-col h-auto gap-0 py-1 px-3'
19
- buttonClx={
20
- 'h-full ' +
21
- '!border-level-3'
22
- }
23
- levelNodes={inst.current.parentNode.subNodes!}
24
- show={familyTabAs}
25
- />
26
- */}
27
- </>)}
@@ -1,10 +0,0 @@
1
- import React from 'react'
2
- import { type LucideProps } from 'lucide-react'
3
-
4
- const CartIcon: React.FC<LucideProps> = (props: LucideProps) => (
5
- <svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' {...props}>
6
- <path d='m3.731 8-1.231 20h27l-1.231-20zm1.991 2.103h20.557l.972 15.794h-22.502l.972-15.794z'/>
7
- <path d='m20.718 8.134c0-2.375-1.932-4.307-4.307-4.307s-4.307 1.932-4.307 4.307h-2.084c0-3.524 2.867-6.391 6.391-6.391s6.391 2.867 6.391 6.391z'/>
8
- </svg>)
9
-
10
- export default CartIcon