@hanzo/commerce 6.1.13 → 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.
- package/components/buy/_to_deprecate_select-family-item-card.tsx +3 -4
- package/components/buy/_to_deprecate_select-family-item-panel.tsx +4 -5
- package/components/buy/add-to-cart-widget.tsx +43 -32
- package/components/buy/buy-button.tsx +1 -1
- package/components/buy/buy-card.tsx +17 -26
- package/components/buy/buy-drawer.tsx +5 -24
- package/components/buy/carousel-buy-card.tsx +145 -194
- package/components/buy/foo.txt +27 -0
- package/components/cart/cart-panel/cart-line-item.tsx +1 -1
- package/components/checkout/payment-step-form/contact-form.tsx +2 -2
- package/components/checkout/payment-step-form/methods/crypto.tsx +1 -2
- package/components/checkout/shipping-step-form.tsx +5 -5
- package/components/index.ts +1 -3
- package/components/item-selector/button.tsx +175 -0
- package/components/item-selector/carousel.tsx +162 -0
- package/components/item-selector/index.ts +5 -0
- package/components/quantity-indicator.tsx +48 -0
- package/components/select-family/family-carousel/index.tsx +47 -38
- package/components/select-family/family-carousel/slide.tsx +83 -0
- package/components/select-family/family-carousel/state.ts +83 -0
- package/components/select-family/level-nodes-widget/index.tsx +2 -2
- package/components/select-family/level-nodes-widget/node-image.tsx +2 -2
- package/index.ts +7 -7
- package/package.json +7 -4
- package/service/context.tsx +8 -7
- package/service/debug.ts +41 -0
- package/service/get-instance.ts +3 -0
- package/service/impls/standalone/actual-line-item.ts +5 -7
- package/service/impls/standalone/index.ts +11 -3
- package/service/impls/standalone/standalone-service.ts +133 -38
- package/service/path-utils.ts +26 -0
- package/service/sep.ts +7 -0
- package/types/category-node.ts +38 -0
- package/types/commerce-service.ts +44 -12
- package/types/family.ts +4 -1
- package/types/index.ts +3 -2
- package/types/item-selector.ts +29 -14
- package/types/product.ts +5 -1
- package/types/selection-ui-specifier.ts +36 -0
- package/types/token-separators.ts +7 -0
- package/util/error.ts +34 -0
- package/util/index.ts +4 -1
- package/util/product-media-accessor.ts +58 -0
- package/util/promo-codes.ts +1 -2
- package/util/selection-ui-specifiers.ts +34 -0
- package/util/use-sync-sku-param-w-current-item.ts +2 -2
- package/components/select-family/family-carousel/family-slide.tsx +0 -36
- package/components/select-product/carousel-selector.tsx +0 -112
- package/components/select-product/image-selector.tsx +0 -162
- package/components/select-product/radio-selector.tsx +0 -125
- package/service/impls/index.ts +0 -3
- package/types/buy-ui-spec.ts +0 -8
- package/types/tree-node.ts +0 -24
- package/util/buy-ui-conf.ts +0 -35
- package/util/sep.ts +0 -5
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import Image from 'next/image'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { CategoryNode } from '../../../types'
|
|
5
5
|
|
|
6
6
|
const ICON_SIZE = 20
|
|
7
7
|
|
|
8
8
|
const NodeImage: React.FC<{
|
|
9
|
-
treeNode:
|
|
9
|
+
treeNode: CategoryNode
|
|
10
10
|
}> = ({
|
|
11
11
|
treeNode: {
|
|
12
12
|
img,
|
package/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export * from './service/context'
|
|
2
2
|
export * from './components'
|
|
3
3
|
export type { StandaloneServiceOptions as ServiceOptions } from './service/impls/standalone/standalone-service'
|
|
4
|
-
export {
|
|
5
|
-
|
|
4
|
+
export {
|
|
5
|
+
useSyncSkuParamWithCurrentItem,
|
|
6
|
+
getFacetValuesMutator,
|
|
7
|
+
formatCurrencyValue,
|
|
8
|
+
ProductMediaAccessor
|
|
9
|
+
} from './util'
|
|
6
10
|
|
|
7
|
-
export
|
|
8
|
-
TOK: '-',
|
|
9
|
-
SUB_TOK: '_',
|
|
10
|
-
DECIMAL: '.'
|
|
11
|
-
}
|
|
11
|
+
export * from './util/selection-ui-specifiers'
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/commerce",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "e-commerce framework.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"publishConfig": {
|
|
6
7
|
"registry": "https://registry.npmjs.org/",
|
|
7
8
|
"access": "public",
|
|
@@ -28,7 +29,9 @@
|
|
|
28
29
|
},
|
|
29
30
|
"exports": {
|
|
30
31
|
".": "./index.ts",
|
|
31
|
-
"./
|
|
32
|
+
"./path-utils": "./service/path-utils.ts",
|
|
33
|
+
"./types": "./types/index.ts",
|
|
34
|
+
"./debug": "./service/debug.ts"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
34
37
|
"ethers": "^6.11.1",
|
|
@@ -38,11 +41,11 @@
|
|
|
38
41
|
},
|
|
39
42
|
"peerDependencies": {
|
|
40
43
|
"@hanzo/auth": "^2.3.4",
|
|
41
|
-
"@hanzo/ui": "^3.
|
|
44
|
+
"@hanzo/ui": "^3.5.0",
|
|
42
45
|
"@hookform/resolvers": "^3.3.4",
|
|
43
46
|
"@radix-ui/react-radio-group": "^1.1.3",
|
|
44
47
|
"firebase": "^10.8.0",
|
|
45
|
-
"lucide-react": "^0.
|
|
48
|
+
"lucide-react": "^0.344.0",
|
|
46
49
|
"mobx": "^6.12.0",
|
|
47
50
|
"mobx-react-lite": "^4.0.5",
|
|
48
51
|
"mobx-utils": "^6.0.8",
|
package/service/context.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import { createContext, useContext, useRef, type PropsWithChildren } from 'react'
|
|
2
|
+
import { createContext, useContext, useRef, type PropsWithChildren, useEffect } from 'react'
|
|
3
3
|
|
|
4
4
|
// https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
|
|
5
5
|
import { enableStaticRendering } from 'mobx-react-lite'
|
|
@@ -8,29 +8,30 @@ enableStaticRendering(typeof window === "undefined")
|
|
|
8
8
|
|
|
9
9
|
import type CommerceService from '../types/commerce-service'
|
|
10
10
|
import type { ServiceOptions } from '..'
|
|
11
|
-
import
|
|
12
|
-
import type { Family,
|
|
11
|
+
import getInstance from './get-instance'
|
|
12
|
+
import type { Family, CategoryNode, SelectionUISpecifier } from '../types'
|
|
13
13
|
|
|
14
14
|
const CommerceServiceContext = createContext<CommerceService | undefined>(undefined)
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
const useCommerce = (): CommerceService => {
|
|
18
17
|
return useContext(CommerceServiceContext) as CommerceService
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
const CommerceServiceProvider: React.FC<PropsWithChildren & {
|
|
22
21
|
productsByFamily: Family[]
|
|
23
|
-
rootNode:
|
|
22
|
+
rootNode: CategoryNode
|
|
24
23
|
options?: ServiceOptions
|
|
24
|
+
uiSpecs?: Record<string, SelectionUISpecifier>
|
|
25
25
|
}> = ({
|
|
26
26
|
children,
|
|
27
27
|
productsByFamily,
|
|
28
28
|
rootNode,
|
|
29
|
-
options
|
|
29
|
+
options,
|
|
30
|
+
uiSpecs
|
|
30
31
|
}) => {
|
|
31
32
|
|
|
32
33
|
// TODO: Inject Promo fixture here
|
|
33
|
-
const serviceRef = useRef<CommerceService>(
|
|
34
|
+
const serviceRef = useRef<CommerceService>(getInstance(productsByFamily, rootNode, options, uiSpecs))
|
|
34
35
|
return (
|
|
35
36
|
<CommerceServiceContext.Provider value={serviceRef.current}>
|
|
36
37
|
{children}
|
package/service/debug.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CategoryNode, CategoryNodeRole, Family, LineItem } from '../types'
|
|
2
|
+
|
|
3
|
+
// Note recursion
|
|
4
|
+
export const categoryNodeDump = (node: CategoryNode, level: number = 0): void => {
|
|
5
|
+
const spacers: string[] = []
|
|
6
|
+
for (let i = 0; i < level; i++) {
|
|
7
|
+
spacers.push('----')
|
|
8
|
+
}
|
|
9
|
+
console.log("NODE:" + spacers.join(''), node.skuToken)
|
|
10
|
+
node.subNodes?.forEach((sn: CategoryNode) => {categoryNodeDump(sn, level + 1)})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const selectedFamiliesDump = (result: Family[]): string => {
|
|
14
|
+
const toDisplay = result.map((c) => (c.id))
|
|
15
|
+
return JSON.stringify(toDisplay, null, 2)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const peekDump = (result : {
|
|
19
|
+
role: CategoryNodeRole
|
|
20
|
+
family: Family | undefined
|
|
21
|
+
families: Family[] | undefined
|
|
22
|
+
node: CategoryNode | undefined
|
|
23
|
+
item: LineItem | undefined
|
|
24
|
+
} | string): string => {
|
|
25
|
+
|
|
26
|
+
if (typeof result === 'string') {
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const toDisplay = {
|
|
31
|
+
role: result.role,
|
|
32
|
+
item: result.item ? result.item.sku : 'UNDEF',
|
|
33
|
+
family: result.family ? result.family.id : 'UNDEF',
|
|
34
|
+
families: result.families ? result.families.map((f) => (f.id)) : 'UNDEF',
|
|
35
|
+
node: result.node ?
|
|
36
|
+
(result.node.skuToken + (result.node.label ? (': ' + result.node.label) : ''))
|
|
37
|
+
:
|
|
38
|
+
'UNDEF'
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify(toDisplay, null, 2)
|
|
41
|
+
}
|
|
@@ -30,11 +30,13 @@ class ActualLineItem
|
|
|
30
30
|
optionLabel: string
|
|
31
31
|
familyTitle: string
|
|
32
32
|
familyId: string
|
|
33
|
+
byline?: string
|
|
33
34
|
desc?: string
|
|
34
35
|
price: number
|
|
35
36
|
img?: ImageDef
|
|
36
37
|
video?: VideoDef
|
|
37
|
-
animation?: string
|
|
38
|
+
animation?: string
|
|
39
|
+
optionImg?: ImageDef
|
|
38
40
|
timeAdded: number = 0 // timeAdded of being added to cart
|
|
39
41
|
|
|
40
42
|
constructor(prod: Product, snap?: ActualLineItemSnapshot) {
|
|
@@ -44,11 +46,13 @@ class ActualLineItem
|
|
|
44
46
|
this.optionLabel = prod.optionLabel
|
|
45
47
|
this.familyTitle = prod.familyTitle
|
|
46
48
|
this.familyId = prod.familyId
|
|
49
|
+
this.byline = prod.byline
|
|
47
50
|
this.desc = prod.desc
|
|
48
51
|
this.price = prod.price
|
|
49
52
|
this.img = prod.img
|
|
50
53
|
this.video = prod.video
|
|
51
54
|
this.animation = prod.animation
|
|
55
|
+
this.optionImg = prod.optionImg
|
|
52
56
|
|
|
53
57
|
if (snap) {
|
|
54
58
|
this.qu = snap.quantity
|
|
@@ -106,12 +110,6 @@ class ActualLineItem
|
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
|
-
|
|
110
|
-
inFamily(id: string): boolean {
|
|
111
|
-
// TODO: will break for level one (which is ok for lux, but not generally)
|
|
112
|
-
return this.sku.includes(`-${id}-`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
export {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { enableStaticRendering } from 'mobx-react-lite'
|
|
2
2
|
|
|
3
|
-
import type { CommerceService, Family,
|
|
3
|
+
import type { CommerceService, Family, CategoryNode, SelectionUISpecifier } from '../../../types'
|
|
4
4
|
import StandaloneService, {type StandaloneServiceOptions} from './standalone-service'
|
|
5
|
+
import { initSelectionUI } from '../../../util'
|
|
5
6
|
|
|
6
7
|
import { readSnapshot, listenAndWriteSnapshots } from './localStorage'
|
|
7
8
|
|
|
@@ -19,8 +20,9 @@ let instance: StandaloneService | undefined = undefined
|
|
|
19
20
|
|
|
20
21
|
export const getInstance = (
|
|
21
22
|
families: Family[],
|
|
22
|
-
rootNode:
|
|
23
|
-
options?: StandaloneServiceOptions
|
|
23
|
+
rootNode: CategoryNode,
|
|
24
|
+
options?: StandaloneServiceOptions,
|
|
25
|
+
uiSpecs?: Record<string, SelectionUISpecifier>
|
|
24
26
|
): CommerceService => {
|
|
25
27
|
|
|
26
28
|
if (!options) {
|
|
@@ -28,6 +30,9 @@ export const getInstance = (
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
if (typeof window === "undefined") {
|
|
33
|
+
if (uiSpecs) {
|
|
34
|
+
initSelectionUI(uiSpecs)
|
|
35
|
+
}
|
|
31
36
|
//_log("NEW INSTANCE FOR SERVER")
|
|
32
37
|
return new StandaloneService(
|
|
33
38
|
families,
|
|
@@ -38,6 +43,9 @@ export const getInstance = (
|
|
|
38
43
|
|
|
39
44
|
// Client side, create the store only once in the client
|
|
40
45
|
if (!instance) {
|
|
46
|
+
if (uiSpecs) {
|
|
47
|
+
initSelectionUI(uiSpecs)
|
|
48
|
+
}
|
|
41
49
|
//_log("NEW INSTANCE FOR CLIENT")
|
|
42
50
|
const snapShot = readSnapshot()
|
|
43
51
|
instance = new StandaloneService(
|
|
@@ -14,7 +14,8 @@ import type {
|
|
|
14
14
|
Family,
|
|
15
15
|
LineItem,
|
|
16
16
|
SelectedPaths,
|
|
17
|
-
|
|
17
|
+
CategoryNode,
|
|
18
|
+
CategoryNodeRole,
|
|
18
19
|
Promo
|
|
19
20
|
} from '../../../types'
|
|
20
21
|
|
|
@@ -25,10 +26,11 @@ import {
|
|
|
25
26
|
} from './orders'
|
|
26
27
|
|
|
27
28
|
import ActualLineItem, { type ActualLineItemSnapshot } from './actual-line-item'
|
|
28
|
-
import
|
|
29
|
+
import { getParentPath } from '../../../service/path-utils'
|
|
30
|
+
import { getErrorMessage } from '../../../util'
|
|
31
|
+
import sep from '../../sep'
|
|
29
32
|
|
|
30
33
|
type StandaloneServiceOptions = {
|
|
31
|
-
levelZeroPrefix?: string
|
|
32
34
|
dbName: string
|
|
33
35
|
ordersTable: string
|
|
34
36
|
}
|
|
@@ -37,11 +39,12 @@ interface StandaloneServiceSnapshot {
|
|
|
37
39
|
items: ActualLineItemSnapshot[]
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
|
|
40
43
|
class StandaloneService
|
|
41
44
|
implements CommerceService
|
|
42
45
|
{
|
|
43
46
|
private _familyMap = new Map<string, Family>()
|
|
44
|
-
private _rootNode:
|
|
47
|
+
private _rootNode: CategoryNode
|
|
45
48
|
private _selectedPaths: SelectedPaths = {}
|
|
46
49
|
private _promo: Promo | null = null
|
|
47
50
|
|
|
@@ -50,7 +53,7 @@ class StandaloneService
|
|
|
50
53
|
|
|
51
54
|
constructor(
|
|
52
55
|
families: Family[],
|
|
53
|
-
rootNode:
|
|
56
|
+
rootNode: CategoryNode,
|
|
54
57
|
options: StandaloneServiceOptions,
|
|
55
58
|
serviceSnapshot?: StandaloneServiceSnapshot,
|
|
56
59
|
) {
|
|
@@ -58,8 +61,8 @@ class StandaloneService
|
|
|
58
61
|
this._rootNode = rootNode
|
|
59
62
|
this._options = options
|
|
60
63
|
|
|
61
|
-
families.forEach((
|
|
62
|
-
|
|
64
|
+
families.forEach((fam) => {
|
|
65
|
+
fam.products = fam.products.map((p) => {
|
|
63
66
|
if (serviceSnapshot) {
|
|
64
67
|
const itemSnapshot = serviceSnapshot.items.find((is) => (is.sku === p.sku))
|
|
65
68
|
if (itemSnapshot) {
|
|
@@ -68,7 +71,7 @@ class StandaloneService
|
|
|
68
71
|
}
|
|
69
72
|
return new ActualLineItem(p)
|
|
70
73
|
})
|
|
71
|
-
this._familyMap.set(
|
|
74
|
+
this._familyMap.set(fam.id, fam)
|
|
72
75
|
})
|
|
73
76
|
|
|
74
77
|
makeObservable<
|
|
@@ -105,44 +108,143 @@ class StandaloneService
|
|
|
105
108
|
return this._familyMap.get(id)
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
getNodeAtPath(skuPath: string):
|
|
109
|
-
const toks = skuPath.split(
|
|
111
|
+
getNodeAtPath(skuPath: string): CategoryNode | undefined {
|
|
112
|
+
const toks = skuPath.split(sep.tok)
|
|
110
113
|
let level = 1
|
|
111
|
-
let
|
|
114
|
+
let node: CategoryNode | undefined = this._rootNode
|
|
112
115
|
do {
|
|
113
|
-
|
|
116
|
+
node = node!.subNodes?.find((sn) => (sn.skuToken === toks[level]))
|
|
114
117
|
level++
|
|
115
118
|
}
|
|
116
|
-
while (
|
|
117
|
-
return level === toks.length ?
|
|
119
|
+
while (node && (level < toks.length))
|
|
120
|
+
return level === toks.length ? node : undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
peek(skuPath: string): {
|
|
124
|
+
role: CategoryNodeRole
|
|
125
|
+
family: Family | undefined
|
|
126
|
+
families: Family[] | undefined
|
|
127
|
+
node: CategoryNode | undefined
|
|
128
|
+
item: LineItem | undefined
|
|
129
|
+
} | string /* OR error string */ {
|
|
130
|
+
|
|
131
|
+
const toks = skuPath.split(sep.tok)
|
|
132
|
+
let level: number
|
|
133
|
+
let node: CategoryNode | undefined = this._rootNode
|
|
134
|
+
let parent: CategoryNode | undefined = undefined
|
|
135
|
+
|
|
136
|
+
for (level = 1; level < toks.length && node && node.subNodes; level++) {
|
|
137
|
+
// https://stackoverflow.com/questions/62367492/inference-problem-referenced-directly-or-indirectly-in-its-own-initializer
|
|
138
|
+
const _node: CategoryNode | undefined =
|
|
139
|
+
node!.subNodes.find((sn) => (sn.skuToken === toks[level]))
|
|
140
|
+
if (!_node) {
|
|
141
|
+
return `service.peekAtNode: traversing '${skuPath}'... no CategoryNode at '${toks[level]}'!`
|
|
142
|
+
}
|
|
143
|
+
parent = node
|
|
144
|
+
node = _node
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const atEnd = level === toks.length
|
|
148
|
+
const possibleSKU = level === toks.length - 1
|
|
149
|
+
|
|
150
|
+
let role: CategoryNodeRole = 'non-outermost'
|
|
151
|
+
let families: Family[] | undefined = undefined
|
|
152
|
+
let family: Family | undefined = undefined
|
|
153
|
+
let item: LineItem | undefined = undefined
|
|
154
|
+
let error: string | undefined = undefined
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
if (node.subNodes && atEnd && node.terminal) {
|
|
158
|
+
role = 'multi-family'
|
|
159
|
+
families = node.subNodes.map((sub) => {
|
|
160
|
+
const familyId = skuPath + sep.tok + sub.skuToken
|
|
161
|
+
const fam = this._familyMap.get(familyId)
|
|
162
|
+
if (!fam) {
|
|
163
|
+
throw new Error(`service.peekAtNode: No Family under for CategoryNode '${skuPath}' with id ${familyId}!`)
|
|
164
|
+
}
|
|
165
|
+
return fam
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
else if (!node.subNodes && (atEnd || possibleSKU)) {
|
|
169
|
+
const _skuPath = (possibleSKU) ? getParentPath(skuPath) : skuPath
|
|
170
|
+
if (parent?.terminal) {
|
|
171
|
+
role = 'family-in-multi-family'
|
|
172
|
+
const fam = this._familyMap.get(_skuPath)
|
|
173
|
+
if (!fam) {
|
|
174
|
+
throw new Error(`service.peekAtNode: '${_skuPath}' graphs as a Family under a multi-family node, but no such family exists!`)
|
|
175
|
+
}
|
|
176
|
+
family = fam
|
|
177
|
+
const parentPath = getParentPath(_skuPath)
|
|
178
|
+
// get all siblings (subnodes of parent)
|
|
179
|
+
families = parent.subNodes!.map((sn) => {
|
|
180
|
+
const familyId = parentPath + sep.tok + sn.skuToken
|
|
181
|
+
const fam = this._familyMap.get(familyId)
|
|
182
|
+
if (!fam) {
|
|
183
|
+
throw new Error(`service.peekAtNode: No sibling Family for '${_skuPath}' with id '${familyId}'!`)
|
|
184
|
+
}
|
|
185
|
+
return fam
|
|
186
|
+
})
|
|
187
|
+
node = parent
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
role = 'single-family'
|
|
191
|
+
const fam = this._familyMap.get(_skuPath)
|
|
192
|
+
if (!fam) {
|
|
193
|
+
throw new Error(`service.peekAtNode: '${_skuPath}' graphs as a single Family, but no such family exists!`)
|
|
194
|
+
}
|
|
195
|
+
family = fam
|
|
196
|
+
}
|
|
197
|
+
if (possibleSKU) {
|
|
198
|
+
const skuToTry = family.id + sep.tok + toks[toks.length - 1]
|
|
199
|
+
const _item = family.products.find((p) => (p.sku === skuToTry))
|
|
200
|
+
if (_item) {
|
|
201
|
+
item = _item as LineItem
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
throw new Error(`service.peekAtNode: '${skuPath}' graphs as LineItem in Family '${family.id}', but no such sku exists there!`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
error = getErrorMessage(e)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return error ?? {
|
|
214
|
+
role,
|
|
215
|
+
family,
|
|
216
|
+
families,
|
|
217
|
+
node,
|
|
218
|
+
item
|
|
219
|
+
}
|
|
118
220
|
}
|
|
119
221
|
|
|
120
|
-
getSelectedNodesAtLevel = computedFn((level: number):
|
|
222
|
+
getSelectedNodesAtLevel = computedFn((level: number): CategoryNode[] | undefined => {
|
|
121
223
|
|
|
122
224
|
let lvl = 1
|
|
123
|
-
let
|
|
225
|
+
let nodesAtLevel: CategoryNode[] | undefined = this._rootNode.subNodes
|
|
124
226
|
|
|
125
227
|
do {
|
|
126
|
-
let selectedAtLevel:
|
|
228
|
+
let selectedAtLevel: CategoryNode[] | undefined = undefined
|
|
127
229
|
// If not specified, assume all
|
|
128
230
|
if (lvl in this._selectedPaths) {
|
|
129
|
-
selectedAtLevel =
|
|
231
|
+
selectedAtLevel = nodesAtLevel!.filter((n) => (this._selectedPaths[lvl].includes(n.skuToken)))
|
|
130
232
|
}
|
|
131
233
|
else {
|
|
132
|
-
selectedAtLevel =
|
|
234
|
+
selectedAtLevel = nodesAtLevel
|
|
133
235
|
}
|
|
134
|
-
let allSubsOfSelected:
|
|
135
|
-
selectedAtLevel?.forEach((
|
|
136
|
-
if (
|
|
137
|
-
allSubsOfSelected = [...allSubsOfSelected, ...
|
|
236
|
+
let allSubsOfSelected: CategoryNode[] = []
|
|
237
|
+
selectedAtLevel?.forEach((n: CategoryNode) => {
|
|
238
|
+
if (n.subNodes) {
|
|
239
|
+
allSubsOfSelected = [...allSubsOfSelected, ...n.subNodes]
|
|
138
240
|
}
|
|
139
241
|
})
|
|
140
242
|
|
|
141
|
-
|
|
243
|
+
nodesAtLevel = allSubsOfSelected
|
|
142
244
|
lvl++
|
|
143
|
-
} while (
|
|
245
|
+
} while (nodesAtLevel.length > 0 && lvl <= level)
|
|
144
246
|
|
|
145
|
-
return (
|
|
247
|
+
return (nodesAtLevel.length > 0 && ((lvl - 1) === level)) ? nodesAtLevel : undefined
|
|
146
248
|
})
|
|
147
249
|
|
|
148
250
|
|
|
@@ -284,7 +386,7 @@ class StandaloneService
|
|
|
284
386
|
}
|
|
285
387
|
|
|
286
388
|
selectPath(skuPath: string): Family[] {
|
|
287
|
-
const toks = skuPath.split(
|
|
389
|
+
const toks = skuPath.split(sep.tok)
|
|
288
390
|
const highestLevel = toks.length - 1
|
|
289
391
|
const fsv: SelectedPaths = {}
|
|
290
392
|
for (let level = 1; level <= highestLevel; level++ ) {
|
|
@@ -308,7 +410,7 @@ class StandaloneService
|
|
|
308
410
|
}
|
|
309
411
|
|
|
310
412
|
return this._rootNode.subNodes!.reduce(
|
|
311
|
-
(acc: Family[], subFacet:
|
|
413
|
+
(acc: Family[], subFacet: CategoryNode) => (
|
|
312
414
|
// Pass the root token as a one member array
|
|
313
415
|
this._reduceNode([this._rootNode.skuToken], acc, subFacet)
|
|
314
416
|
),
|
|
@@ -316,7 +418,7 @@ class StandaloneService
|
|
|
316
418
|
)
|
|
317
419
|
}
|
|
318
420
|
|
|
319
|
-
private _reduceNode(parentPath: string[], acc: Family[], node:
|
|
421
|
+
private _reduceNode(parentPath: string[], acc: Family[], node: CategoryNode): Family[] {
|
|
320
422
|
const path = [...parentPath, node.skuToken] // Don't mutate original please :)
|
|
321
423
|
const level = path.length - 1
|
|
322
424
|
// If there is no token array supplied for this level,
|
|
@@ -336,9 +438,9 @@ class StandaloneService
|
|
|
336
438
|
, acc)
|
|
337
439
|
}
|
|
338
440
|
// Process leaf
|
|
339
|
-
const fam = this._familyMap.get(path.join(
|
|
441
|
+
const fam = this._familyMap.get(path.join(sep.tok))
|
|
340
442
|
if (!fam) {
|
|
341
|
-
throw new Error("selectedFamilies WTF?!" + path.join(
|
|
443
|
+
throw new Error("selectedFamilies WTF?!" + path.join(sep.tok))
|
|
342
444
|
}
|
|
343
445
|
acc.push(fam)
|
|
344
446
|
}
|
|
@@ -392,13 +494,6 @@ class StandaloneService
|
|
|
392
494
|
)
|
|
393
495
|
}
|
|
394
496
|
|
|
395
|
-
debug_getSelectedFamilies = () => {
|
|
396
|
-
const spec = this.selectedFamilies
|
|
397
|
-
console.log('NUM SEL FAMS: ', spec.length)
|
|
398
|
-
if (spec.length > 0) {
|
|
399
|
-
console.log('IDS: ', (spec.map((c) => (c.id))).join(', '))
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
497
|
}
|
|
403
498
|
|
|
404
499
|
export {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SelectedPaths } from '../types'
|
|
2
|
+
import sep from './sep'
|
|
3
|
+
|
|
4
|
+
export const getSelectedPaths = (
|
|
5
|
+
skuPath: string
|
|
6
|
+
): { paths: SelectedPaths; level: number } => {
|
|
7
|
+
|
|
8
|
+
const toks = skuPath.split(sep.tok)
|
|
9
|
+
const level = toks.length - 1
|
|
10
|
+
const paths: SelectedPaths = {}
|
|
11
|
+
for (let l = 1; l <= level; l++ ) {
|
|
12
|
+
paths[l] = [toks[l]]
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
paths,
|
|
16
|
+
level
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getParentPath = (skuPath: string): string => (
|
|
21
|
+
skuPath.split(sep.tok).slice(0, -1).join(sep.tok)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export const lastToken = (skuPath: string): string => (
|
|
25
|
+
skuPath.split(sep.tok).pop()!
|
|
26
|
+
)
|
package/service/sep.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
interface CategoryNode {
|
|
4
|
+
skuToken: string // a token in the sku
|
|
5
|
+
label: string;
|
|
6
|
+
/**
|
|
7
|
+
* If this node has subnodes, marks it as a "last user-visible" / "outermost" Category.
|
|
8
|
+
* Normally, a node without subnodes (one that has products and is a family), is by definition terminal.
|
|
9
|
+
* If it *has* subnodes, but its subNodes are to be treated as sibling families
|
|
10
|
+
* of a Category, rather rather sub Categories, this is specified by setting terminal=true.
|
|
11
|
+
* */
|
|
12
|
+
terminal?: boolean;
|
|
13
|
+
img? : string | ReactNode // icon is required
|
|
14
|
+
imgAR? : number // helps with svgs
|
|
15
|
+
|
|
16
|
+
/** Short desc of what subNodes refer to.
|
|
17
|
+
* For example, if this node's label is 'Lux Credit',
|
|
18
|
+
* it's subNodesLabel would be 'Level',
|
|
19
|
+
* refering to membership 'levels' ('black', 'elite', etc)
|
|
20
|
+
*/
|
|
21
|
+
subNodesLabel?: string
|
|
22
|
+
subNodes?: CategoryNode[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type CategoryNodeRole =
|
|
26
|
+
'single-family' |
|
|
27
|
+
'family-in-multi-family' |
|
|
28
|
+
'multi-family' |
|
|
29
|
+
'non-outermost'
|
|
30
|
+
|
|
31
|
+
// Which facets tokens are 'on' at each level
|
|
32
|
+
type SelectedPaths = Record<number, string[]>
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
CategoryNodeRole,
|
|
36
|
+
CategoryNode,
|
|
37
|
+
SelectedPaths
|
|
38
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { LineItem, ObsLineItemRef } from './line-item'
|
|
2
|
-
import type {
|
|
2
|
+
import type { CategoryNode, SelectedPaths, CategoryNodeRole } from './category-node'
|
|
3
3
|
import type Family from './family'
|
|
4
4
|
import type Promo from './promo'
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
interface CommerceService extends ObsLineItemRef {
|
|
7
8
|
|
|
8
9
|
/** Items in cart */
|
|
@@ -39,29 +40,60 @@ interface CommerceService extends ObsLineItemRef {
|
|
|
39
40
|
selectPaths(value: SelectedPaths): Family[]
|
|
40
41
|
selectPath(skuPath: string): Family[]
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* With role as ...
|
|
45
|
+
*
|
|
46
|
+
* 'single-family':
|
|
47
|
+
* item: item with skuPath as SKU, otherwise undefined
|
|
48
|
+
* node: node at path (or item's parent node),
|
|
49
|
+
* family: node at path as family (or item's parent node as family),
|
|
50
|
+
* families: undefined
|
|
51
|
+
*
|
|
52
|
+
* 'family-in-multi-family':
|
|
53
|
+
* item: item with skuPath as SKU, otherwise undefined
|
|
54
|
+
* node: parent node of this family (or parent node of item's family)
|
|
55
|
+
* family: this family (or item's family)
|
|
56
|
+
* families: this family (or item's family) and siblings
|
|
57
|
+
*
|
|
58
|
+
* 'multi-family':
|
|
59
|
+
* item: undefined
|
|
60
|
+
* node: node at path
|
|
61
|
+
* family: undefined
|
|
62
|
+
* families: subnodes / families of this node
|
|
63
|
+
*
|
|
64
|
+
* 'non-outermost':
|
|
65
|
+
* item: undefined
|
|
66
|
+
* node: node at path
|
|
67
|
+
* family: undefined
|
|
68
|
+
* families: undefined
|
|
69
|
+
*
|
|
70
|
+
*/
|
|
71
|
+
peek(skuPath: string): {
|
|
72
|
+
role: CategoryNodeRole
|
|
44
73
|
family: Family | undefined
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
74
|
+
families: Family[] | undefined
|
|
75
|
+
node: CategoryNode | undefined
|
|
76
|
+
item: LineItem | undefined
|
|
77
|
+
} | string // or error string
|
|
78
|
+
|
|
79
|
+
/** @deprecated
|
|
80
|
+
* Whether this path defines a Family, or if it has further levels
|
|
81
|
+
* */
|
|
82
|
+
getNodeAtPath(skuPath: string): CategoryNode | undefined
|
|
83
|
+
|
|
50
84
|
get selectedPaths(): SelectedPaths // returns a copy
|
|
51
85
|
|
|
52
86
|
get selectedItems(): LineItem[]
|
|
53
87
|
get selectedFamilies(): Family[]
|
|
54
88
|
get hasSelection(): boolean
|
|
55
89
|
|
|
56
|
-
/** Whether this path defines a Family, or if it has further levels */
|
|
57
|
-
getNodeAtPath(skuPath: string): ProductTreeNode | undefined
|
|
58
90
|
|
|
59
91
|
/**
|
|
60
92
|
* Who are the subnodes selected at 'level'?
|
|
61
93
|
* If more than one value is selected at 'level',
|
|
62
|
-
* the returned
|
|
94
|
+
* the returned CategoryNode[] may represent multiple sets.
|
|
63
95
|
* */
|
|
64
|
-
getSelectedNodesAtLevel(level: number):
|
|
96
|
+
getSelectedNodesAtLevel(level: number): CategoryNode[] | undefined
|
|
65
97
|
|
|
66
98
|
/**
|
|
67
99
|
* For convenience, so widgets can share state.
|