@hanzo/commerce 1.0.8

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.
@@ -0,0 +1,198 @@
1
+ import {
2
+ computed,
3
+ makeObservable,
4
+ observable,
5
+ runInAction,
6
+ action,
7
+ toJS
8
+ } from 'mobx'
9
+
10
+ import type {
11
+ Category,
12
+ LineItem,
13
+ FacetsValue,
14
+ FacetsDesc
15
+ } from '../../../types'
16
+
17
+ import type CommerceService from '../../commerce-service'
18
+
19
+ import ObservableLineItem from './obs-line-item'
20
+
21
+ export type StandaloneServiceOptions = {
22
+ levelZeroPrefix?: string
23
+ }
24
+
25
+ class StandaloneCommerceService
26
+ implements CommerceService
27
+ {
28
+ private _categoryMap = new Map<string, Category>()
29
+ private _facetsDesc: FacetsDesc
30
+ private _selectedFacets: FacetsValue = {}
31
+
32
+ private _options : StandaloneServiceOptions
33
+
34
+ private _currentItemSku: string | undefined = undefined
35
+
36
+ constructor(
37
+ categories: Category[],
38
+ facets: FacetsDesc,
39
+ options: StandaloneServiceOptions={}
40
+ ) {
41
+
42
+ this._facetsDesc = facets
43
+ this._options = options
44
+
45
+ categories.forEach((c) => {
46
+ c.products = c.products.map((p) => (new ObservableLineItem(p)))
47
+ this._categoryMap.set(c.id, c)
48
+ })
49
+
50
+ makeObservable<
51
+ StandaloneCommerceService,
52
+ '_selectedFacets' |
53
+ '_currentItemSku'
54
+ >(this, {
55
+ _selectedFacets : observable.deep,
56
+ _currentItemSku: observable,
57
+ })
58
+
59
+ makeObservable(this, {
60
+ cartItems: computed,
61
+ cartTotal: computed,
62
+ specifiedItems: computed,
63
+ setCurrentItem: action,
64
+ currentItem: computed,
65
+ item: computed,
66
+ specifiedCategories: computed,
67
+ facets: computed
68
+ /* NOT setFacets. It implements it's action mechanism */
69
+ })
70
+
71
+ }
72
+
73
+ get cartItems(): LineItem[] {
74
+ let result: LineItem[] = []
75
+ this._categoryMap.forEach((cat) => {
76
+ result = [...result, ...(cat.products as LineItem[]).filter((item) => (item.isInCart))]
77
+ })
78
+ return result
79
+ }
80
+
81
+ get cartTotal(): number {
82
+ return this.cartItems.reduce(
83
+ (total, item) => (total + item.price * item.quantity),
84
+ 0
85
+ )
86
+ }
87
+
88
+ setCurrentItem(sku: string | undefined): void {
89
+ this._currentItemSku = sku
90
+ }
91
+
92
+ /* ObsLineItemRef */
93
+ get item(): LineItem | undefined {
94
+ return this.currentItem
95
+ }
96
+
97
+ get currentItem(): LineItem | undefined {
98
+ if (!this._currentItemSku) return undefined
99
+
100
+ const categoriesTried: string[] = []
101
+ if (this.specifiedCategories && this.specifiedCategories.length > 0) {
102
+
103
+ for (let category of this.specifiedCategories) {
104
+ categoriesTried.push(category.id)
105
+ const foundItem =
106
+ (category.products as LineItem[]).find((item) => (item.sku === this._currentItemSku))
107
+ if (foundItem) {
108
+ return foundItem
109
+ }
110
+ }
111
+ }
112
+
113
+ let foundItem: LineItem | undefined = undefined
114
+ this._categoryMap.forEach((category, categoryId) => {
115
+ if (foundItem) return
116
+ if (categoriesTried.includes(categoryId)) return
117
+ foundItem = (category.products as LineItem[]).find((item) => (item.sku === this._currentItemSku))
118
+ })
119
+
120
+ return foundItem
121
+ }
122
+
123
+ setFacets(sel: FacetsValue): Category[] {
124
+ runInAction (() => {
125
+ const res = this._processAndValidate(sel)
126
+ if (res) {
127
+ this._selectedFacets = res
128
+ }
129
+ })
130
+ return this.specifiedCategories
131
+ }
132
+
133
+ get facets(): FacetsValue {
134
+ return this._selectedFacets
135
+ }
136
+
137
+ get specifiedCategories(): Category[] {
138
+ if (Object.keys(toJS(this._selectedFacets)).length === 0) {
139
+ // FacetsDesc have never been set or unset, so cannot evaluate them
140
+ return []
141
+ }
142
+ const keysStr = Object.keys(this._facetsDesc)
143
+ // 1-base, visiting two per iteration
144
+ let current: string[] = this._selectedFacets[1]
145
+ for (let i = 2; i <= keysStr.length; i++) {
146
+ current = StandaloneCommerceService._visit(current, this._selectedFacets[i])
147
+ }
148
+ const prefix = this._options.levelZeroPrefix ?? ''
149
+ return current.map((almostTheCatId) => (this._categoryMap.get(prefix + almostTheCatId)!))
150
+ }
151
+
152
+ private static _visit(current: string[], next: string[]): string[] {
153
+ const result: string[] = []
154
+ current.forEach((c) => {
155
+ next.forEach((n) => {
156
+ result.push(`${c}-${n}`)
157
+ })
158
+ })
159
+ return result
160
+ }
161
+
162
+ private _processAndValidate(partial: FacetsValue): FacetsValue | undefined {
163
+ const result: FacetsValue = {}
164
+ const keysStr = Object.keys(this._facetsDesc)
165
+ const keysNum = keysStr.map((key) => (parseInt(key)))
166
+ keysNum.forEach((key) => {
167
+ // if not present, assume the facet is "off" and allow all (include all in the set).
168
+ if (!partial[key]) {
169
+ result[key] = this._facetsDesc[key].map((fv) => (fv.value))
170
+ }
171
+ // If present, filter out the bad values if any
172
+ const filtered = partial[key].filter((fv) => this._facetsDesc[key].find((fvDesc) => (fvDesc.value === fv)))
173
+ result[key] = filtered
174
+ })
175
+ return result
176
+ }
177
+
178
+ get specifiedItems(): LineItem[] {
179
+ if (Object.keys(toJS(this._selectedFacets)).length === 0) {
180
+ // FacetsDesc have never been set or unset, so cannot evaluate them
181
+ return []
182
+ }
183
+
184
+ return this.specifiedCategories.reduce(
185
+ (allProducts, cat) => ([...allProducts, ...(cat.products as LineItem[])]), [] as LineItem[])
186
+ }
187
+
188
+ getCartCategorySubtotal(categoryId: string): number {
189
+ const c = this._categoryMap.get(categoryId)!
190
+ return (c.products as LineItem[]).reduce(
191
+ // avoid floating point bs around zero
192
+ (total, item) => (item.quantity > 0 ? total + item.price * item.quantity : total),
193
+ 0
194
+ )
195
+ }
196
+ }
197
+
198
+ export default StandaloneCommerceService
@@ -0,0 +1,4 @@
1
+ export type { default as CommerceService } from './commerce-service'
2
+ export { useCommerce, CommerceServiceProvider } from './context'
3
+ export { default as persistCart } from './utils'
4
+
@@ -0,0 +1,37 @@
1
+ import type { LineItem } from "../types"
2
+ import firebase_app from "./firebase-config"
3
+ import { getFirestore, collection, addDoc, setDoc, doc } from "firebase/firestore"
4
+
5
+ const db = getFirestore(firebase_app, 'lux-commerce')
6
+
7
+ export default async function persistCart(cart: LineItem[], userEmail: string) {
8
+ let result = null
9
+ let error = null
10
+ const ordersRef = collection(db, "orders");
11
+
12
+ const cartAsObject = cart.reduce((acc, item) => {
13
+ acc[item.sku] = {
14
+ sku: item.sku,
15
+ title: item.title,
16
+ quantity: item.quantity,
17
+ description: item.desc,
18
+ img: item.img,
19
+ price: item.price,
20
+ }
21
+ return acc
22
+ }, {} as Record<string, {}>)
23
+
24
+ try {
25
+ try {
26
+ result = await setDoc(doc(ordersRef, `${userEmail}-${Date.now()}`), { order: cartAsObject })
27
+ } catch (e) {
28
+ console.error('Error writing item document: ', e)
29
+ error = e
30
+ }
31
+ } catch (e) {
32
+ console.error('Error writing order document: ', e)
33
+ error = e
34
+ }
35
+
36
+ return { result, error }
37
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../tsconfig.hanzo-modules.base.json",
3
+ "include": [
4
+ "**/*.ts",
5
+ "**/*.tsx",
6
+ ],
7
+ "exclude": [
8
+ "node_modules",
9
+ "service/impl/hanzo-adapter/commerceJs/Commerce.test.ts"
10
+ ]
11
+ }
@@ -0,0 +1,2 @@
1
+ These are the types that the card ui expects and understands.
2
+ There should always be adaptors both ways from Hanzo commerceJs types.
package/types/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ interface Product {
4
+ id: string // DB index // not a logical aspect of our domain. may not be necessary at all
5
+ sku: string // human visible on orders etc.
6
+ title: string
7
+ shortTitle?: string
8
+ titleAsOption: string
9
+ categoryId: string // skuPath, eg LXB-AU-B
10
+ desc?: string
11
+ price: number
12
+ img?: string // if undefined: (category's img exists) ? (use it) : (use generic placeholder)
13
+ }
14
+
15
+ interface Category {
16
+ id: string // LXB-AU-B
17
+ title: string // Lux Gold, Minted Bar
18
+ desc?: string
19
+ img?: string
20
+ // inbound they're Products and then interally they become LineItems
21
+ products: Product[] | LineItem[]
22
+ }
23
+
24
+
25
+ interface FacetValueDesc {
26
+ value: string // a token in the sku
27
+ label: string
28
+ img : string | ReactNode // icon is required
29
+ imgAR? : number // helps with svgs
30
+ }
31
+
32
+ /* *** FOR EXAMPLE **
33
+ {
34
+ 1: [ {
35
+ token: 'AG',
36
+ label: 'Silver',
37
+ img: '/assets/img/cart/ui/facets/silver-swatch-200x200.png'
38
+ },
39
+ ... more FaceValues describing the "type" level (Silver, Gold)
40
+ ],
41
+ 2 [
42
+ {
43
+ token: 'B'
44
+ label: 'Minted Bar
45
+ },
46
+ ... more FaceValues describing the "form" level (Bar, Coin, )
47
+ ]
48
+ }
49
+ */
50
+ type FacetsDesc = Record<number, FacetValueDesc[]>
51
+
52
+ // Which facets tokens are on at each level
53
+ type FacetsValue = Record<number, string[]>
54
+
55
+ // Client code always
56
+ // has a LineItem, whether the Product
57
+ // is in the cart or not. Something is in the cart
58
+ // when its quantity > 0. That's the only difference.
59
+ // The ui, and as well as some Cart state, reacts to
60
+ // changes in this quantity.
61
+
62
+ // It could have more accurately been named
63
+ // 'QuantifiedProduct' but that sucked.
64
+ interface LineItem extends Product {
65
+
66
+ /** all observable */
67
+ get quantity(): number
68
+ get canDecrement(): boolean
69
+ get isInCart(): boolean
70
+
71
+ increment(): void
72
+ decrement(): void
73
+ }
74
+
75
+ interface ObsLineItemRef {
76
+ get item(): LineItem | undefined
77
+ }
78
+
79
+
80
+ interface StringMutator {
81
+ get(): string | null
82
+ set(v: string | null): void
83
+ }
84
+
85
+ interface StringArrayMutator {
86
+ get(): string[] | null
87
+ set(v: string[] | null): void
88
+ }
89
+
90
+ export {
91
+ type Product,
92
+ type Category,
93
+ type LineItem,
94
+ type ObsLineItemRef,
95
+ type FacetsDesc,
96
+ type FacetsValue,
97
+ type FacetValueDesc,
98
+ type StringMutator,
99
+ type StringArrayMutator
100
+ }
101
+
102
+
103
+
package/util/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export function toTitleCase(str: string) {
2
+ return str.replace(
3
+ /\w\S*/g,
4
+ (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase()
5
+ )
6
+ }
7
+
8
+ export function slugify(str: string) {
9
+ return str
10
+ .toLowerCase()
11
+ .replace(/ /g, '-')
12
+ .replace(/[^\w-]+/g, '')
13
+ .replace(/--+/g, '-')
14
+ }
15
+
16
+ export function unslugify(str: string) {
17
+ return str.replace(/-/g, ' ')
18
+ }
19
+
20
+ export function formatPrice(price: number): string {
21
+ return price.toLocaleString('en-US', {
22
+ style: 'currency',
23
+ currency: 'USD',
24
+ });
25
+ }