@hanzo/commerce 6.1.12 → 6.2.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 (55) hide show
  1. package/components/buy/_to_deprecate_select-family-item-card.tsx +3 -4
  2. package/components/buy/_to_deprecate_select-family-item-panel.tsx +4 -5
  3. package/components/buy/add-to-cart-widget.tsx +43 -32
  4. package/components/buy/buy-button.tsx +1 -1
  5. package/components/buy/buy-card.tsx +17 -26
  6. package/components/buy/buy-drawer.tsx +5 -24
  7. package/components/buy/carousel-buy-card.tsx +145 -194
  8. package/components/buy/foo.txt +27 -0
  9. package/components/cart/cart-panel/cart-line-item.tsx +1 -1
  10. package/components/checkout/payment-step-form/contact-form.tsx +2 -2
  11. package/components/checkout/payment-step-form/methods/crypto.tsx +1 -2
  12. package/components/checkout/shipping-step-form.tsx +5 -5
  13. package/components/index.ts +1 -3
  14. package/components/item-selector/button.tsx +175 -0
  15. package/components/item-selector/carousel.tsx +162 -0
  16. package/components/item-selector/index.ts +5 -0
  17. package/components/quantity-indicator.tsx +48 -0
  18. package/components/select-family/family-carousel/index.tsx +47 -38
  19. package/components/select-family/family-carousel/slide.tsx +83 -0
  20. package/components/select-family/family-carousel/state.ts +83 -0
  21. package/components/select-family/level-nodes-widget/index.tsx +2 -2
  22. package/components/select-family/level-nodes-widget/node-image.tsx +2 -2
  23. package/index.ts +7 -7
  24. package/package.json +7 -4
  25. package/service/context.tsx +8 -7
  26. package/service/debug.ts +41 -0
  27. package/service/get-instance.ts +3 -0
  28. package/service/impls/standalone/actual-line-item.ts +5 -7
  29. package/service/impls/standalone/index.ts +11 -3
  30. package/service/impls/standalone/standalone-service.ts +133 -38
  31. package/service/path-utils.ts +26 -0
  32. package/service/sep.ts +7 -0
  33. package/types/category-node.ts +38 -0
  34. package/types/commerce-service.ts +44 -12
  35. package/types/family.ts +4 -1
  36. package/types/index.ts +3 -2
  37. package/types/item-selector.ts +29 -14
  38. package/types/product.ts +5 -1
  39. package/types/selection-ui-specifier.ts +36 -0
  40. package/types/token-separators.ts +7 -0
  41. package/util/error.ts +34 -0
  42. package/util/index.ts +4 -1
  43. package/util/product-media-accessor.ts +58 -0
  44. package/util/promo-codes.ts +1 -2
  45. package/util/selection-ui-specifiers.ts +34 -0
  46. package/util/use-sync-sku-param-w-current-item.ts +2 -2
  47. package/components/select-family/family-carousel/family-slide.tsx +0 -36
  48. package/components/select-product/carousel-selector.tsx +0 -112
  49. package/components/select-product/image-selector.tsx +0 -162
  50. package/components/select-product/radio-selector.tsx +0 -125
  51. package/service/impls/index.ts +0 -3
  52. package/types/buy-ui-spec.ts +0 -8
  53. package/types/tree-node.ts +0 -24
  54. package/util/buy-ui-conf.ts +0 -35
  55. package/util/sep.ts +0 -5
@@ -8,7 +8,7 @@ import { Skeleton } from '@hanzo/ui/primitives'
8
8
  import type { ItemSelector, LineItem } from '../../types'
9
9
 
10
10
  import AddToCartWidget from './add-to-cart-widget'
11
- import RadioItemSelector from '../select-product/radio-selector'
11
+ import { ButtonItemSelector } from '../item-selector'
12
12
  import { formatCurrencyValue } from '../../util'
13
13
 
14
14
  const SelectFamilyItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & ItemSelector & {
@@ -50,13 +50,12 @@ const SelectFamilyItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & Item
50
50
  className
51
51
  )}>
52
52
  {title && (<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } /> )}
53
- <RadioItemSelector
53
+ <ButtonItemSelector
54
54
  items={items}
55
55
  selectedItemRef={selItemRef}
56
56
  selectSku={selectSku}
57
57
  clx='mt-2'
58
- showQuantity={false}
59
- scrollList={false}
58
+ scrollable={false}
60
59
  itemClx='flex flex-row gap-2.5 items-center'
61
60
  />
62
61
  </div>
@@ -11,7 +11,7 @@ import { formatCurrencyValue } from '../../util'
11
11
  import { Icons } from '../Icons'
12
12
 
13
13
  import AddToCartWidget from './add-to-cart-widget'
14
- import RadioItemSelector from '../select-product/radio-selector'
14
+ import { ButtonItemSelector } from '../item-selector'
15
15
 
16
16
  const SelectFamilyItemPanel: React.FC<
17
17
  React.HTMLAttributes<HTMLDivElement> &
@@ -91,13 +91,12 @@ const SelectFamilyItemPanel: React.FC<
91
91
  <h6 className='text-center font-semibold'>Available options</h6>
92
92
  <div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } />
93
93
  </div>
94
- <RadioItemSelector
94
+ <ButtonItemSelector
95
95
  items={family.products as LineItem[]}
96
96
  selectedItemRef={selectedItemRef}
97
97
  selectSku={selectSku}
98
- showQuantity={showQuantity}
99
- scrollList={false}
100
-
98
+ options={{ showQuantity }}
99
+ scrollable={false}
101
100
  clx='block columns-2 gap-4'
102
101
  itemClx='flex flex-row gap-2 items-center mb-2.5'
103
102
  />
@@ -2,7 +2,7 @@
2
2
  import React from 'react'
3
3
  import { observer } from 'mobx-react-lite'
4
4
 
5
- import { Button, toast, type ButtonSizes } from '@hanzo/ui/primitives'
5
+ import { Button, toast, type ButtonSizes, type ButtonVariants } from '@hanzo/ui/primitives'
6
6
  import { cn } from '@hanzo/ui/util'
7
7
 
8
8
  import { Icons } from '../Icons'
@@ -11,15 +11,15 @@ import { sendFBEvent, sendGAEvent } from '../../util/analytics'
11
11
 
12
12
  const AddToCartWidget: React.FC<{
13
13
  item: LineItem
14
- ghost?: boolean
15
14
  disabled?: boolean
16
15
  className?: string
17
16
  buttonClx?: string
18
17
  size?: ButtonSizes
18
+ variant?: 'minimal' | 'primary' | 'outline'
19
19
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
20
20
  }> = observer(({
21
21
  item,
22
- ghost=false,
22
+ variant='primary',
23
23
  disabled=false,
24
24
  className='',
25
25
  buttonClx='',
@@ -27,12 +27,23 @@ 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 opacity-50'
31
- const digitClx = ghost ? 'px-2 md:px-0.5 text-foreground ' : 'sm:px-2 font-semibold text-primary-fg '
30
+ const ROUNDED_VAL = 'lg'
31
+ // no need to safelist, since its used widely
32
+ const ROUNDED_CLX = ` rounded-${ROUNDED_VAL} `
33
+
34
+ const ghost = variant === 'minimal'
35
+ const outline = variant === 'outline'
36
+ const primary = variant === 'primary'
37
+
38
+ let iconClx = ghost ? 'h-4 w-4 md:h-3 md:w-3 text-muted-3 ' : 'h-5 w-7 px-1 opacity-70 '
39
+ iconClx += ghost ? 'group-hover:text-foreground' : 'group-hover:opacity-100'
40
+
41
+ let digitClx = ghost ? 'px-2 md:px-0.5 ' : 'sm:px-2 font-semibold '
42
+ digitClx += (ghost || outline) ? 'text-foreground ' : 'text-primary-fg '
32
43
 
33
44
  if (disabled) {
34
45
  return (
35
- <div className={cn('flex flex-row items-stretch ' + (ghost ? 'bg-transparent rounded-xl' : 'bg-primary rounded-xl'), className)}>
46
+ <div className={cn('flex flex-row items-stretch' + ROUNDED_CLX + (!primary ? 'bg-transparent' : 'bg-primary'), className)}>
36
47
  <div className={'text-sm flex items-center cursor-default ' + digitClx} >{item.quantity}</div>
37
48
  </div>
38
49
  )
@@ -44,14 +55,6 @@ const AddToCartWidget: React.FC<{
44
55
  if (onQuantityChanged) {
45
56
  onQuantityChanged(item.sku, old, old + 1)
46
57
  }
47
- /*
48
- if (old === 0) {
49
- toast(`Added ${item.title} to your bag.`)
50
- }
51
- else {
52
- toast(`Changed quantity to ${old + 1} for ${item.title}.`)
53
- }
54
- */
55
58
  sendGAEvent('add_to_cart', {
56
59
  items: [{
57
60
  item_id: item.sku,
@@ -81,14 +84,6 @@ const AddToCartWidget: React.FC<{
81
84
  if (onQuantityChanged) {
82
85
  onQuantityChanged(item.sku, old, old - 1)
83
86
  }
84
- /*
85
- if (old === 1) {
86
- toast(`Removed ${item.title} from your bag.`)
87
- }
88
- else {
89
- toast(`Changed quantity to ${old - 1} for ${item.title}.`)
90
- }
91
- */
92
87
  sendGAEvent('remove_from_cart', {
93
88
  items: [{
94
89
  item_id: item.sku,
@@ -103,13 +98,24 @@ const AddToCartWidget: React.FC<{
103
98
  }
104
99
 
105
100
  return ( item.isInCart ? (
106
- <div className={cn('flex flex-row items-stretch justify-center ' + (ghost ? 'bg-transparent rounded-xl' : 'bg-primary rounded-xl'), className)}>
101
+ <div className={cn(
102
+ 'flex flex-row items-stretch justify-between ',
103
+ ROUNDED_CLX,
104
+ (primary ? 'bg-primary' : 'bg-transparent'),
105
+ (outline ? 'border border-muted' : ''),
106
+ className
107
+ )}>
107
108
  <Button
108
109
  aria-label={'Remove a ' + item.title + ' from the cart'}
109
110
  size={size}
110
- variant={ghost ? 'ghost' : 'primary'}
111
- rounded={ghost ? 'full' : 'xl'}
112
- className={cn('px-1 lg:min-w-0 lg:px-2 xs:justify-end', buttonClx)}
111
+ variant={primary ? 'primary' : 'ghost'}
112
+ rounded={ghost ? 'full' : ROUNDED_VAL}
113
+ className={cn(
114
+ 'lg:min-w-0 lg:px-2 grow justify-start group',
115
+ (ghost ? 'px-1' : 'px-2'),
116
+ (outline ? 'hover:bg-transparent' : ''),
117
+ buttonClx
118
+ )}
113
119
  key='left'
114
120
  onClick={dec}
115
121
  >
@@ -119,13 +125,18 @@ const AddToCartWidget: React.FC<{
119
125
  <Icons.trash className={iconClx} aria-hidden='true'/>
120
126
  )}
121
127
  </Button>
122
- <div className={'text-sm flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}{ghost ? '' : ' in Bag'}</div>
128
+ <div className={'text-sm grow-0 shrink-0 flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}{ghost ? '' : ' in Bag'}</div>
123
129
  <Button
124
130
  aria-label={'Add another ' + item.title + ' to the cart'}
125
131
  size={size}
126
- variant={ghost ? 'ghost' : 'primary'}
127
- rounded={ghost ? 'full' : 'xl'}
128
- className={cn('px-1 lg:min-w-0 lg:px-2 xs:justify-start', buttonClx)}
132
+ variant={primary ? 'primary' : 'ghost'}
133
+ rounded={ghost ? 'full' : ROUNDED_VAL}
134
+ className={cn(
135
+ 'lg:min-w-0 lg:px-2 grow justify-end group',
136
+ (ghost ? 'px-1' : 'px-2'),
137
+ (outline ? 'hover:bg-transparent' : ''),
138
+ buttonClx
139
+ )}
129
140
  onClick={inc}
130
141
  key='right'
131
142
  >
@@ -136,8 +147,8 @@ const AddToCartWidget: React.FC<{
136
147
  <Button
137
148
  aria-label={'Add a ' + item.title + ' to cart'}
138
149
  size={size}
139
- variant='primary'
140
- rounded='xl'
150
+ variant={variant as ButtonVariants}
151
+ rounded={ROUNDED_VAL}
141
152
  className={cn(buttonClx, className)}
142
153
  onClick={inc}
143
154
  >
@@ -10,7 +10,7 @@ const BuyButton: React.FC<PropsWithChildren & {
10
10
  skuPath: string
11
11
  variant? : ButtonVariants
12
12
  size?: ButtonSizes
13
- /* rounded?: ButtonRounded // TODO: wait for version bump*/
13
+ /* rounded?: ButtonRoundedValue // TODO: wait for version bump*/
14
14
  className?: string
15
15
  mobile?: boolean
16
16
  }> = ({
@@ -7,16 +7,15 @@ import { cn } from '@hanzo/ui/util'
7
7
  import { ApplyTypography, MediaStack } from '@hanzo/ui/primitives'
8
8
 
9
9
  import type {
10
- SelectedPaths,
11
10
  ItemSelectorProps,
12
11
  ItemSelectorCompProps,
13
12
  LineItem,
14
13
  Family,
15
- ProductTreeNode,
14
+ CategoryNode,
16
15
  } from '../../types'
17
16
 
18
17
  import { useCommerce } from '../../service/context'
19
- import SEP from '../../util/sep'
18
+ import * as pathUtils from '../../service/path-utils'
20
19
  import { getFacetValuesMutator, ObsStringMutator } from '../../util'
21
20
 
22
21
  import LevelNodesWidget from '../select-family/level-nodes-widget'
@@ -79,8 +78,8 @@ const BuyCard: React.FC<{
79
78
  const inst = useRef<{
80
79
  level: number,
81
80
  requestedFamily : Family | undefined
82
- requestedNode: ProductTreeNode | undefined
83
- currentFamily: ObsStringMutator
81
+ requestedNode: CategoryNode | undefined
82
+ currentFamTokenMutator: ObsStringMutator
84
83
  disposers: IReactionDisposer[]
85
84
  } | undefined>(undefined)
86
85
 
@@ -90,15 +89,12 @@ const BuyCard: React.FC<{
90
89
  let requestedFamily = cmmc.getFamily(skuPath)
91
90
  let requestedNode = requestedFamily ? undefined : cmmc.getNodeAtPath(skuPath)
92
91
  let initialFamily = undefined
93
- const toks = skuPath.split(SEP.TOK)
94
- const level = toks.length - 1
92
+
93
+ const { paths, level } = pathUtils.getSelectedPaths(skuPath)
94
+
95
95
  if (level === 0) {
96
96
  throw new Error('BuyCard.useEffect(): must specify at least at least one Level in skuPath!')
97
97
  }
98
- const paths: SelectedPaths = {}
99
- for (let l = 1; l <= level; l++ ) {
100
- paths[l] = [toks[l]]
101
- }
102
98
  if (requestedFamily) {
103
99
  if (allVariants && level >= 2) {
104
100
  // select all siblings of requestedFamily.
@@ -106,7 +102,7 @@ const BuyCard: React.FC<{
106
102
  delete paths[level]
107
103
  initialFamily = requestedFamily
108
104
  requestedFamily = undefined
109
- requestedNode = cmmc.getNodeAtPath(toks.slice(0, -1).join(SEP.TOK))
105
+ requestedNode = cmmc.getNodeAtPath(pathUtils.getParentPath(skuPath))
110
106
  }
111
107
  }
112
108
  else {
@@ -123,13 +119,13 @@ const BuyCard: React.FC<{
123
119
  const selectedFams = cmmc.selectPaths(paths)
124
120
  initialFamily = initialFamily ? initialFamily : (requestedFamily ? requestedFamily : selectedFams[0])
125
121
  cmmc.setCurrentItem(initialFamily.products[0].sku)
126
- const currentFamily = new ObsStringMutator(initialFamily!.id.split(SEP.TOK).pop()!)
122
+ const currentFamTokenMutator = new ObsStringMutator(pathUtils.lastToken(initialFamily!.id))
127
123
 
128
124
  inst.current = {
129
125
  level,
130
126
  requestedFamily,
131
127
  requestedNode,
132
- currentFamily,
128
+ currentFamTokenMutator,
133
129
  disposers: []
134
130
  }
135
131
 
@@ -157,8 +153,8 @@ const BuyCard: React.FC<{
157
153
  }, [])
158
154
 
159
155
  const setFamilyPath = (pathValue: string | null) => {
160
- if (inst.current?.currentFamily.get() !== pathValue) {
161
- inst.current?.currentFamily.set(pathValue)
156
+ if (inst.current?.currentFamTokenMutator.get() !== pathValue) {
157
+ inst.current?.currentFamTokenMutator.set(pathValue)
162
158
  }
163
159
  const found = cmmc.selectedItems.find((item) => (item.familyId.endsWith(pathValue!)))
164
160
  if (found) {
@@ -168,9 +164,9 @@ const BuyCard: React.FC<{
168
164
 
169
165
  const selectSku = (sku: string) => {
170
166
  cmmc.setCurrentItem(sku)
171
- const pathValue = cmmc.currentItem?.familyId.split(SEP.TOK).pop()!
172
- if (pathValue !== inst.current?.currentFamily.get()) {
173
- inst.current?.currentFamily.set(pathValue)
167
+ const pathValue = pathUtils.lastToken(cmmc.currentItem!.familyId)
168
+ if (pathValue !== inst.current?.currentFamTokenMutator.get()) {
169
+ inst.current?.currentFamTokenMutator.set(pathValue)
174
170
  }
175
171
  }
176
172
 
@@ -219,7 +215,7 @@ const BuyCard: React.FC<{
219
215
  mobile={mobile}
220
216
  mutator={allVariants ?
221
217
  {
222
- get: () => (inst.current!.currentFamily.s),
218
+ get: () => (inst.current!.currentFamTokenMutator.s),
223
219
  set: setFamilyPath
224
220
  }
225
221
  :
@@ -246,12 +242,7 @@ const BuyCard: React.FC<{
246
242
  selectedItemRef={cmmc}
247
243
  selectSku={selectSku}
248
244
  {...selectorProps}
249
- scrollList={scroll}
250
- showFamily={selectorProps.showFamily ?
251
- selectorProps.showFamily
252
- :
253
- (allVariants && !inst.current?.requestedFamily)
254
- }
245
+ scrollable={scroll}
255
246
  />
256
247
  )}
257
248
  {(cmmc.currentItem) && (
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
  import React, { useState, type ReactNode } from 'react'
3
+ import { useRouter } from 'next/navigation'
3
4
 
4
5
  import { X as LucideX} from 'lucide-react'
5
6
 
@@ -12,14 +13,9 @@ import {
12
13
 
13
14
  import { cn } from '@hanzo/ui/util'
14
15
 
15
- import { getUISpecFromPath } from '../../util'
16
-
17
16
  //import BuyCard from './buy-card'
18
- import FamilyCar from '../select-family/family-carousel'
19
17
 
20
- import RadioItemSelector from '../select-product/radio-selector'
21
- import ImageItemSelector from '../select-product/image-selector'
22
- import CarouselItemSelector from '../select-product/carousel-selector'
18
+ import CarouselBuyCard from './carousel-buy-card'
23
19
 
24
20
  const BuyDrawer: React.FC<{
25
21
  skuPath: string
@@ -38,20 +34,7 @@ const BuyDrawer: React.FC<{
38
34
  }) => {
39
35
 
40
36
  const [open, setOpen] = useState<boolean>(false)
41
-
42
- const onQuantityChanged = (sku: string, oldV: number, newV: number) => {
43
- //if (oldV === 0 && newV === 1) {
44
- // setTimeout(() => {setOpen(false)}, 150)
45
- //}
46
- }
47
-
48
- const spec = getUISpecFromPath(skuPath) ?? {
49
- selector: 'radio',
50
- allVariants: false
51
- }
52
-
53
- const selector = spec.selector === 'radio' ? RadioItemSelector :
54
- (spec.selector === 'image' ? ImageItemSelector : CarouselItemSelector)
37
+ const router = useRouter()
55
38
 
56
39
  return (
57
40
  <Drawer open={open} onOpenChange={setOpen} >
@@ -61,13 +44,11 @@ const BuyDrawer: React.FC<{
61
44
  <DrawerContent
62
45
  className={cn('rounded-t-xl mt-6 pb-12 h-auto min-h-[35vh] pt-6 md:max-w-[550px] md:mx-auto', drawerClx)}
63
46
  >
64
-
65
- <FamilyCar
47
+ <CarouselBuyCard
66
48
  skuPath={skuPath}
49
+ handleCheckout={() => {router.push('/checkout')}}
67
50
  mobile={mobile}
68
- onQuantityChanged={onQuantityChanged}
69
51
  clx={cn('w-full', cardClx)}
70
-
71
52
  />
72
53
 
73
54
  <Button