@hanzo/commerce 6.0.0 → 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.
@@ -39,7 +39,7 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
39
39
  const item = items[0] as LineItem
40
40
  return (
41
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.titleAsOption + ', ' + formatCurrencyValue(item.price)}</p>
42
+ <p className='text-lg text-center font-semibold'>{item.optionLabel + ', ' + formatCurrencyValue(item.price)}</p>
43
43
  </div>
44
44
  )
45
45
  }
@@ -56,6 +56,7 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
56
56
  selectSku={selectSku}
57
57
  clx='mt-2'
58
58
  showQuantity={false}
59
+ scrollList={false}
59
60
  itemClx='flex flex-row gap-2.5 items-center'
60
61
  />
61
62
  </div>
@@ -52,7 +52,7 @@ const SelectCategoryItemPanel: React.FC<
52
52
  <div className={cn('w-full border rounded-xl p-6 ')}>
53
53
  <div className={cn('w-full aspect-square relative')}>
54
54
  <Image
55
- src={category.img}
55
+ src={category.img.src}
56
56
  fill
57
57
  sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, (max-width: 1200px) 50vw, 20vw"
58
58
  alt={category.title}
@@ -96,6 +96,8 @@ const SelectCategoryItemPanel: React.FC<
96
96
  selectedItemRef={selectedItemRef}
97
97
  selectSku={selectSku}
98
98
  showQuantity={showQuantity}
99
+ scrollList={false}
100
+
99
101
  clx='block columns-2 gap-4'
100
102
  itemClx='flex flex-row gap-2 items-center mb-2.5'
101
103
  />
@@ -124,7 +126,7 @@ const SelectCategoryItemPanel: React.FC<
124
126
  </h3>
125
127
  {selectedItemRef.item?.sku ? (
126
128
  <h6 className='text-center font-semibold'>
127
- {(soleOption ? '' : (selectedItemRef.item.titleAsOption + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
129
+ {(soleOption ? '' : (selectedItemRef.item.optionLabel + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
128
130
  </h6>
129
131
  ) : ''}
130
132
  </div>
@@ -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 (
@@ -81,12 +81,14 @@ const AddToCartWidget: React.FC<{
81
81
  if (onQuantityChanged) {
82
82
  onQuantityChanged(item.sku, old, old - 1)
83
83
  }
84
+ /*
84
85
  if (old === 1) {
85
86
  toast(`Removed ${item.title} from your bag.`)
86
87
  }
87
88
  else {
88
89
  toast(`Changed quantity to ${old - 1} for ${item.title}.`)
89
90
  }
91
+ */
90
92
  sendGAEvent('remove_from_cart', {
91
93
  items: [{
92
94
  item_id: item.sku,
@@ -117,7 +119,7 @@ const AddToCartWidget: React.FC<{
117
119
  <Icons.trash className={iconClx} aria-hidden='true'/>
118
120
  )}
119
121
  </Button>
120
- <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>
121
123
  <Button
122
124
  aria-label={'Add another ' + item.title + ' to the cart'}
123
125
  size={size}
@@ -1,98 +1,182 @@
1
1
  'use client'
2
2
  import React, { useRef, useEffect, type ComponentType } from 'react'
3
- import { autorun } from 'mobx'
3
+ import { reaction, type IReactionDisposer } from 'mobx'
4
4
  import { observer } from 'mobx-react-lite'
5
5
 
6
6
  import { cn } from '@hanzo/ui/util'
7
7
 
8
- import type { SelectedPaths, ItemSelectorProps, LineItem } from '../../types'
8
+ import type {
9
+ SelectedPaths,
10
+ ItemSelectorProps,
11
+ ItemSelectorCompProps,
12
+ LineItem,
13
+ Category,
14
+ ProductTreeNode,
15
+ } from '../../types'
16
+
9
17
  import { useCommerce } from '../../service/context'
10
- import { getFacetValuesMutator } from '../../util'
18
+ import { getFacetValuesMutator, ObsStringMutator } from '../../util'
11
19
 
12
- import FacetValuesWidget from '../facet-values-widget'
20
+ import LevelNodesWidget from '../level-nodes-widget'
13
21
  import AddToCartWidget from './add-to-cart-widget'
14
22
  import ItemMedia from '../item/item-media'
15
23
  import { ApplyTypography } from '@hanzo/ui/primitives'
16
24
 
17
25
  const BuyCard: React.FC<{
18
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
+
19
42
  selector: ComponentType<ItemSelectorProps>
20
- selShowPrice?: boolean
21
- selShowQuantity?: boolean
22
- selClx?: string
23
- selSoleItemClx?: string
24
- selItemClx?: string
25
- selExt?: any
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. */
26
50
  allVariants?: boolean
27
- className?: string
28
- facetsWidgetClx?: string
29
- facetsAs?: 'image' | 'label' | 'image-and-label'
30
- addWidgetClx?: string
31
- mobile?: boolean
51
+
52
+ categoryTabAs?: 'image' | 'label' | 'image-and-label'
53
+ showItemMedia?: boolean
54
+
32
55
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
56
+ mobile?: boolean
33
57
  }> = observer(({
34
58
  skuPath,
35
- className='',
36
- facetsWidgetClx='',
37
- selector: Selector,
38
- selShowPrice=true,
39
- selShowQuantity=false,
40
- selClx='',
41
- selItemClx='',
42
- selSoleItemClx='',
43
- selExt,
59
+ scrollAfter=5,
60
+ scrollHeightClx='h-[80vh]',
61
+ clx='',
62
+ catWidgetClx='',
44
63
  addWidgetClx='',
45
- facetsAs='image-and-label',
46
- allVariants=true,
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,
47
74
  mobile=false,
48
75
  onQuantityChanged,
49
76
  }) => {
50
77
 
51
78
  const cmmc = useCommerce()
52
- const levelRef = useRef<number>(-1)
79
+ const inst = useRef<{
80
+ level: number,
81
+ requestedCat : Category | undefined
82
+ requestedNode: ProductTreeNode | undefined
83
+ currentCat: ObsStringMutator
84
+ disposers: IReactionDisposer[]
85
+ } | undefined>(undefined)
53
86
 
54
- const requestedCat = cmmc.getCategory(skuPath)
55
- const levelNode = requestedCat ? undefined : cmmc.getNodeAtPath(skuPath)
56
87
 
57
88
  useEffect(() => {
58
89
 
90
+ let requestedCat = cmmc.getCategory(skuPath)
91
+ let requestedNode = requestedCat ? undefined : cmmc.getNodeAtPath(skuPath)
92
+ let initialCat = undefined
59
93
  const toks = skuPath.split('-')
60
- levelRef.current = toks.length - 1
61
- if (levelRef.current === 0) {
62
- throw new Error('BuyCard.useEffect(): must specify at least a Level 1 skuPath!')
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!')
63
97
  }
64
- const fsv: SelectedPaths = {}
65
- for (let level = 1; level <= levelRef.current; level++ ) {
66
- fsv[level] = [toks[level]]
98
+ const paths: SelectedPaths = {}
99
+ for (let l = 1; l <= level; l++ ) {
100
+ paths[l] = [toks[l]]
67
101
  }
68
- // Actually specify the requested Cat,
69
- // or the first Cat if no Cats at this level
70
- if (!requestedCat) {
71
- fsv[levelRef.current + 1] = [levelNode!.subNodes![0].skuToken]
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
+ }
72
111
  }
73
- cmmc.selectPaths(fsv)
74
-
75
- return autorun(() => {
76
- const cats = cmmc.selectedCategories
77
- // Original requestedCat was legit
78
- if (requestedCat /* && (cats.length === 0 || cats[0].id !== requestedCat.id) */) {
79
- if (
80
- !cmmc.currentItem
81
- ||
82
- cmmc.currentItem.categoryId !== requestedCat.id
83
- ||
84
- requestedCat.products.length === 1
85
- ) {
86
- cmmc.setCurrentItem(requestedCat.products[0].sku)
87
- }
112
+ else {
113
+ // select siblings paths
114
+ if (allVariants) {
115
+ paths[level + 1] = [...requestedNode!.subNodes!.map((node) => (node.skuToken))]
88
116
  }
89
- else if (cats.length > 0) {
90
- if (!cmmc.currentItem || cmmc.currentItem.categoryId !== cats[0].id) {
91
- cmmc.setCurrentItem(cats[0].products[0].sku)
92
- }
117
+ // Actually select the the first cat at the cat level,
118
+ else {
119
+ paths[level + 1] = [requestedNode!.subNodes![0].skuToken]
93
120
  }
94
- })
95
- }, [requestedCat, levelNode?.subNodes])
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
+
96
180
 
97
181
  const TitleArea: React.FC<{
98
182
  title: string
@@ -109,49 +193,63 @@ const BuyCard: React.FC<{
109
193
  </ApplyTypography>
110
194
  )
111
195
 
112
- const catTitle = requestedCat ? requestedCat.title : cmmc.selectedCategories?.[0]?.title
113
-
196
+ const scroll = !!(itemsToShow && itemsToShow.length > scrollAfter)
114
197
  return (
115
- <div className={cn('px-4 md:px-6 pt-3 pb-4 flex flex-col items-center', className)} >
116
- {levelNode && (<>
117
- <ApplyTypography className=''>
118
- <h3>{levelNode.label}</h3>
119
- {levelNode.subNodesLabel && (
120
- <h6 className='!text-center mt-4 mb-1'>{levelNode.subNodesLabel}</h6>
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>
121
208
  )}
122
209
  </ApplyTypography>
123
- <FacetValuesWidget
210
+ <LevelNodesWidget
124
211
  className={cn(
125
- 'grid gap-0 ' + `grid-cols-${levelNode!.subNodes!.length}`,
212
+ 'grid gap-0 ' + `grid-cols-${inst.current.requestedNode.subNodes!.length}`,
126
213
  'border-b-2 rounded-lg border-level-3 mb-4 -mr-2 -ml-2 max-w-[460px]',
127
- facetsWidgetClx
214
+ (scroll ? 'shrink-0' : ''),
215
+ catWidgetClx
128
216
  )}
129
217
  mobile={mobile}
130
- mutator={getFacetValuesMutator(levelRef.current + 1, cmmc)}
218
+ mutator={allVariants ?
219
+ {
220
+ get: () => (inst.current!.currentCat.s),
221
+ set: setCatPath
222
+ }
223
+ :
224
+ getFacetValuesMutator(inst.current.level + 1, cmmc)
225
+ }
131
226
  itemClx='flex-col h-auto gap-0 py-1 px-3'
132
227
  buttonClx={
133
228
  'h-full ' +
134
229
  '!border-level-3'
135
230
  }
136
- facetValues={levelNode!.subNodes!}
137
- show={facetsAs}
231
+ levelNodes={inst.current.requestedNode.subNodes!}
232
+ show={categoryTabAs}
138
233
  />
139
234
  </>)}
140
- {!levelNode && catTitle && (
235
+ {!inst.current?.requestedNode && catTitle && (
141
236
  <TitleArea title={catTitle} clx=''/>
142
237
  )}
143
- {(cmmc.currentItem) && (<ItemMedia item={cmmc.currentItem} />)}
144
- {cmmc.selectedCategories[0] && (
238
+ {(cmmc.currentItem && showItemMedia) && (
239
+ <ItemMedia item={cmmc.currentItem} constrainTo={{w: 200, h: 200}} clx={(scroll ? 'shrink-0' : '')}/>
240
+ )}
241
+ {itemsToShow && (
145
242
  <Selector
146
- items={cmmc.selectedCategories[0].products as LineItem[]}
243
+ items={itemsToShow}
147
244
  selectedItemRef={cmmc}
148
- selectSku={cmmc.setCurrentItem.bind(cmmc)}
149
- clx={selClx}
150
- itemClx={selItemClx}
151
- soleItemClx={selSoleItemClx}
152
- ext={selExt}
153
- showPrice={selShowPrice}
154
- showQuantity={selShowQuantity}
245
+ selectSku={selectSku}
246
+ {...selectorProps}
247
+ scrollList={scroll}
248
+ showCategory={selectorProps.showCategory ?
249
+ selectorProps.showCategory
250
+ :
251
+ (allVariants && !inst.current?.requestedCat)
252
+ }
155
253
  />
156
254
  )}
157
255
  {(cmmc.currentItem) && (
@@ -159,7 +257,7 @@ const BuyCard: React.FC<{
159
257
  size='default'
160
258
  item={cmmc.currentItem}
161
259
  onQuantityChanged={onQuantityChanged}
162
- className={cn('min-w-[160px] mx-auto mt-4', addWidgetClx)}
260
+ className={cn('min-w-[160px] mx-auto mt-4', (scroll ? 'shrink-0' : ''), addWidgetClx)}
163
261
  />
164
262
  )}
165
263
  </div >
@@ -12,9 +12,12 @@ import {
12
12
 
13
13
  import { cn } from '@hanzo/ui/util'
14
14
 
15
+ import { getUISpecFromPath } from '../../util'
16
+
15
17
  import BuyCard from './buy-card'
16
18
  import RadioItemSelector from '../select/radio-selector'
17
19
  import ImageItemSelector from '../select/image-selector'
20
+ import CarouselItemSelector from '../select/carousel-selector'
18
21
 
19
22
  const BuyDrawer: React.FC<{
20
23
  skuPath: string
@@ -40,6 +43,14 @@ const BuyDrawer: React.FC<{
40
43
  //}
41
44
  }
42
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
+
43
54
  return (
44
55
  <Drawer open={open} onOpenChange={setOpen} >
45
56
  <DrawerTrigger asChild className={triggerClx}>
@@ -51,15 +62,15 @@ const BuyDrawer: React.FC<{
51
62
  >
52
63
  <BuyCard
53
64
  skuPath={skuPath}
65
+ scrollAfter={spec.selector === 'carousel' ? 999 : undefined}
54
66
  mobile={mobile}
55
67
  onQuantityChanged={onQuantityChanged}
56
- className={cn('w-full', cardClx)}
57
- selector={ImageItemSelector}
58
- selClx=''
59
- selItemClx=''
60
- selSoleItemClx='mb-3'
61
- addWidgetClx=''
62
- facetsAs='label'
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}
63
74
  />
64
75
  <Button
65
76
  variant='ghost'
@@ -1,16 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import React from 'react'
4
- import Image from 'next/image'
5
4
  import { observer } from 'mobx-react-lite'
6
5
 
7
6
  import { cn } from '@hanzo/ui/util'
7
+ import { Image } from '@hanzo/ui/primitives'
8
8
 
9
9
  import type { LineItem } from '../../../types'
10
10
  import { formatCurrencyValue } from '../../../util'
11
11
  import AddToCartWidget from '../../buy/add-to-cart-widget'
12
12
  import { useCommerce } from '../../../service/context'
13
- import type { Dimensions } from '@hanzo/ui/types'
14
13
 
15
14
  const DEF_IMG_SIZE=40
16
15
 
@@ -27,47 +26,38 @@ const CartLineItem: React.FC<{
27
26
  className?: string
28
27
  imgSizePx?: number
29
28
  showPromoCode?: boolean
29
+ itemClicked?: (item: LineItem) => void
30
+ selected?: boolean
30
31
  }> = observer(({
31
32
  item,
32
33
  className='',
33
34
  imgSizePx=DEF_IMG_SIZE,
34
- showPromoCode
35
+ showPromoCode=false,
36
+ itemClicked,
37
+ selected=false
35
38
  }) => {
36
39
 
37
40
  const cmmc = useCommerce()
38
41
  const promoPrice = showPromoCode ? cmmc.itemPromoPrice(item) : undefined
39
-
40
- let dim: Dimensions
41
- if (item.imgAR) {
42
- if (item.imgAR >= 1) {
43
- dim = {
44
- w: imgSizePx,
45
- h: imgSizePx / item.imgAR
46
- }
47
- }
48
- else {
49
- dim = {
50
- w: imgSizePx * item.imgAR,
51
- h: imgSizePx
52
- }
53
- }
54
- }
55
- else {
56
- dim = {
57
- w: imgSizePx,
58
- h: imgSizePx
59
- }
60
- }
61
42
 
62
43
  return (
63
44
  <div className={cn('flex flex-col justify-start items-start text-sm font-sans', className)}>
64
- <div className='flex flex-row justify-between items-center gap-2'>
65
- {!!item.img ? (
66
- <Image src={item.img} alt={item.title + ' image'} height={dim.h} width={dim.w} className=''/>
67
- ) : ( // placeholder so things align
68
- <div style={{height: imgSizePx, width: imgSizePx}}/>
69
- )}
70
- <div className='grow leading-tight'>{renderTitle(item.title)}</div>
45
+ <div
46
+ className={
47
+ 'flex flex-row justify-between items-center gap-1 ' +
48
+ (itemClicked && !selected ? 'cursor-pointer ' : 'cursor-default ')
49
+ }
50
+ onClick={() => {itemClicked && itemClicked(item)}}
51
+ >
52
+ {/* 1px gap between image and border for better emphasis w small images */}
53
+ <div className={selected ? 'border-foreground p-[1px] border rounded-sm' : 'p-[2px]'}>
54
+ {item.img ? (
55
+ <Image def={item.img} constrainTo={{w: imgSizePx, h: imgSizePx}} />
56
+ ) : ( // placeholder so things align
57
+ <div style={{height: imgSizePx, width: imgSizePx}} className='bg-level-3'/>
58
+ )}
59
+ </div>
60
+ <div className='grow leading-tight ml-1'>{renderTitle(item.title)}</div>
71
61
  </div>
72
62
  <div className='flex flex-row items-center justify-between w-full'>
73
63
  <div className='flex flex-row items-center'>
@@ -11,6 +11,7 @@ import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
11
11
 
12
12
  import CartLineItem from './cart-line-item'
13
13
  import PromoCode from './promo-code'
14
+ import type { LineItem } from '../../../types'
14
15
 
15
16
  const CartPanel: React.FC<PropsWithChildren & {
16
17
  /** fix size and scroll after 'scrollAfter items in teh cart. */
@@ -25,6 +26,7 @@ const CartPanel: React.FC<PropsWithChildren & {
25
26
  buttonClx?: string
26
27
  showPromoCode?: boolean
27
28
  showShipping?: boolean
29
+ selectItems?: boolean
28
30
  /** if not provided, 'checkout' button will be rendered */
29
31
  handleCheckout?: () => void
30
32
  }> = observer(({
@@ -41,6 +43,7 @@ const CartPanel: React.FC<PropsWithChildren & {
41
43
  buttonClx='',
42
44
  showPromoCode=false,
43
45
  showShipping=false,
46
+ selectItems=false,
44
47
  handleCheckout,
45
48
  }) => {
46
49
 
@@ -74,6 +77,10 @@ const CartPanel: React.FC<PropsWithChildren & {
74
77
  handleCheckout && handleCheckout()
75
78
  }
76
79
 
80
+ const itemClicked = (item: LineItem) => {
81
+ cmmc.setCurrentItem(item.sku)
82
+ }
83
+
77
84
  const Main: React.FC = observer(() => (<>
78
85
  {cmmc.cartEmpty ? (
79
86
  <p className={cn('text-center mt-4', noItemsClx)}>No items in cart</p>
@@ -85,6 +92,8 @@ const CartPanel: React.FC<PropsWithChildren & {
85
92
  item={item}
86
93
  className={cn('mb-2', itemClx)}
87
94
  showPromoCode={showPromoCode}
95
+ itemClicked={selectItems ? itemClicked : undefined}
96
+ selected={selectItems ? cmmc.currentItem?.sku === item.sku : false}
88
97
  />
89
98
  ))}
90
99
  </>)}
@@ -7,7 +7,7 @@ export { default as BuyDrawer } from './buy/buy-drawer'
7
7
  export { default as CartAccordian } from './cart/cart-accordian'
8
8
  export { default as CartPanel } from './cart/cart-panel'
9
9
 
10
- export { default as FacetValuesWidget } from './facet-values-widget'
10
+ export { default as LevelNodesWidget } from './level-nodes-widget'
11
11
 
12
12
  export { default as PaymentStepForm } from './checkout/payment-step-form'
13
13
  export { default as ShippingStepForm } from './checkout/shipping-step-form'
@@ -2,29 +2,26 @@ import React from 'react'
2
2
 
3
3
  import Spline from '@splinetool/react-spline'
4
4
 
5
- import type { Dimensions } from '@hanzo/ui/types'
5
+ import { Image } from '@hanzo/ui/primitives'
6
6
  import { cn } from '@hanzo/ui/util'
7
7
 
8
8
  import {
9
9
  VideoBlockComponent,
10
- ImageBlockComponent,
11
- type ImageBlock,
12
10
  type Block,
13
11
  type VideoBlock
14
12
  } from '@hanzo/ui/blocks'
15
- import type { LineItem } from '../../types'
13
+ import type { Product } from '../../types'
16
14
 
17
15
  const ItemMedia: React.FC<{
18
- item: LineItem
19
- constrainTo?: Dimensions
16
+ item: Product
17
+ constrainTo?: {w: number, h: number}
20
18
  clx?: string
21
19
  }> = ({
22
- item,
20
+ item: {img, video, animation},
23
21
  constrainTo: cnst = {w: 250, h: 250},
24
22
  clx=''
25
23
  }) => {
26
24
 
27
- const {title, img, video, animation} = item
28
25
  // Order of precedence: 3D > MP4 > Image
29
26
  return animation ? (
30
27
  <Spline
@@ -38,21 +35,17 @@ const ItemMedia: React.FC<{
38
35
  />
39
36
  ) : video ? (
40
37
  <VideoBlockComponent
41
- constraint={cnst}
38
+ constrainTo={cnst}
42
39
  block={{blockType: 'video', ...video} satisfies VideoBlock as Block}
43
40
  className={clx}
44
41
  />
45
- ) : (
46
- <ImageBlockComponent
47
- block={{
48
- blockType: 'image',
49
- src: img ?? '',
50
- alt: title + ' image',
51
- dim: cnst
52
- } satisfies ImageBlock as Block}
42
+ ) : (img &&
43
+ <Image
44
+ def={img}
45
+ constrainTo={cnst}
53
46
  className={cn('m-auto', clx)}
54
47
  />
55
- )
48
+ ) // TODO: return placeholder
56
49
  }
57
50
 
58
51
  export default ItemMedia