@hanzo/commerce 5.1.3 → 6.1.0

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.
Files changed (67) hide show
  1. package/components/{buy-item/select-category-item-card.tsx → buy/_to_deprecate_select-category-item-card.tsx} +22 -32
  2. package/components/{select-category-item-panel.tsx → buy/_to_deprecate_select-category-item-panel.tsx} +16 -22
  3. package/components/{buy-item/buy-item-popup.tsx → buy/_unused_buy-popup.tsx_} +4 -4
  4. package/components/{add-to-cart-widget.tsx → buy/add-to-cart-widget.tsx} +10 -6
  5. package/components/buy/buy-button.tsx +31 -0
  6. package/components/buy/buy-card.tsx +267 -0
  7. package/components/buy/buy-drawer.tsx +89 -0
  8. package/components/buy/buy-trigger-wrapper.tsx +20 -0
  9. package/components/{cart-accordian.tsx → cart/cart-accordian.tsx} +2 -1
  10. package/components/{cart-panel → cart/cart-panel}/cart-line-item.tsx +27 -14
  11. package/components/{cart-panel → cart/cart-panel}/index.tsx +12 -3
  12. package/components/{cart-panel → cart/cart-panel}/promo-code.tsx +3 -3
  13. package/components/{payment-step-form → checkout/payment-step-form}/contact-form.tsx +1 -1
  14. package/components/{payment-step-form → checkout/payment-step-form}/index.tsx +3 -3
  15. package/components/{payment-step-form → checkout/payment-step-form}/methods/bank-transfer.tsx +1 -1
  16. package/components/{payment-step-form → checkout/payment-step-form}/methods/card.tsx +4 -4
  17. package/components/{payment-step-form → checkout/payment-step-form}/methods/crypto.tsx +3 -3
  18. package/components/{payment-step-form → checkout/payment-step-form}/methods/index.ts +1 -1
  19. package/components/{shipping-step-form.tsx → checkout/shipping-step-form.tsx} +4 -4
  20. package/components/index.ts +20 -18
  21. package/components/item/item-media.tsx +51 -0
  22. package/components/item/product-card.tsx +48 -0
  23. package/components/{facet-values-widget → level-nodes-widget}/index.tsx +20 -19
  24. package/components/level-nodes-widget/node-image.tsx +31 -0
  25. package/components/select/carousel-selector.tsx +112 -0
  26. package/components/select/image-selector.tsx +162 -0
  27. package/components/select/radio-selector.tsx +125 -0
  28. package/package.json +3 -2
  29. package/service/context.tsx +4 -4
  30. package/service/impls/standalone/actual-line-item.ts +30 -16
  31. package/service/impls/standalone/index.ts +4 -4
  32. package/service/impls/standalone/standalone-service.ts +94 -64
  33. package/types/buy-ui-spec.ts +8 -0
  34. package/types/category.ts +6 -4
  35. package/types/commerce-service.ts +21 -16
  36. package/types/index.ts +3 -2
  37. package/types/item-selector.ts +34 -6
  38. package/types/line-item.ts +2 -0
  39. package/types/product.ts +5 -5
  40. package/types/tree-node.ts +24 -0
  41. package/util/buy-ui-conf.ts +34 -0
  42. package/util/index.ts +14 -11
  43. package/util/obs-string-mutator.ts +22 -0
  44. package/util/use-sync-sku-param-w-current-item.ts +5 -5
  45. package/components/buy-item/buy-item-button-wrapper.tsx +0 -27
  46. package/components/buy-item/buy-item-button.tsx +0 -34
  47. package/components/buy-item/buy-item-card.tsx +0 -92
  48. package/components/buy-item/buy-item-mobile-drawer.tsx +0 -54
  49. package/components/buy-item/select-category-and-item-widget.tsx +0 -83
  50. package/components/category-item-radio-selector.tsx +0 -55
  51. package/components/category-item-scroll-selector.tsx +0 -40
  52. package/components/facet-values-widget/facet-image.tsx +0 -41
  53. package/components/item-carousel.tsx +0 -80
  54. package/components/product-card.tsx +0 -79
  55. package/types/facet.ts +0 -35
  56. /package/components/{cart-icon.tsx → cart/cart-icon.tsx} +0 -0
  57. /package/components/{payment-step-form → checkout/payment-step-form}/card-icon-row.tsx +0 -0
  58. /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/amex.tsx +0 -0
  59. /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/diners-club.tsx +0 -0
  60. /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/discover.tsx +0 -0
  61. /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/jcb.tsx +0 -0
  62. /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/mastercard.tsx +0 -0
  63. /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/visa.tsx +0 -0
  64. /package/components/{payment-step-form → checkout/payment-step-form}/cc-button.tsx +0 -0
  65. /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/btc.tsx +0 -0
  66. /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/eth.tsx +0 -0
  67. /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/usdt.tsx +0 -0
@@ -7,40 +7,39 @@ import { Skeleton } from '@hanzo/ui/primitives'
7
7
 
8
8
  import type { ItemSelector, LineItem } from '../../types'
9
9
 
10
- import AddToCartWidget from '../add-to-cart-widget'
11
- import CategoryItemRadioSelector from '../category-item-radio-selector'
12
- import CategoryItemScrollSelector from '../category-item-scroll-selector'
10
+ import AddToCartWidget from './add-to-cart-widget'
11
+ import RadioItemSelector from '../select/radio-selector'
13
12
  import { formatCurrencyValue } from '../../util'
14
13
 
15
14
  const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & ItemSelector & {
16
15
  isLoading?: boolean
17
16
  mobile?: boolean
18
- noTitle?: boolean
17
+ title?: string
19
18
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
20
19
  }> = /* NOT observer */({
21
- category,
20
+ items,
22
21
  selectedItemRef: selItemRef,
23
22
  selectSku,
24
23
  className,
25
24
  isLoading = false,
26
25
  mobile = false,
27
- noTitle = false,
26
+ title,
28
27
  onQuantityChanged,
29
28
  ...props
30
29
  }) => {
31
30
 
32
- const soleOption = category.products.length === 1
31
+ const soleOption = items.length === 1
33
32
 
34
33
  const SelectProductComp: React.FC<{ className?: string }> = ({
35
34
  className = ''
36
35
  }) => {
37
36
 
38
- const mobilePicker = (mobile || category.products.length > 6)
37
+ const mobilePicker = (mobile || items.length > 6)
39
38
  if (soleOption) {
40
- const item = category.products[0] as LineItem
39
+ const item = items[0] as LineItem
41
40
  return (
42
41
  <div className={cn('flex flex-col justify-center items-center ' + (mobilePicker ? 'h-[180px] ' : 'h-auto min-h-24'), className)}>
43
- <p className='text-lg text-center font-semibold'>{item.titleAsOption + ', ' + formatCurrencyValue(item.price)}</p>
42
+ <p className='text-lg text-center font-semibold'>{item.optionLabel + ', ' + formatCurrencyValue(item.price)}</p>
44
43
  </div>
45
44
  )
46
45
  }
@@ -50,25 +49,16 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
50
49
  'sm:w-pr-80 sm:mx-auto md:w-full flex flex-col justify-start items-center',
51
50
  className
52
51
  )}>
53
- {!noTitle && (<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } /> )}
54
- {mobilePicker ? (
55
- <CategoryItemScrollSelector
56
- category={category}
57
- selectedItemRef={selItemRef}
58
- selectSku={selectSku}
59
- itemClx='h-10 border-b px-4'
60
- outerClx='min-w-pr-80 h-[180px]' // 80% of 65% parent
61
- />
62
- ) : (
63
- <CategoryItemRadioSelector
64
- category={category}
65
- selectedItemRef={selItemRef}
66
- selectSku={selectSku}
67
- groupClx='mt-2'
68
- showQuantity={false}
69
- itemClx='flex flex-row gap-2.5 items-center'
70
- />
71
- )}
52
+ {title && (<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } /> )}
53
+ <RadioItemSelector
54
+ items={items}
55
+ selectedItemRef={selItemRef}
56
+ selectSku={selectSku}
57
+ clx='mt-2'
58
+ showQuantity={false}
59
+ scrollList={false}
60
+ itemClx='flex flex-row gap-2.5 items-center'
61
+ />
72
62
  </div>
73
63
  )
74
64
  }
@@ -90,7 +80,7 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
90
80
  isLoading ? (<Skeleton className={'h-8 w-full ' + className} />) : (
91
81
 
92
82
  <div className={cn('text-center flex flex-col justify-start items-center', className)}>
93
- <p className='font-heading text-center'>{category.title}</p>
83
+ <p className='font-heading text-center'>{title}</p>
94
84
  </div>
95
85
 
96
86
  )))
@@ -103,13 +93,13 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
103
93
  )}
104
94
  {...props}
105
95
  >
106
- {!noTitle && (<TitleArea className='grow pt-3 mb-0' />)}
96
+ {title && (<TitleArea className='grow pt-3 mb-0' />)}
107
97
  <SelectProductComp className='w-pr-65' />
108
98
  <AddToCartComp className='w-pr-65' />
109
99
  </div>
110
100
  ) : (
111
101
  <div className={cn('', className)} {...props}>
112
- {!noTitle && (<TitleArea className='' />)}
102
+ {title && (<TitleArea className='' />)}
113
103
  <div className='flex flex-col justify-start items-center gap-4'>
114
104
  <SelectProductComp />
115
105
  <AddToCartComp className='' />
@@ -6,17 +6,20 @@ import { observer } from 'mobx-react-lite'
6
6
  import { cn } from '@hanzo/ui/util'
7
7
  import { Skeleton } from '@hanzo/ui/primitives'
8
8
 
9
- import type { ItemSelector } from '../types'
10
- import { formatCurrencyValue } from '../util'
11
- import { Icons } from './Icons'
9
+ import type { Category, ObsLineItemRef, LineItem } from '../../types'
10
+ import { formatCurrencyValue } from '../../util'
11
+ import { Icons } from '../Icons'
12
12
 
13
13
  import AddToCartWidget from './add-to-cart-widget'
14
- import CategoryItemRadioSelector from './category-item-radio-selector'
15
- import CategoryItemScrollSelector from './category-item-scroll-selector'
14
+ import RadioItemSelector from '../select/radio-selector'
16
15
 
17
16
  const SelectCategoryItemPanel: React.FC<
18
- React.HTMLAttributes<HTMLDivElement> & ItemSelector &
17
+ React.HTMLAttributes<HTMLDivElement> &
19
18
  {
19
+ category: Category
20
+ selectedItemRef: ObsLineItemRef
21
+ selectSku: (sku: string) => void
22
+ showQuantity?: boolean
20
23
  isLoading?: boolean
21
24
  mobile?: boolean
22
25
  }
@@ -49,7 +52,7 @@ const SelectCategoryItemPanel: React.FC<
49
52
  <div className={cn('w-full border rounded-xl p-6 ')}>
50
53
  <div className={cn('w-full aspect-square relative')}>
51
54
  <Image
52
- src={category.img}
55
+ src={category.img.src}
53
56
  fill
54
57
  sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, (max-width: 1200px) 50vw, 20vw"
55
58
  alt={category.title}
@@ -88,25 +91,16 @@ const SelectCategoryItemPanel: React.FC<
88
91
  <h6 className='text-center font-semibold'>Available options</h6>
89
92
  <div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } />
90
93
  </div>
91
- {mobilePicker ? (
92
- <CategoryItemScrollSelector
93
- category={category}
94
+ <RadioItemSelector
95
+ items={category.products as LineItem[]}
94
96
  selectedItemRef={selectedItemRef}
95
97
  selectSku={selectSku}
96
98
  showQuantity={showQuantity}
97
- itemClx='h-10 border-b px-4'
98
- outerClx='mb-4 h-[180px]'
99
- />
100
- ) : (
101
- <CategoryItemRadioSelector
102
- category={category}
103
- selectedItemRef={selectedItemRef}
104
- selectSku={selectSku}
105
- showQuantity={showQuantity}
106
- groupClx='block columns-2 gap-4'
99
+ scrollList={false}
100
+
101
+ clx='block columns-2 gap-4'
107
102
  itemClx='flex flex-row gap-2 items-center mb-2.5'
108
103
  />
109
- )}
110
104
  </div>
111
105
  )
112
106
  })
@@ -132,7 +126,7 @@ const SelectCategoryItemPanel: React.FC<
132
126
  </h3>
133
127
  {selectedItemRef.item?.sku ? (
134
128
  <h6 className='text-center font-semibold'>
135
- {(soleOption ? '' : (selectedItemRef.item.titleAsOption + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
129
+ {(soleOption ? '' : (selectedItemRef.item.optionLabel + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
136
130
  </h6>
137
131
  ) : ''}
138
132
  </div>
@@ -12,9 +12,9 @@ import {
12
12
 
13
13
  import { cn } from '@hanzo/ui/util'
14
14
 
15
- import BuyItemCard from './buy-item-card'
15
+ import BuyCard from './buy-item-card'
16
16
 
17
- const BuyItemPopup: React.FC<PropsWithChildren & {
17
+ const BuyPopup: React.FC<PropsWithChildren & {
18
18
  skuPath: string
19
19
  triggerClx?: string
20
20
  popupClx?: string
@@ -42,10 +42,10 @@ const BuyItemPopup: React.FC<PropsWithChildren & {
42
42
  </PopoverTrigger>
43
43
  <PopoverContent className={cn('relative flex flex-col p-0 px-4 pb-4 pt-2', popupClx)}>
44
44
  <PopoverClose className='absolute z-20 right-2 top-2 self-end hover:bg-level-3 text-muted hover:text-accent p-1 rounded-full'><X className='w-5 h-5'/></PopoverClose>
45
- <BuyItemCard skuPath={skuPath} onQuantityChanged={onQuantityChanged} className={cn("w-full relative ", cardClx)}/>
45
+ <BuyCard skuPath={skuPath} onQuantityChanged={onQuantityChanged} className={cn("w-full relative ", cardClx)}/>
46
46
  </PopoverContent>
47
47
  </Popover>
48
48
  )
49
49
  }
50
50
 
51
- export default BuyItemPopup
51
+ export default BuyPopup
@@ -5,9 +5,9 @@ import { observer } from 'mobx-react-lite'
5
5
  import { Button, toast, type ButtonSizes } from '@hanzo/ui/primitives'
6
6
  import { cn } from '@hanzo/ui/util'
7
7
 
8
- import { Icons } from './Icons'
9
- import type { LineItem } from '../types'
10
- import { sendFBEvent, sendGAEvent } from '../util/analytics'
8
+ import { Icons } from '../Icons'
9
+ import type { LineItem } from '../../types'
10
+ import { sendFBEvent, sendGAEvent } from '../../util/analytics'
11
11
 
12
12
  const AddToCartWidget: React.FC<{
13
13
  item: LineItem
@@ -27,8 +27,8 @@ const AddToCartWidget: React.FC<{
27
27
  onQuantityChanged
28
28
  }) => {
29
29
 
30
- const iconClx = ghost ? 'h-4 w-4 md:h-3 md:w-3 text-muted-3 hover:text-foreground' : 'h-5 w-7 px-1'
31
- const digitClx = ghost ? 'px-2 md:px-0.5 text-foreground ' : 'sm:px-2 font-bold text-primary-fg '
30
+ const iconClx = ghost ? 'h-4 w-4 md:h-3 md:w-3 text-muted-3 hover:text-foreground' : 'h-5 w-7 px-1 opacity-50'
31
+ const digitClx = ghost ? 'px-2 md:px-0.5 text-foreground ' : 'sm:px-2 font-semibold text-primary-fg '
32
32
 
33
33
  if (disabled) {
34
34
  return (
@@ -44,12 +44,14 @@ const AddToCartWidget: React.FC<{
44
44
  if (onQuantityChanged) {
45
45
  onQuantityChanged(item.sku, old, old + 1)
46
46
  }
47
+ /*
47
48
  if (old === 0) {
48
49
  toast(`Added ${item.title} to your bag.`)
49
50
  }
50
51
  else {
51
52
  toast(`Changed quantity to ${old + 1} for ${item.title}.`)
52
53
  }
54
+ */
53
55
  sendGAEvent('add_to_cart', {
54
56
  items: [{
55
57
  item_id: item.sku,
@@ -79,12 +81,14 @@ const AddToCartWidget: React.FC<{
79
81
  if (onQuantityChanged) {
80
82
  onQuantityChanged(item.sku, old, old - 1)
81
83
  }
84
+ /*
82
85
  if (old === 1) {
83
86
  toast(`Removed ${item.title} from your bag.`)
84
87
  }
85
88
  else {
86
89
  toast(`Changed quantity to ${old - 1} for ${item.title}.`)
87
90
  }
91
+ */
88
92
  sendGAEvent('remove_from_cart', {
89
93
  items: [{
90
94
  item_id: item.sku,
@@ -115,7 +119,7 @@ const AddToCartWidget: React.FC<{
115
119
  <Icons.trash className={iconClx} aria-hidden='true'/>
116
120
  )}
117
121
  </Button>
118
- <div className={'text-sm flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}</div>
122
+ <div className={'text-sm flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}{ghost ? '' : ' in Bag'}</div>
119
123
  <Button
120
124
  aria-label={'Add another ' + item.title + ' to the cart'}
121
125
  size={size}
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+ import React, {type PropsWithChildren} from 'react'
3
+
4
+ import { type ButtonVariants, type ButtonSizes, Button } from '@hanzo/ui/primitives'
5
+
6
+ import BuyTriggerWrapper from './buy-trigger-wrapper'
7
+ import { cn } from '@hanzo/ui/util'
8
+
9
+ const BuyButton: React.FC<PropsWithChildren & {
10
+ skuPath: string
11
+ variant? : ButtonVariants
12
+ size?: ButtonSizes
13
+ /* rounded?: ButtonRounded // TODO: wait for version bump*/
14
+ className?: string
15
+ mobile?: boolean
16
+ }> = ({
17
+ skuPath,
18
+ variant,
19
+ size,
20
+ children,
21
+ 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
+ )
28
+
29
+
30
+
31
+ export default BuyButton
@@ -0,0 +1,267 @@
1
+ 'use client'
2
+ import React, { useRef, useEffect, type ComponentType } from 'react'
3
+ import { reaction, type IReactionDisposer } from 'mobx'
4
+ import { observer } from 'mobx-react-lite'
5
+
6
+ import { cn } from '@hanzo/ui/util'
7
+
8
+ import type {
9
+ SelectedPaths,
10
+ ItemSelectorProps,
11
+ ItemSelectorCompProps,
12
+ LineItem,
13
+ Category,
14
+ ProductTreeNode,
15
+ } from '../../types'
16
+
17
+ import { useCommerce } from '../../service/context'
18
+ import { getFacetValuesMutator, ObsStringMutator } from '../../util'
19
+
20
+ import LevelNodesWidget from '../level-nodes-widget'
21
+ import AddToCartWidget from './add-to-cart-widget'
22
+ import ItemMedia from '../item/item-media'
23
+ import { ApplyTypography } from '@hanzo/ui/primitives'
24
+
25
+ const BuyCard: React.FC<{
26
+ skuPath: string
27
+
28
+ /** For any selectors that show a list of choices,
29
+ * starting with 'scrollAfter' elements,
30
+ * the list will scroll, and the card will
31
+ * have 'scrollHeightClx' applied to it to define and fix the height.
32
+ * This is most like something like '80vh'
33
+ * defaults are '5' and 'h-[70vh]'
34
+ * */
35
+ scrollAfter?: number
36
+ scrollHeightClx?: string
37
+
38
+ clx?: string
39
+ catWidgetClx?: string
40
+ addWidgetClx?: string
41
+
42
+ selector: ComponentType<ItemSelectorProps>
43
+ selectorProps?: ItemSelectorCompProps
44
+
45
+ /** Show all items from all sibling categories
46
+ * If skuPath defines a Category, the first of it's items will
47
+ * be selected. The facet widget will just select a the first
48
+ * item in the chosen Category without changings the overall set
49
+ * of choices. */
50
+ allVariants?: boolean
51
+
52
+ categoryTabAs?: 'image' | 'label' | 'image-and-label'
53
+ showItemMedia?: boolean
54
+
55
+ onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
56
+ mobile?: boolean
57
+ }> = observer(({
58
+ skuPath,
59
+ scrollAfter=5,
60
+ scrollHeightClx='h-[80vh]',
61
+ clx='',
62
+ catWidgetClx='',
63
+ addWidgetClx='',
64
+ selector: Selector,
65
+ selectorProps={
66
+ clx: '',
67
+ soleItemClx: '',
68
+ itemClx: '',
69
+ ext: {}
70
+ },
71
+ categoryTabAs='image-and-label',
72
+ showItemMedia=true,
73
+ allVariants=false,
74
+ mobile=false,
75
+ onQuantityChanged,
76
+ }) => {
77
+
78
+ const cmmc = useCommerce()
79
+ const inst = useRef<{
80
+ level: number,
81
+ requestedCat : Category | undefined
82
+ requestedNode: ProductTreeNode | undefined
83
+ currentCat: ObsStringMutator
84
+ disposers: IReactionDisposer[]
85
+ } | undefined>(undefined)
86
+
87
+
88
+ useEffect(() => {
89
+
90
+ let requestedCat = cmmc.getCategory(skuPath)
91
+ let requestedNode = requestedCat ? undefined : cmmc.getNodeAtPath(skuPath)
92
+ let initialCat = undefined
93
+ const toks = skuPath.split('-')
94
+ const level = toks.length - 1
95
+ if (level === 0) {
96
+ throw new Error('BuyCard.useEffect(): must specify at least at least one Level in skuPath!')
97
+ }
98
+ const paths: SelectedPaths = {}
99
+ for (let l = 1; l <= level; l++ ) {
100
+ paths[l] = [toks[l]]
101
+ }
102
+ if (requestedCat) {
103
+ if (allVariants && level >= 2) {
104
+ // select all siblings of requestedCat.
105
+ // ie, go one level back
106
+ delete paths[level]
107
+ initialCat = requestedCat
108
+ requestedCat = undefined
109
+ requestedNode = cmmc.getNodeAtPath(toks.slice(0, -1).join('-'))
110
+ }
111
+ }
112
+ else {
113
+ // select siblings paths
114
+ if (allVariants) {
115
+ paths[level + 1] = [...requestedNode!.subNodes!.map((node) => (node.skuToken))]
116
+ }
117
+ // Actually select the the first cat at the cat level,
118
+ else {
119
+ paths[level + 1] = [requestedNode!.subNodes![0].skuToken]
120
+ }
121
+ }
122
+
123
+ const selectedCats = cmmc.selectPaths(paths)
124
+ initialCat = initialCat ? initialCat : (requestedCat ? requestedCat : selectedCats[0])
125
+ cmmc.setCurrentItem(initialCat.products[0].sku)
126
+ const currentCat = new ObsStringMutator(initialCat!.id.split('-').pop()!)
127
+
128
+ inst.current = {
129
+ level,
130
+ requestedCat,
131
+ requestedNode,
132
+ currentCat,
133
+ disposers: []
134
+ }
135
+
136
+ if (!allVariants) {
137
+ inst.current?.disposers.push(reaction(
138
+ () => {
139
+ const cats = cmmc.selectedCategories
140
+ return (cats.length > 0) ? cats[0].id : undefined
141
+ },
142
+ (catId) => {
143
+ if (catId && catId !== cmmc.currentItem?.categoryId) {
144
+ const cat = cmmc.getCategory(catId)
145
+ cmmc.setCurrentItem(cat?.products[0].sku)
146
+ }
147
+ }
148
+ ))
149
+ }
150
+
151
+ return () => {
152
+ inst.current?.disposers.forEach((d) => {d()})
153
+ }
154
+
155
+ }, [])
156
+
157
+ const setCatPath = (pathValue: string | null) => {
158
+ if (inst.current?.currentCat.get() !== pathValue) {
159
+ inst.current?.currentCat.set(pathValue)
160
+ }
161
+ const found = cmmc.selectedItems.find((item) => (item.categoryId.endsWith(pathValue!)))
162
+ if (found) {
163
+ cmmc.setCurrentItem(found.sku)
164
+ }
165
+ }
166
+
167
+ const selectSku = (sku: string) => {
168
+ cmmc.setCurrentItem(sku)
169
+ const pathValue = cmmc.currentItem?.categoryId.split('-').pop()!
170
+ if (pathValue !== inst.current?.currentCat.get()) {
171
+ inst.current?.currentCat.set(pathValue)
172
+ }
173
+ }
174
+
175
+ const catTitle = inst.current?.requestedCat ? inst.current.requestedCat.title : cmmc.selectedCategories?.[0]?.title
176
+
177
+ const itemsToShow = (!cmmc.hasSelection) ? undefined :
178
+ allVariants ? cmmc.selectedItems : cmmc.selectedCategories[0].products as LineItem[]
179
+
180
+
181
+ const TitleArea: React.FC<{
182
+ title: string
183
+ byline?: string
184
+ clx?: string
185
+ }> = ({
186
+ title,
187
+ byline,
188
+ clx=''
189
+ }) => (
190
+ <ApplyTypography className={clx}>
191
+ <h3>{title}</h3>
192
+ {byline && (<h6>{byline}</h6>)}
193
+ </ApplyTypography>
194
+ )
195
+
196
+ const scroll = !!(itemsToShow && itemsToShow.length > scrollAfter)
197
+ return (
198
+ <div className={cn(
199
+ 'px-4 md:px-6 pt-3 pb-4 flex flex-col items-center min-h-[60vh]',
200
+ clx,
201
+ scroll ? scrollHeightClx : 'h-auto'
202
+ )} >
203
+ {inst.current?.requestedNode && (<>
204
+ <ApplyTypography className={'mb-2 ' + (scroll ? 'shrink-0' : '')}>
205
+ <h3>{inst.current!.requestedNode.label}</h3>
206
+ {inst.current!.requestedNode.subNodesLabel && (
207
+ <h6 className='!text-center font-bold text-muted mt-3'>{inst.current!.requestedNode.subNodesLabel}</h6>
208
+ )}
209
+ </ApplyTypography>
210
+ <LevelNodesWidget
211
+ className={cn(
212
+ 'grid gap-0 ' + `grid-cols-${inst.current.requestedNode.subNodes!.length}`,
213
+ 'border-b-2 rounded-lg border-level-3 mb-4 -mr-2 -ml-2 max-w-[460px]',
214
+ (scroll ? 'shrink-0' : ''),
215
+ catWidgetClx
216
+ )}
217
+ mobile={mobile}
218
+ mutator={allVariants ?
219
+ {
220
+ get: () => (inst.current!.currentCat.s),
221
+ set: setCatPath
222
+ }
223
+ :
224
+ getFacetValuesMutator(inst.current.level + 1, cmmc)
225
+ }
226
+ itemClx='flex-col h-auto gap-0 py-1 px-3'
227
+ buttonClx={
228
+ 'h-full ' +
229
+ '!border-level-3'
230
+ }
231
+ levelNodes={inst.current.requestedNode.subNodes!}
232
+ show={categoryTabAs}
233
+ />
234
+ </>)}
235
+ {!inst.current?.requestedNode && catTitle && (
236
+ <TitleArea title={catTitle} clx=''/>
237
+ )}
238
+ {(cmmc.currentItem && showItemMedia) && (
239
+ <ItemMedia item={cmmc.currentItem} constrainTo={{w: 200, h: 200}} clx={(scroll ? 'shrink-0' : '')}/>
240
+ )}
241
+ {itemsToShow && (
242
+ <Selector
243
+ items={itemsToShow}
244
+ selectedItemRef={cmmc}
245
+ selectSku={selectSku}
246
+ {...selectorProps}
247
+ scrollList={scroll}
248
+ showCategory={selectorProps.showCategory ?
249
+ selectorProps.showCategory
250
+ :
251
+ (allVariants && !inst.current?.requestedCat)
252
+ }
253
+ />
254
+ )}
255
+ {(cmmc.currentItem) && (
256
+ <AddToCartWidget
257
+ size='default'
258
+ item={cmmc.currentItem}
259
+ onQuantityChanged={onQuantityChanged}
260
+ className={cn('min-w-[160px] mx-auto mt-4', (scroll ? 'shrink-0' : ''), addWidgetClx)}
261
+ />
262
+ )}
263
+ </div >
264
+ )
265
+ })
266
+
267
+ export default BuyCard
@@ -0,0 +1,89 @@
1
+ 'use client'
2
+ import React, { useState, type ReactNode } from 'react'
3
+
4
+ import { X as LucideX} from 'lucide-react'
5
+
6
+ import {
7
+ Drawer,
8
+ DrawerTrigger,
9
+ DrawerContent,
10
+ Button,
11
+ } from '@hanzo/ui/primitives'
12
+
13
+ import { cn } from '@hanzo/ui/util'
14
+
15
+ import { getUISpecFromPath } from '../../util'
16
+
17
+ import BuyCard from './buy-card'
18
+ import RadioItemSelector from '../select/radio-selector'
19
+ import ImageItemSelector from '../select/image-selector'
20
+ import CarouselItemSelector from '../select/carousel-selector'
21
+
22
+ const BuyDrawer: React.FC<{
23
+ skuPath: string
24
+ trigger: ReactNode
25
+ triggerClx?: string
26
+ drawerClx?: string
27
+ cardClx?: string
28
+ mobile?: boolean
29
+ }> = ({
30
+ skuPath,
31
+ trigger,
32
+ triggerClx='',
33
+ drawerClx='',
34
+ cardClx='',
35
+ mobile=false
36
+ }) => {
37
+
38
+ const [open, setOpen] = useState<boolean>(false)
39
+
40
+ const onQuantityChanged = (sku: string, oldV: number, newV: number) => {
41
+ //if (oldV === 0 && newV === 1) {
42
+ // setTimeout(() => {setOpen(false)}, 150)
43
+ //}
44
+ }
45
+
46
+ const spec = getUISpecFromPath(skuPath) ?? {
47
+ selector: 'radio',
48
+ allVariants: false
49
+ }
50
+
51
+ const selector = spec.selector === 'radio' ? RadioItemSelector :
52
+ (spec.selector === 'image' ? ImageItemSelector : CarouselItemSelector)
53
+
54
+ return (
55
+ <Drawer open={open} onOpenChange={setOpen} >
56
+ <DrawerTrigger asChild className={triggerClx}>
57
+ {trigger}
58
+ </DrawerTrigger>
59
+ <DrawerContent
60
+ className={cn('rounded-t-xl z-[52] mt-6 pb-12 h-auto min-h-[35vh] pt-6 md:max-w-[550px] md:mx-auto', drawerClx)}
61
+ overlayClx='z-[51]'
62
+ >
63
+ <BuyCard
64
+ skuPath={skuPath}
65
+ scrollAfter={spec.selector === 'carousel' ? 999 : undefined}
66
+ mobile={mobile}
67
+ onQuantityChanged={onQuantityChanged}
68
+ clx={cn('w-full', cardClx)}
69
+ selector={selector}
70
+ selectorProps={{soleItemClx:'mb-3'}}
71
+ showItemMedia={spec.selector !== 'carousel'}
72
+ categoryTabAs='label'
73
+ allVariants={spec.allVariants}
74
+ />
75
+ <Button
76
+ variant='ghost'
77
+ size='icon'
78
+ onClick={() => {setOpen(false)}}
79
+ className={'absolute top-4 right-4 w-8 h-8 group rounded-full p-1 hidden md:flex items-center'}
80
+ >
81
+ <LucideX className='w-6 h-6 text-muted group-hover:text-foreground'/>
82
+ </Button>
83
+
84
+ </DrawerContent>
85
+ </Drawer>
86
+ )
87
+ }
88
+
89
+ export default BuyDrawer
@@ -0,0 +1,20 @@
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