@hanzo/commerce 6.0.0 → 6.1.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.
- package/components/buy/_to_deprecate_select-category-item-card.tsx +2 -1
- package/components/buy/_to_deprecate_select-category-item-panel.tsx +4 -2
- package/components/buy/add-to-cart-widget.tsx +5 -3
- package/components/buy/buy-card.tsx +184 -86
- package/components/buy/buy-drawer.tsx +18 -7
- package/components/cart/cart-panel/cart-line-item.tsx +22 -32
- package/components/cart/cart-panel/index.tsx +9 -0
- package/components/index.ts +1 -1
- package/components/item/item-media.tsx +11 -18
- package/components/item/product-card.tsx +20 -49
- package/components/{facet-values-widget → level-nodes-widget}/index.tsx +13 -13
- package/components/level-nodes-widget/node-image.tsx +31 -0
- package/components/select/carousel-selector.tsx +71 -23
- package/components/select/image-selector.tsx +93 -56
- package/components/select/radio-selector.tsx +86 -27
- package/package.json +2 -2
- package/service/impls/standalone/actual-line-item.ts +30 -16
- package/service/impls/standalone/standalone-service.ts +21 -3
- package/types/buy-ui-spec.ts +8 -0
- package/types/category.ts +6 -4
- package/types/commerce-service.ts +1 -0
- package/types/index.ts +1 -0
- package/types/item-selector.ts +25 -4
- package/types/line-item.ts +2 -0
- package/types/product.ts +5 -6
- package/util/buy-ui-conf.ts +34 -0
- package/util/index.ts +5 -2
- package/util/obs-string-mutator.ts +22 -0
- package/components/facet-values-widget/facet-image.tsx +0 -41
|
@@ -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.
|
|
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.
|
|
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-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
61
|
-
if (
|
|
62
|
-
throw new Error('BuyCard.useEffect(): must specify at least
|
|
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
|
|
65
|
-
for (let
|
|
66
|
-
|
|
98
|
+
const paths: SelectedPaths = {}
|
|
99
|
+
for (let l = 1; l <= level; l++ ) {
|
|
100
|
+
paths[l] = [toks[l]]
|
|
67
101
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
|
|
196
|
+
const scroll = !!(itemsToShow && itemsToShow.length > scrollAfter)
|
|
114
197
|
return (
|
|
115
|
-
<div className={cn(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
<
|
|
210
|
+
<LevelNodesWidget
|
|
124
211
|
className={cn(
|
|
125
|
-
'grid gap-0 ' + `grid-cols-${
|
|
212
|
+
'grid gap-0 align-stretch justify-normal ' + `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
|
-
|
|
214
|
+
(scroll ? 'shrink-0' : ''),
|
|
215
|
+
catWidgetClx
|
|
128
216
|
)}
|
|
129
217
|
mobile={mobile}
|
|
130
|
-
mutator={
|
|
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
|
-
|
|
137
|
-
show={
|
|
231
|
+
levelNodes={inst.current.requestedNode.subNodes!}
|
|
232
|
+
show={categoryTabAs}
|
|
138
233
|
/>
|
|
139
234
|
</>)}
|
|
140
|
-
{!
|
|
235
|
+
{!inst.current?.requestedNode && catTitle && (
|
|
141
236
|
<TitleArea title={catTitle} clx=''/>
|
|
142
237
|
)}
|
|
143
|
-
{(cmmc.currentItem) && (
|
|
144
|
-
|
|
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={
|
|
243
|
+
items={itemsToShow}
|
|
147
244
|
selectedItemRef={cmmc}
|
|
148
|
-
selectSku={
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
57
|
-
selector={
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
</>)}
|
package/components/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
13
|
+
import type { Product } from '../../types'
|
|
16
14
|
|
|
17
15
|
const ItemMedia: React.FC<{
|
|
18
|
-
item:
|
|
19
|
-
constrainTo?:
|
|
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
|
-
|
|
38
|
+
constrainTo={cnst}
|
|
42
39
|
block={{blockType: 'video', ...video} satisfies VideoBlock as Block}
|
|
43
40
|
className={clx}
|
|
44
41
|
/>
|
|
45
|
-
) : (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
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
|