@hanzo/commerce 6.1.13 → 6.2.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.
Files changed (55) hide show
  1. package/components/buy/_to_deprecate_select-family-item-card.tsx +3 -4
  2. package/components/buy/_to_deprecate_select-family-item-panel.tsx +4 -5
  3. package/components/buy/add-to-cart-widget.tsx +43 -32
  4. package/components/buy/buy-button.tsx +1 -1
  5. package/components/buy/buy-card.tsx +17 -26
  6. package/components/buy/buy-drawer.tsx +6 -25
  7. package/components/buy/carousel-buy-card.tsx +145 -194
  8. package/components/buy/foo.txt +27 -0
  9. package/components/cart/cart-panel/cart-line-item.tsx +1 -1
  10. package/components/checkout/payment-step-form/contact-form.tsx +2 -2
  11. package/components/checkout/payment-step-form/methods/crypto.tsx +1 -2
  12. package/components/checkout/shipping-step-form.tsx +5 -5
  13. package/components/index.ts +1 -3
  14. package/components/item-selector/button.tsx +175 -0
  15. package/components/item-selector/carousel.tsx +162 -0
  16. package/components/item-selector/index.ts +5 -0
  17. package/components/quantity-indicator.tsx +48 -0
  18. package/components/select-family/family-carousel/index.tsx +47 -38
  19. package/components/select-family/family-carousel/slide.tsx +83 -0
  20. package/components/select-family/family-carousel/state.ts +83 -0
  21. package/components/select-family/level-nodes-widget/index.tsx +2 -2
  22. package/components/select-family/level-nodes-widget/node-image.tsx +2 -2
  23. package/index.ts +7 -7
  24. package/package.json +7 -4
  25. package/service/context.tsx +8 -7
  26. package/service/debug.ts +41 -0
  27. package/service/get-instance.ts +3 -0
  28. package/service/impls/standalone/actual-line-item.ts +5 -7
  29. package/service/impls/standalone/index.ts +11 -3
  30. package/service/impls/standalone/standalone-service.ts +133 -38
  31. package/service/path-utils.ts +26 -0
  32. package/service/sep.ts +7 -0
  33. package/types/category-node.ts +38 -0
  34. package/types/commerce-service.ts +44 -12
  35. package/types/family.ts +4 -1
  36. package/types/index.ts +3 -2
  37. package/types/item-selector.ts +29 -14
  38. package/types/product.ts +5 -1
  39. package/types/selection-ui-specifier.ts +36 -0
  40. package/types/token-separators.ts +7 -0
  41. package/util/error.ts +34 -0
  42. package/util/index.ts +4 -1
  43. package/util/product-media-accessor.ts +58 -0
  44. package/util/promo-codes.ts +1 -2
  45. package/util/selection-ui-specifiers.ts +34 -0
  46. package/util/use-sync-sku-param-w-current-item.ts +2 -2
  47. package/components/select-family/family-carousel/family-slide.tsx +0 -36
  48. package/components/select-product/carousel-selector.tsx +0 -112
  49. package/components/select-product/image-selector.tsx +0 -162
  50. package/components/select-product/radio-selector.tsx +0 -125
  51. package/service/impls/index.ts +0 -3
  52. package/types/buy-ui-spec.ts +0 -8
  53. package/types/tree-node.ts +0 -24
  54. package/util/buy-ui-conf.ts +0 -35
  55. package/util/sep.ts +0 -5
@@ -1,12 +1,12 @@
1
1
  import React from 'react'
2
2
  import Image from 'next/image'
3
3
 
4
- import type { ProductTreeNode } from '../../../types'
4
+ import type { CategoryNode } from '../../../types'
5
5
 
6
6
  const ICON_SIZE = 20
7
7
 
8
8
  const NodeImage: React.FC<{
9
- treeNode: ProductTreeNode
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 { useSyncSkuParamWithCurrentItem, getFacetValuesMutator, formatCurrencyValue } from './util'
5
- export { PROMO_CODES } from './util/promo-codes'
4
+ export {
5
+ useSyncSkuParamWithCurrentItem,
6
+ getFacetValuesMutator,
7
+ formatCurrencyValue,
8
+ ProductMediaAccessor
9
+ } from './util'
6
10
 
7
- export const SEP = {
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.1.13",
3
+ "version": "6.2.1",
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
- "./types": "./types/index.ts"
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.4.2",
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.307.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",
@@ -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 getServiceSingleton from './impls'
12
- import type { Family, ProductTreeNode } from '../types'
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: ProductTreeNode
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>(getServiceSingleton(productsByFamily, rootNode, options))
34
+ const serviceRef = useRef<CommerceService>(getInstance(productsByFamily, rootNode, options, uiSpecs))
34
35
  return (
35
36
  <CommerceServiceContext.Provider value={serviceRef.current}>
36
37
  {children}
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { getInstance } from './impls/standalone'
2
+
3
+ export default getInstance
@@ -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, ProductTreeNode } from '../../../types'
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: ProductTreeNode,
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
- ProductTreeNode,
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 SEP from '../../../util/sep'
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: ProductTreeNode
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: ProductTreeNode,
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((c) => {
62
- c.products = c.products.map((p) => {
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(c.id, c)
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): ProductTreeNode | undefined {
109
- const toks = skuPath.split(SEP.TOK)
111
+ getNodeAtPath(skuPath: string): CategoryNode | undefined {
112
+ const toks = skuPath.split(sep.tok)
110
113
  let level = 1
111
- let desc: ProductTreeNode | undefined = this._rootNode
114
+ let node: CategoryNode | undefined = this._rootNode
112
115
  do {
113
- desc = desc!.subNodes?.find((vf) => (vf.skuToken === toks[level]))
116
+ node = node!.subNodes?.find((sn) => (sn.skuToken === toks[level]))
114
117
  level++
115
118
  }
116
- while (desc && (level < toks.length))
117
- return level === toks.length ? desc : undefined
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): ProductTreeNode[] | undefined => {
222
+ getSelectedNodesAtLevel = computedFn((level: number): CategoryNode[] | undefined => {
121
223
 
122
224
  let lvl = 1
123
- let valuesAtLevel: ProductTreeNode[] | undefined = this._rootNode.subNodes
225
+ let nodesAtLevel: CategoryNode[] | undefined = this._rootNode.subNodes
124
226
 
125
227
  do {
126
- let selectedAtLevel: ProductTreeNode[] | undefined = undefined
228
+ let selectedAtLevel: CategoryNode[] | undefined = undefined
127
229
  // If not specified, assume all
128
230
  if (lvl in this._selectedPaths) {
129
- selectedAtLevel = valuesAtLevel!.filter((fv) => (this._selectedPaths[lvl].includes(fv.skuToken)))
231
+ selectedAtLevel = nodesAtLevel!.filter((n) => (this._selectedPaths[lvl].includes(n.skuToken)))
130
232
  }
131
233
  else {
132
- selectedAtLevel = valuesAtLevel
234
+ selectedAtLevel = nodesAtLevel
133
235
  }
134
- let allSubsOfSelected: ProductTreeNode[] = []
135
- selectedAtLevel?.forEach((fvd: ProductTreeNode) => {
136
- if (fvd.subNodes) {
137
- allSubsOfSelected = [...allSubsOfSelected, ...fvd.subNodes]
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
- valuesAtLevel = allSubsOfSelected
243
+ nodesAtLevel = allSubsOfSelected
142
244
  lvl++
143
- } while (valuesAtLevel.length > 0 && lvl <= level)
245
+ } while (nodesAtLevel.length > 0 && lvl <= level)
144
246
 
145
- return (valuesAtLevel.length > 0 && ((lvl - 1) === level)) ? valuesAtLevel : undefined
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(SEP.TOK)
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: ProductTreeNode) => (
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: ProductTreeNode): Family[] {
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(SEP.TOK))
441
+ const fam = this._familyMap.get(path.join(sep.tok))
340
442
  if (!fam) {
341
- throw new Error("selectedFamilies WTF?!" + path.join(SEP.TOK))
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,7 @@
1
+ import type { TokenSeparators } from '../types'
2
+
3
+ export default {
4
+ tok: '-',
5
+ subTok: '_',
6
+ decimal: '.'
7
+ } satisfies TokenSeparators
@@ -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 { ProductTreeNode, SelectedPaths } from './tree-node'
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
- peekPath(skuPath: string): {
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
- siblingFamilies: Family[] | undefined
46
- categoryNode: ProductTreeNode | undefined
47
- isSku: boolean
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 ProductTreeNode[] may represent multiple sets.
94
+ * the returned CategoryNode[] may represent multiple sets.
63
95
  * */
64
- getSelectedNodesAtLevel(level: number): ProductTreeNode[] | undefined
96
+ getSelectedNodesAtLevel(level: number): CategoryNode[] | undefined
65
97
 
66
98
  /**
67
99
  * For convenience, so widgets can share state.