@hanzo/commerce 5.1.2 → 6.0.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.
- package/components/{buy-item/select-category-item-card.tsx → buy/_to_deprecate_select-category-item-card.tsx} +20 -31
- package/components/{select-category-item-panel.tsx → buy/_to_deprecate_select-category-item-panel.tsx} +12 -20
- package/components/{buy-item/buy-item-popup.tsx → buy/_unused_buy-popup.tsx_} +4 -4
- package/components/{add-to-cart-widget.tsx → buy/add-to-cart-widget.tsx} +5 -3
- package/components/buy/buy-button.tsx +31 -0
- package/components/buy/buy-card.tsx +169 -0
- package/components/buy/buy-drawer.tsx +78 -0
- package/components/buy/buy-trigger-wrapper.tsx +20 -0
- package/components/{cart-accordian.tsx → cart/cart-accordian.tsx} +2 -1
- package/components/{cart-panel → cart/cart-panel}/cart-line-item.tsx +28 -5
- package/components/{cart-panel → cart/cart-panel}/index.tsx +9 -5
- package/components/{cart-panel → cart/cart-panel}/promo-code.tsx +3 -3
- package/components/{payment-step-form → checkout/payment-step-form}/contact-form.tsx +1 -1
- package/components/{payment-step-form → checkout/payment-step-form}/index.tsx +3 -3
- package/components/{payment-step-form → checkout/payment-step-form}/methods/bank-transfer.tsx +1 -1
- package/components/{payment-step-form → checkout/payment-step-form}/methods/card.tsx +4 -4
- package/components/{payment-step-form → checkout/payment-step-form}/methods/crypto.tsx +3 -3
- package/components/{payment-step-form → checkout/payment-step-form}/methods/index.ts +1 -1
- package/components/{shipping-step-form.tsx → checkout/shipping-step-form.tsx} +4 -4
- package/components/facet-values-widget/facet-image.tsx +2 -2
- package/components/facet-values-widget/index.tsx +13 -12
- package/components/index.ts +19 -17
- package/components/item/item-media.tsx +58 -0
- package/components/{product-card.tsx → item/product-card.tsx} +5 -7
- package/components/select/carousel-selector.tsx +64 -0
- package/components/select/image-selector.tsx +125 -0
- package/components/select/radio-selector.tsx +66 -0
- package/package.json +2 -1
- package/service/context.tsx +4 -4
- package/service/impls/standalone/index.ts +4 -4
- package/service/impls/standalone/standalone-service.ts +74 -62
- package/types/commerce-service.ts +20 -16
- package/types/index.ts +2 -2
- package/types/item-selector.ts +12 -5
- package/types/product.ts +1 -0
- package/types/tree-node.ts +24 -0
- package/util/index.ts +9 -9
- package/util/use-sync-sku-param-w-current-item.ts +5 -5
- package/components/buy-item/buy-item-button-wrapper.tsx +0 -27
- package/components/buy-item/buy-item-button.tsx +0 -34
- package/components/buy-item/buy-item-card.tsx +0 -92
- package/components/buy-item/buy-item-mobile-drawer.tsx +0 -54
- package/components/buy-item/select-category-and-item-widget.tsx +0 -83
- package/components/category-item-radio-selector.tsx +0 -55
- package/components/category-item-scroll-selector.tsx +0 -40
- package/components/item-carousel.tsx +0 -80
- package/types/facet.ts +0 -35
- /package/components/{cart-icon.tsx → cart/cart-icon.tsx} +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icon-row.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/amex.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/diners-club.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/discover.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/jcb.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/mastercard.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/visa.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/cc-button.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/btc.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/eth.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/usdt.tsx +0 -0
|
@@ -13,8 +13,8 @@ import type {
|
|
|
13
13
|
CommerceService,
|
|
14
14
|
Category,
|
|
15
15
|
LineItem,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
SelectedPaths,
|
|
17
|
+
ProductTreeNode,
|
|
18
18
|
Promo
|
|
19
19
|
} from '../../../types'
|
|
20
20
|
|
|
@@ -42,8 +42,8 @@ class StandaloneService
|
|
|
42
42
|
implements CommerceService
|
|
43
43
|
{
|
|
44
44
|
private _categoryMap = new Map<string, Category>()
|
|
45
|
-
private
|
|
46
|
-
private
|
|
45
|
+
private _rootNode: ProductTreeNode
|
|
46
|
+
private _selectedPaths: SelectedPaths = {}
|
|
47
47
|
private _promo: Promo | null = null
|
|
48
48
|
|
|
49
49
|
private _options : StandaloneServiceOptions
|
|
@@ -51,12 +51,12 @@ class StandaloneService
|
|
|
51
51
|
|
|
52
52
|
constructor(
|
|
53
53
|
categories: Category[],
|
|
54
|
-
|
|
54
|
+
rootNode: ProductTreeNode,
|
|
55
55
|
options: StandaloneServiceOptions,
|
|
56
56
|
serviceSnapshot?: StandaloneServiceSnapshot,
|
|
57
57
|
) {
|
|
58
58
|
|
|
59
|
-
this.
|
|
59
|
+
this._rootNode = rootNode
|
|
60
60
|
this._options = options
|
|
61
61
|
|
|
62
62
|
categories.forEach((c) => {
|
|
@@ -74,11 +74,11 @@ class StandaloneService
|
|
|
74
74
|
|
|
75
75
|
makeObservable<
|
|
76
76
|
StandaloneService,
|
|
77
|
-
'
|
|
77
|
+
'_selectedPaths' |
|
|
78
78
|
'_currentItem' |
|
|
79
79
|
'_promo'
|
|
80
80
|
>(this, {
|
|
81
|
-
|
|
81
|
+
_selectedPaths : observable.deep,
|
|
82
82
|
_currentItem: observable,
|
|
83
83
|
_promo: observable,
|
|
84
84
|
})
|
|
@@ -89,54 +89,52 @@ class StandaloneService
|
|
|
89
89
|
cartTotal: computed,
|
|
90
90
|
promoAppliedCartTotal: computed,
|
|
91
91
|
cartEmpty: computed,
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
selectedItems: computed,
|
|
93
|
+
selectedCategories: computed,
|
|
94
94
|
setCurrentItem: action,
|
|
95
95
|
currentItem: computed,
|
|
96
96
|
item: computed,
|
|
97
|
-
|
|
97
|
+
selectedPaths: computed,
|
|
98
98
|
appliedPromo: computed,
|
|
99
99
|
setAppliedPromo: action,
|
|
100
|
-
/* NOT
|
|
100
|
+
/* NOT selectPaths. It implements it's action mechanism */
|
|
101
101
|
})
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
105
104
|
getCategory(id: string): Category | undefined {
|
|
106
105
|
return this._categoryMap.get(id)
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
|
|
108
|
+
getNodeAtPath(skuPath: string): ProductTreeNode | undefined {
|
|
110
109
|
const toks = skuPath.split(SEP)
|
|
111
110
|
let level = 1
|
|
112
|
-
let
|
|
111
|
+
let desc: ProductTreeNode | undefined = this._rootNode
|
|
113
112
|
do {
|
|
114
|
-
|
|
115
|
-
valuesAtLevel = fvalue ? fvalue.sub : undefined
|
|
113
|
+
desc = desc!.subNodes?.find((vf) => (vf.skuToken === toks[level]))
|
|
116
114
|
level++
|
|
117
115
|
}
|
|
118
|
-
while (
|
|
119
|
-
return level === toks.length ?
|
|
116
|
+
while (desc && (level < toks.length))
|
|
117
|
+
return level === toks.length ? desc : undefined
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
|
|
120
|
+
getSelectedNodesAtLevel = computedFn((level: number): ProductTreeNode[] | undefined => {
|
|
123
121
|
|
|
124
122
|
let lvl = 1
|
|
125
|
-
let valuesAtLevel:
|
|
123
|
+
let valuesAtLevel: ProductTreeNode[] | undefined = this._rootNode.subNodes
|
|
126
124
|
|
|
127
125
|
do {
|
|
128
|
-
let selectedAtLevel:
|
|
126
|
+
let selectedAtLevel: ProductTreeNode[] | undefined = undefined
|
|
129
127
|
// If not specified, assume all
|
|
130
|
-
if (lvl in this.
|
|
131
|
-
selectedAtLevel = valuesAtLevel!.filter((fv) => (this.
|
|
128
|
+
if (lvl in this._selectedPaths) {
|
|
129
|
+
selectedAtLevel = valuesAtLevel!.filter((fv) => (this._selectedPaths[lvl].includes(fv.skuToken)))
|
|
132
130
|
}
|
|
133
131
|
else {
|
|
134
132
|
selectedAtLevel = valuesAtLevel
|
|
135
133
|
}
|
|
136
|
-
let allSubsOfSelected:
|
|
137
|
-
selectedAtLevel?.forEach((fvd:
|
|
138
|
-
if (fvd.
|
|
139
|
-
allSubsOfSelected = [...allSubsOfSelected, ...fvd.
|
|
134
|
+
let allSubsOfSelected: ProductTreeNode[] = []
|
|
135
|
+
selectedAtLevel?.forEach((fvd: ProductTreeNode) => {
|
|
136
|
+
if (fvd.subNodes) {
|
|
137
|
+
allSubsOfSelected = [...allSubsOfSelected, ...fvd.subNodes]
|
|
140
138
|
}
|
|
141
139
|
})
|
|
142
140
|
|
|
@@ -199,11 +197,9 @@ class StandaloneService
|
|
|
199
197
|
if (!this._promo) {
|
|
200
198
|
return this.cartTotal
|
|
201
199
|
}
|
|
202
|
-
|
|
203
200
|
if (!this._promo.skus) {
|
|
204
201
|
return this._promoValue_unsafe(this.cartTotal)
|
|
205
202
|
}
|
|
206
|
-
|
|
207
203
|
let total = this.cartItems.reduce(
|
|
208
204
|
(total, item) => {
|
|
209
205
|
const itemPrice = this._promo!.skus!.includes(item.sku) ?
|
|
@@ -214,7 +210,6 @@ class StandaloneService
|
|
|
214
210
|
},
|
|
215
211
|
0
|
|
216
212
|
)
|
|
217
|
-
|
|
218
213
|
return total
|
|
219
214
|
}
|
|
220
215
|
|
|
@@ -250,8 +245,8 @@ class StandaloneService
|
|
|
250
245
|
this._currentItem = ((): ActualLineItem | undefined => {
|
|
251
246
|
|
|
252
247
|
const categoriesTried: string[] = []
|
|
253
|
-
if (this.
|
|
254
|
-
for (let category of this.
|
|
248
|
+
if (this.selectedCategories && this.selectedCategories.length > 0) {
|
|
249
|
+
for (let category of this.selectedCategories) {
|
|
255
250
|
categoriesTried.push(category.id)
|
|
256
251
|
const foundItem = category.products.find((p) => (p.sku === skuToFind))
|
|
257
252
|
if (foundItem) {
|
|
@@ -281,52 +276,61 @@ class StandaloneService
|
|
|
281
276
|
return this._currentItem
|
|
282
277
|
}
|
|
283
278
|
|
|
284
|
-
|
|
279
|
+
selectPaths(sel: SelectedPaths): Category[] {
|
|
285
280
|
runInAction (() => {
|
|
286
|
-
this.
|
|
281
|
+
this._selectedPaths = this._processAndValidate(sel)
|
|
287
282
|
})
|
|
288
|
-
return this.
|
|
283
|
+
return this.selectedCategories
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
selectPath(skuPath: string): Category[] {
|
|
287
|
+
const toks = skuPath.split(SEP)
|
|
288
|
+
const highestLevel = toks.length - 1
|
|
289
|
+
const fsv: SelectedPaths = {}
|
|
290
|
+
for (let level = 1; level <= highestLevel; level++ ) {
|
|
291
|
+
fsv[level] = [toks[level]]
|
|
292
|
+
}
|
|
293
|
+
return this.selectPaths(fsv)
|
|
289
294
|
}
|
|
290
295
|
|
|
291
|
-
get
|
|
292
|
-
const result:
|
|
293
|
-
for( let level in this.
|
|
294
|
-
result[level] = [...this.
|
|
296
|
+
get selectedPaths(): SelectedPaths {
|
|
297
|
+
const result: SelectedPaths = {}
|
|
298
|
+
for( let level in this._selectedPaths ) {
|
|
299
|
+
result[level] = [...this._selectedPaths[level]]
|
|
295
300
|
}
|
|
296
301
|
return result
|
|
297
302
|
}
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (Object.keys(toJS(this._selectedFacets)).length === 0) {
|
|
304
|
+
get selectedCategories(): Category[] {
|
|
305
|
+
if (Object.keys(toJS(this._selectedPaths)).length === 0) {
|
|
302
306
|
// FacetsDesc have never been set or unset, so cannot evaluate them
|
|
303
307
|
return []
|
|
304
308
|
}
|
|
305
309
|
|
|
306
|
-
return this.
|
|
307
|
-
(acc: Category[], subFacet:
|
|
310
|
+
return this._rootNode.subNodes!.reduce(
|
|
311
|
+
(acc: Category[], subFacet: ProductTreeNode) => (
|
|
308
312
|
// Pass the root token as a one member array
|
|
309
|
-
this._reduceNode([this.
|
|
313
|
+
this._reduceNode([this._rootNode.skuToken], acc, subFacet)
|
|
310
314
|
),
|
|
311
315
|
[]
|
|
312
316
|
)
|
|
313
317
|
}
|
|
314
318
|
|
|
315
|
-
private _reduceNode(parentPath: string[], acc: Category[], node:
|
|
316
|
-
const path = [...parentPath, node.
|
|
319
|
+
private _reduceNode(parentPath: string[], acc: Category[], node: ProductTreeNode): Category[] {
|
|
320
|
+
const path = [...parentPath, node.skuToken] // Don't mutate original please :)
|
|
317
321
|
const level = path.length - 1
|
|
318
322
|
// If there is no token array supplied for this level,
|
|
319
323
|
// assume all are specified. Otherwise, see if the
|
|
320
324
|
// current node is in the array
|
|
321
325
|
const specified = (
|
|
322
|
-
!this.
|
|
326
|
+
!this._selectedPaths[level]
|
|
323
327
|
||
|
|
324
|
-
this.
|
|
328
|
+
this._selectedPaths[level].includes(node.skuToken)
|
|
325
329
|
)
|
|
326
330
|
if (specified) {
|
|
327
331
|
// Process subnodes
|
|
328
|
-
if (node.
|
|
329
|
-
return node.
|
|
332
|
+
if (node.subNodes && node.subNodes.length > 0) {
|
|
333
|
+
return node.subNodes.reduce((acc, n) => (
|
|
330
334
|
this._reduceNode(path, acc, n)
|
|
331
335
|
)
|
|
332
336
|
, acc)
|
|
@@ -334,30 +338,30 @@ class StandaloneService
|
|
|
334
338
|
// Process leaf
|
|
335
339
|
const cat = this._categoryMap.get(path.join(SEP))
|
|
336
340
|
if (!cat) {
|
|
337
|
-
throw new Error("
|
|
341
|
+
throw new Error("selectedCategories WTF?!" + path.join(SEP))
|
|
338
342
|
}
|
|
339
343
|
acc.push(cat)
|
|
340
344
|
}
|
|
341
345
|
return acc
|
|
342
346
|
}
|
|
343
347
|
|
|
344
|
-
private _processAndValidate(partial:
|
|
348
|
+
private _processAndValidate(partial: SelectedPaths): SelectedPaths {
|
|
345
349
|
|
|
346
|
-
const result:
|
|
350
|
+
const result: SelectedPaths = {}
|
|
347
351
|
|
|
348
352
|
let level = 1
|
|
349
|
-
let currentSet = this.
|
|
353
|
+
let currentSet = this._rootNode.subNodes!
|
|
350
354
|
|
|
351
355
|
while (true) {
|
|
352
|
-
let possibleCurrent = currentSet.map((el) => (el.
|
|
356
|
+
let possibleCurrent = currentSet.map((el) => (el.skuToken))
|
|
353
357
|
const validTokens = !partial[level] ? undefined : partial[level].filter((tok) => possibleCurrent.includes(tok))
|
|
354
358
|
if (!validTokens) {
|
|
355
359
|
break
|
|
356
360
|
}
|
|
357
361
|
result[level] = validTokens
|
|
358
362
|
currentSet = validTokens.map((tok) => {
|
|
359
|
-
const fd = currentSet.find((node) => ( node.
|
|
360
|
-
return (fd && fd.
|
|
363
|
+
const fd = currentSet.find((node) => ( node.skuToken === tok ))
|
|
364
|
+
return (fd && fd.subNodes && fd.subNodes.length > 0) ? fd.subNodes : []
|
|
361
365
|
}).flat()
|
|
362
366
|
level++
|
|
363
367
|
}
|
|
@@ -365,13 +369,13 @@ class StandaloneService
|
|
|
365
369
|
return result
|
|
366
370
|
}
|
|
367
371
|
|
|
368
|
-
get
|
|
369
|
-
if (Object.keys(toJS(this.
|
|
372
|
+
get selectedItems(): LineItem[] {
|
|
373
|
+
if (Object.keys(toJS(this._selectedPaths)).length === 0) {
|
|
370
374
|
// FacetsDesc have never been set or unset, so cannot evaluate them
|
|
371
375
|
return []
|
|
372
376
|
}
|
|
373
377
|
|
|
374
|
-
return this.
|
|
378
|
+
return this.selectedCategories.reduce(
|
|
375
379
|
(allProducts, cat) => ([...allProducts, ...(cat.products as LineItem[])]), [] as LineItem[])
|
|
376
380
|
}
|
|
377
381
|
|
|
@@ -383,6 +387,14 @@ class StandaloneService
|
|
|
383
387
|
0
|
|
384
388
|
)
|
|
385
389
|
}
|
|
390
|
+
|
|
391
|
+
debug_getSelectedCategories = () => {
|
|
392
|
+
const spec = this.selectedCategories
|
|
393
|
+
console.log('NUM SPEC CATS: ', spec.length)
|
|
394
|
+
if (spec.length > 0) {
|
|
395
|
+
console.log('IDS: ', (spec.map((c) => (c.id))).join(', '))
|
|
396
|
+
}
|
|
397
|
+
}
|
|
386
398
|
}
|
|
387
399
|
|
|
388
400
|
export {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LineItem, ObsLineItemRef } from './line-item'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProductTreeNode, SelectedPaths } from './tree-node'
|
|
3
3
|
import type Category from './category'
|
|
4
4
|
import type Promo from './promo'
|
|
5
5
|
|
|
@@ -29,36 +29,40 @@ interface CommerceService extends ObsLineItemRef {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Sets the tokens at each level supplied.
|
|
32
|
-
* If a level is
|
|
33
|
-
* If a level is missing (undefined), everything at that level is
|
|
32
|
+
* If a level is selected as [], nothing will be selected.
|
|
33
|
+
* If a level is missing (undefined), everything at that level is selected
|
|
34
34
|
*
|
|
35
|
-
* This
|
|
35
|
+
* This selects one or more Category's, and all the LineItem's in them.
|
|
36
36
|
*
|
|
37
|
-
* An empty value object
|
|
37
|
+
* An empty value object selects all Category's and all LineItem's,
|
|
38
38
|
* */
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
get
|
|
39
|
+
selectPaths(value: SelectedPaths): Category[]
|
|
40
|
+
selectPath(skuPath: string): Category[]
|
|
41
|
+
|
|
42
|
+
get selectedPaths(): SelectedPaths // returns a copy
|
|
43
|
+
|
|
44
|
+
get selectedItems(): LineItem[]
|
|
45
|
+
get selectedCategories(): Category[]
|
|
43
46
|
|
|
44
47
|
/** Whether this path defines a Category, or if it has further levels */
|
|
45
|
-
|
|
48
|
+
getNodeAtPath(skuPath: string): ProductTreeNode | undefined
|
|
46
49
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
/**
|
|
51
|
+
* Who are the subnodes selected at 'level'?
|
|
52
|
+
* If more than one value is selected at 'level',
|
|
53
|
+
* the returned ProductTreeNode[] may represent multiple sets.
|
|
50
54
|
* */
|
|
51
|
-
|
|
55
|
+
getSelectedNodesAtLevel(level: number): ProductTreeNode[] | undefined
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* For convenience, so widgets can share state.
|
|
55
|
-
* "current" is unrelated to what is "
|
|
59
|
+
* "current" is unrelated to what is "selected",
|
|
56
60
|
* ie, facets' values
|
|
57
61
|
* */
|
|
58
62
|
setCurrentItem(sku: string | undefined): boolean // valid sku and was set.
|
|
59
63
|
/**
|
|
60
64
|
* For convenience, so widgets can share state.
|
|
61
|
-
* "current" is unrelated to what is "
|
|
65
|
+
* "current" is unrelated to what is "selected",
|
|
62
66
|
* ie, facets' values
|
|
63
67
|
*
|
|
64
68
|
* note: for ObsLineItemRef, there is also
|
package/types/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
export type { default as Category } from './category'
|
|
3
3
|
export type { default as CommerceService } from './commerce-service'
|
|
4
|
-
export type { default as ItemSelector } from './item-selector'
|
|
5
4
|
export type { default as Product } from './product'
|
|
6
5
|
export type { default as Promo } from './promo'
|
|
7
6
|
|
|
8
7
|
export * from './checkout'
|
|
8
|
+
export * from './item-selector'
|
|
9
9
|
export * from './line-item'
|
|
10
|
-
export * from './
|
|
10
|
+
export * from './tree-node'
|
|
11
11
|
export * from './string-mutator'
|
package/types/item-selector.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { ObsLineItemRef } from './line-item'
|
|
1
|
+
import type { LineItem, ObsLineItemRef } from './line-item'
|
|
3
2
|
|
|
4
3
|
interface ItemSelector {
|
|
5
|
-
|
|
4
|
+
items: LineItem[]
|
|
6
5
|
selectedItemRef: ObsLineItemRef
|
|
7
6
|
selectSku: (sku: string) => void
|
|
8
7
|
showPrice?: boolean // true by default
|
|
9
|
-
showQuantity?: boolean //
|
|
8
|
+
showQuantity?: boolean // false by default (not impl)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ItemSelectorProps extends ItemSelector {
|
|
12
|
+
clx?: string
|
|
13
|
+
soleItemClx?: string
|
|
14
|
+
itemClx?: string
|
|
15
|
+
ext?: any
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export {
|
|
13
|
-
type ItemSelector
|
|
19
|
+
type ItemSelector,
|
|
20
|
+
type ItemSelectorProps
|
|
14
21
|
}
|
package/types/product.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ProductTreeNode {
|
|
4
|
+
skuToken: string // a token in the sku
|
|
5
|
+
label: string
|
|
6
|
+
img? : string | ReactNode // icon is required
|
|
7
|
+
imgAR? : number // helps with svgs
|
|
8
|
+
|
|
9
|
+
/** Short desc of what subNodes refer to.
|
|
10
|
+
* For example, if this node's label is 'Lux Credit',
|
|
11
|
+
* it's subNodesLabel would be 'Level',
|
|
12
|
+
* refering to membership 'levels' ('black', 'elite', etc)
|
|
13
|
+
*/
|
|
14
|
+
subNodesLabel?: string
|
|
15
|
+
subNodes?: ProductTreeNode[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Which facets tokens are 'on' at each level
|
|
19
|
+
type SelectedPaths = Record<number, string[]>
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
ProductTreeNode,
|
|
23
|
+
SelectedPaths
|
|
24
|
+
}
|
package/util/index.ts
CHANGED
|
@@ -32,19 +32,19 @@ export function formatCurrencyValue(price: number): string {
|
|
|
32
32
|
export const getFacetValuesMutator = (level: number, cmmc: CommerceService): StringMutator => {
|
|
33
33
|
|
|
34
34
|
const setLevel = (value: string, level: number ): void => {
|
|
35
|
-
const facets = cmmc.
|
|
35
|
+
const facets = cmmc.selectedPaths
|
|
36
36
|
facets[level] = [value]
|
|
37
|
-
cmmc.
|
|
38
|
-
const subFacets = cmmc.
|
|
37
|
+
cmmc.selectPaths(facets)
|
|
38
|
+
const subFacets = cmmc.getSelectedNodesAtLevel(level)
|
|
39
39
|
if (subFacets) {
|
|
40
|
-
const facets = cmmc.
|
|
41
|
-
facets[level + 1] = [subFacets[0].
|
|
42
|
-
cmmc.
|
|
40
|
+
const facets = cmmc.selectedPaths
|
|
41
|
+
facets[level + 1] = [subFacets[0].skuToken]
|
|
42
|
+
cmmc.selectPaths(facets)
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const getLevelValueSafe = (level: number): string | null => {
|
|
47
|
-
const facets = cmmc.
|
|
47
|
+
const facets = cmmc.selectedPaths
|
|
48
48
|
if (!(level in facets) || facets[level].length === 0 ) {
|
|
49
49
|
return null
|
|
50
50
|
}
|
|
@@ -52,8 +52,8 @@ export const getFacetValuesMutator = (level: number, cmmc: CommerceService): Str
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return {
|
|
55
|
-
get: () => (getLevelValueSafe(level)),
|
|
56
|
-
set: (v: string) => {setLevel(v, level)}
|
|
55
|
+
get: (): string | null => (getLevelValueSafe(level)),
|
|
56
|
+
set: (v: string): void => {setLevel(v, level)}
|
|
57
57
|
} satisfies StringMutator
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from 'next-usequerystate'
|
|
10
10
|
|
|
11
11
|
import { useCommerce } from '../service/context'
|
|
12
|
-
import type {
|
|
12
|
+
import type { SelectedPaths } from '../types'
|
|
13
13
|
|
|
14
14
|
const PLEASE_SELECT_FACETS = 'Please select an option from each group.'
|
|
15
15
|
|
|
@@ -32,7 +32,7 @@ const useSyncSkuParamWithCurrentItem = (
|
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
|
|
34
34
|
return reaction(() => ({
|
|
35
|
-
specifiedCat: cmmc.
|
|
35
|
+
specifiedCat: cmmc.selectedCategories.length === 1 ? cmmc.selectedCategories[0] : undefined,
|
|
36
36
|
currentItem: cmmc.currentItem
|
|
37
37
|
}),
|
|
38
38
|
({specifiedCat, currentItem}) => {
|
|
@@ -55,14 +55,14 @@ const useSyncSkuParamWithCurrentItem = (
|
|
|
55
55
|
|
|
56
56
|
const setCurrentCategoryFromSku = (sku: string) => {
|
|
57
57
|
const toks: string[] = sku.split('-')
|
|
58
|
-
const fv:
|
|
59
|
-
// TODO: confirm that extra trailing nonsense tokens won't break
|
|
58
|
+
const fv: SelectedPaths = {}
|
|
59
|
+
// TODO: confirm that extra trailing nonsense tokens won't break selectPaths()
|
|
60
60
|
for (let i = 1; i < categoryLevel; i++) {
|
|
61
61
|
if (i in toks) {
|
|
62
62
|
fv[i] = [toks[i]]
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
cmmc.
|
|
65
|
+
cmmc.selectPaths(fv)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// setCI returns true if it's a recognized sku
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React, {type PropsWithChildren} from 'react'
|
|
3
|
-
|
|
4
|
-
import BuyItemPopup from './buy-item-popup'
|
|
5
|
-
import BuyItemMobileDrawer from './buy-item-mobile-drawer'
|
|
6
|
-
|
|
7
|
-
const BuyItemButtonWrapper: React.FC<{
|
|
8
|
-
skuPath: string
|
|
9
|
-
desktopTrigger: React.ReactNode
|
|
10
|
-
mobileTrigger: React.ReactNode
|
|
11
|
-
popupClx?: string
|
|
12
|
-
}> = ({
|
|
13
|
-
skuPath,
|
|
14
|
-
desktopTrigger,
|
|
15
|
-
mobileTrigger,
|
|
16
|
-
popupClx=''
|
|
17
|
-
}) => (<>
|
|
18
|
-
<BuyItemPopup skuPath={skuPath} popupClx={popupClx}>
|
|
19
|
-
{desktopTrigger}
|
|
20
|
-
</BuyItemPopup>
|
|
21
|
-
<BuyItemMobileDrawer skuPath={skuPath} trigger={mobileTrigger} />
|
|
22
|
-
</>
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export default BuyItemButtonWrapper
|
|
@@ -1,34 +0,0 @@
|
|
|
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 BuyItemPopup from './buy-item-popup'
|
|
7
|
-
import BuyItemMobileDrawer from './buy-item-mobile-drawer'
|
|
8
|
-
import { cn } from '@hanzo/ui/util'
|
|
9
|
-
|
|
10
|
-
const BuyItemButton: React.FC<PropsWithChildren & {
|
|
11
|
-
skuPath: string
|
|
12
|
-
variant? : ButtonVariants
|
|
13
|
-
size?: ButtonSizes
|
|
14
|
-
/* rounded?: ButtonRounded // wait for version bump*/
|
|
15
|
-
className?: string
|
|
16
|
-
popupClx?: string
|
|
17
|
-
}> = ({
|
|
18
|
-
skuPath,
|
|
19
|
-
variant,
|
|
20
|
-
size,
|
|
21
|
-
children,
|
|
22
|
-
className='',
|
|
23
|
-
popupClx=''
|
|
24
|
-
}) => (<>
|
|
25
|
-
<BuyItemPopup skuPath={skuPath} popupClx={popupClx}>
|
|
26
|
-
<Button size={size} variant={variant} className={cn(className, 'hidden md:flex')}>{children}</Button>
|
|
27
|
-
</BuyItemPopup>
|
|
28
|
-
<BuyItemMobileDrawer skuPath={skuPath} trigger={<Button size={size} variant={variant} className={cn(className, 'md:hidden')}>{children}</Button>} />
|
|
29
|
-
</>
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export default BuyItemButton
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React, { useRef, useEffect } from 'react'
|
|
3
|
-
import { autorun } from 'mobx'
|
|
4
|
-
import { observer } from 'mobx-react-lite'
|
|
5
|
-
|
|
6
|
-
import { cn } from '@hanzo/ui/util'
|
|
7
|
-
|
|
8
|
-
import type { FacetsValue } from '../../types'
|
|
9
|
-
import { useCommerce } from '../../service/context'
|
|
10
|
-
import { getFacetValuesMutator } from '../../util'
|
|
11
|
-
import FacetValuesWidget from '../facet-values-widget'
|
|
12
|
-
import SelectCategoryItemCard from './select-category-item-card'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const BuyItemCard: React.FC<{
|
|
16
|
-
skuPath: string
|
|
17
|
-
mobile?: boolean
|
|
18
|
-
className?: string
|
|
19
|
-
onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
|
|
20
|
-
}> = observer(({
|
|
21
|
-
skuPath,
|
|
22
|
-
mobile=false,
|
|
23
|
-
className='',
|
|
24
|
-
onQuantityChanged
|
|
25
|
-
}) => {
|
|
26
|
-
|
|
27
|
-
const cmmc = useCommerce()
|
|
28
|
-
const levelRef = useRef<number>(-1)
|
|
29
|
-
|
|
30
|
-
const cat = cmmc.getCategory(skuPath)
|
|
31
|
-
const facets = cat ? undefined : cmmc.getFacetValuesAtSkuPath(skuPath)
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
|
|
35
|
-
const toks = skuPath.split('-')
|
|
36
|
-
levelRef.current = toks.length - 1
|
|
37
|
-
const fsv: FacetsValue = {}
|
|
38
|
-
for (let level = 1; level <= levelRef.current; level++ ) {
|
|
39
|
-
fsv[level] = [toks[level]]
|
|
40
|
-
}
|
|
41
|
-
if (facets) {
|
|
42
|
-
fsv[levelRef.current + 1] = [facets[0].value]
|
|
43
|
-
}
|
|
44
|
-
cmmc.setFacets(fsv)
|
|
45
|
-
|
|
46
|
-
return autorun(() => {
|
|
47
|
-
const cats = cmmc.specifiedCategories
|
|
48
|
-
// Original cat was legit
|
|
49
|
-
if (cat && (cats.length === 0 || cats[0].id !== cat.id)) {
|
|
50
|
-
if (!cmmc.currentItem || cmmc.currentItem.categoryId !== cat.id) {
|
|
51
|
-
cmmc.setCurrentItem(cat.products[0].sku)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else if (cats.length > 0) {
|
|
55
|
-
if (!cmmc.currentItem || cmmc.currentItem.categoryId !== cats[0].id) {
|
|
56
|
-
cmmc.setCurrentItem(cats[0].products[0].sku)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
}, [cat, facets])
|
|
61
|
-
|
|
62
|
-
const renderFacetTabs = facets && levelRef.current > 0
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div className={className} >
|
|
66
|
-
{renderFacetTabs && (
|
|
67
|
-
<FacetValuesWidget
|
|
68
|
-
className={cn('grid gap-0 ' + `grid-cols-${facets.length}` + ' self-start ', 'border-b-2 border-level-3 mb-2 -mr-2 -ml-2')}
|
|
69
|
-
isMobile={false}
|
|
70
|
-
mutator={getFacetValuesMutator(levelRef.current + 1, cmmc)}
|
|
71
|
-
itemClx='flex-col h-auto gap-0 pb-1 pt-3 px-3'
|
|
72
|
-
buttonClx={'h-full !rounded-bl-none !rounded-br-none !rounded-tl-lg !rounded-tr-lg ' +
|
|
73
|
-
'!border-r !border-t !border-level-3'}
|
|
74
|
-
facetValues={facets}
|
|
75
|
-
/>
|
|
76
|
-
)}
|
|
77
|
-
{cmmc.specifiedCategories[0] && (
|
|
78
|
-
<SelectCategoryItemCard
|
|
79
|
-
noTitle
|
|
80
|
-
mobile={mobile}
|
|
81
|
-
category={cmmc.specifiedCategories[0]}
|
|
82
|
-
selectedItemRef={cmmc /* ...conveniently. :) */ }
|
|
83
|
-
selectSku={cmmc.setCurrentItem.bind(cmmc)}
|
|
84
|
-
className=''
|
|
85
|
-
onQuantityChanged={onQuantityChanged}
|
|
86
|
-
/>
|
|
87
|
-
)}
|
|
88
|
-
</div >
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
export default BuyItemCard
|