@hanzo/commerce 1.0.12 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import React, { type PropsWithChildren, useState } from 'react'
2
+ import React, { type PropsWithChildren } from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
4
  import { observer } from 'mobx-react-lite'
5
5
 
@@ -7,7 +7,7 @@ import { Button } from '@hanzo/ui/primitives'
7
7
  import { cn } from '@hanzo/ui/util'
8
8
  import { useAuth } from '@hanzo/auth/service'
9
9
 
10
- import { persistCart, useCommerce } from '../service'
10
+ import { useCommerce } from '../service/context'
11
11
  import { formatPrice } from '../util'
12
12
 
13
13
  import CartLineItem from './cart-line-item'
@@ -17,42 +17,34 @@ const Cart: React.FC<PropsWithChildren & {
17
17
  isMobile?: boolean,
18
18
  hideCheckout?: boolean
19
19
  }> = observer(({
20
+ /** Children is the heading area. */
20
21
  children,
21
22
  className='',
22
23
  isMobile=false,
23
24
  hideCheckout=false
24
25
  }) => {
25
- const [loadingCheckout, setLoadingCheckout] = useState(false)
26
26
  const cmmc = useCommerce()
27
27
  const router = useRouter()
28
28
  const auth = useAuth()
29
-
30
- const checkout = async () => {
31
- setLoadingCheckout(true)
32
- if (auth.user) {
33
- await persistCart(cmmc.cartItems, auth.user.email)
34
- }
35
- router.push('/checkout')
36
- }
37
29
 
38
30
  return (
39
31
  <div className={cn('border p-6 rounded-lg', className)}>
40
32
  {children}
41
33
  <div className='mt-2'>
42
- {!!children && <div className='h-[1px] w-pr-80 mx-auto bg-muted-3'/>}
34
+ {!!children && <div className='h-[1px] w-pr-80 mb-3 mx-auto bg-muted-3'/>}
43
35
  {cmmc.cartItems.length === 0 ? (
44
36
  <p className='text-center mt-4'>No items in cart</p>
45
37
  ) : (<>
46
38
  {cmmc.cartItems.map((item, i) => (<CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-4 sm:mb-2'/>))}
47
- <p className='mt-6 text-right border-t pt-1'>TOTAL: {cmmc.cartTotal === 0 ? '0' : formatPrice(cmmc.cartTotal)}</p>
39
+ <p className='mt-6 text-right border-t pt-1'>TOTAL: <span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatPrice(cmmc.cartTotal)}</span></p>
48
40
  </>)}
49
41
  </div>
50
42
  {cmmc.cartItems.length > 0 && !hideCheckout && (
51
43
  <>
52
- {!auth.loggedIn ? (
53
- <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login</Button>
44
+ {!(auth && auth.loggedIn) ? (
45
+ <Button size='sm' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login to checkout</Button>
54
46
  ) : (
55
- <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={checkout} disabled={loadingCheckout}>Checkout</Button>
47
+ <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/checkout')}>Checkout</Button>
56
48
  )}
57
49
  </>
58
50
  )}
@@ -8,4 +8,4 @@ export { default as ProductSelectionMobilePicker } from './product-selection-mob
8
8
  export { default as ProductSelectionRadioGroup } from './product-selection-radio-group'
9
9
  export { default as SelectCategoryAndItemWidget } from './select-category-and-item-widget'
10
10
  export { default as SelectItemInCategoryView } from './select-item-in-category-view'
11
- export * as Icons from './Icons'
11
+ export { Icons } from './Icons'
@@ -1,13 +1,12 @@
1
1
  'use client'
2
2
  import React, { useEffect } from 'react'
3
- import { computed } from 'mobx'
4
3
  import { observer } from 'mobx-react-lite'
5
4
 
6
5
  import { ApplyTypography, ListBox } from '@hanzo/ui/primitives'
7
6
  import { cn } from '@hanzo/ui/util'
8
7
 
9
8
  import type { FacetValueDesc, FacetsValue, LineItem } from '../types'
10
- import { useCommerce } from '../service'
9
+ import { useCommerce } from '../service/context'
11
10
  import { formatPrice } from '../util'
12
11
 
13
12
  import FacetTogglesWidget from './facet-toggles-widget'
@@ -1,13 +1,12 @@
1
1
  'use client'
2
- import React, { useState } from 'react'
2
+ import React from 'react'
3
3
  import Image from 'next/image'
4
4
  import { observer } from 'mobx-react-lite'
5
5
 
6
6
  import { cn } from '@hanzo/ui/util'
7
7
  import { Skeleton } from '@hanzo/ui/primitives'
8
8
 
9
- import { useCommerce } from '../service'
10
- import type { Category, LineItem, ObsLineItemRef } from '../types'
9
+ import type { Category, ObsLineItemRef } from '../types'
11
10
  import { formatPrice } from '../util'
12
11
  import { Icons } from './Icons'
13
12
 
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types'
2
+ export * from './service/context'
3
+ export type { StandaloneServiceOptions as ServiceOptions } from './service/impls/standalone/standalone-service'
4
+ export { useSkuAndFacetParams } from './util'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "1.0.12",
3
+ "version": "1.1.1",
4
4
  "description": "Library with shopping cart components.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -6,21 +6,22 @@ import { enableStaticRendering } from 'mobx-react-lite'
6
6
  enableStaticRendering(typeof window === "undefined")
7
7
 
8
8
 
9
- import type CommerceService from './commerce-service'
10
-
11
- import getServiceSingleton from './impl'
9
+ import type CommerceService from '../types/commerce-service'
10
+ import type { ServiceOptions } from '..'
11
+ import getServiceSingleton from './impls'
12
12
  import type { Category, FacetsDesc } from '../types'
13
13
 
14
14
  const CommerceServiceContext = createContext<CommerceService | undefined>(undefined)
15
15
 
16
- export const useCommerce = (): CommerceService => {
16
+
17
+ const useCommerce = (): CommerceService => {
17
18
  return useContext(CommerceServiceContext) as CommerceService
18
19
  }
19
20
 
20
- export const CommerceServiceProvider: React.FC<PropsWithChildren & {
21
+ const CommerceServiceProvider: React.FC<PropsWithChildren & {
21
22
  productsByCategory: Category[]
22
23
  facets: FacetsDesc
23
- options?: any
24
+ options?: ServiceOptions
24
25
  }> = ({
25
26
  children,
26
27
  productsByCategory,
@@ -30,10 +31,12 @@ export const CommerceServiceProvider: React.FC<PropsWithChildren & {
30
31
 
31
32
  const serviceRef = useRef<CommerceService>(getServiceSingleton(productsByCategory, facets, options))
32
33
  return (
33
- <CommerceServiceContext.Provider
34
- value={serviceRef.current}
35
- >
34
+ <CommerceServiceContext.Provider value={serviceRef.current}>
36
35
  {children}
37
36
  </CommerceServiceContext.Provider>
38
37
  )
39
38
  }
39
+
40
+ export {
41
+ useCommerce, CommerceServiceProvider
42
+ }
@@ -0,0 +1,3 @@
1
+ import { getInstance } from './standalone'
2
+
3
+ export default getInstance
@@ -7,7 +7,18 @@ import {
7
7
 
8
8
  import type { Product, LineItem } from '../../../types'
9
9
 
10
- class ObsLineItem implements LineItem {
10
+ interface ActualLineItemSnapshot {
11
+ sku: string
12
+ categoryId: string // helps impl of restoreFromSnapshot
13
+ title: string
14
+ price: number
15
+ quantity: number
16
+ timeAdded: number // helps to sort view of order and cart
17
+ }
18
+
19
+ class ActualLineItem
20
+ implements LineItem
21
+ {
11
22
 
12
23
  qu: number = 0
13
24
 
@@ -18,9 +29,10 @@ class ObsLineItem implements LineItem {
18
29
  categoryId: string
19
30
  desc?: string
20
31
  price: number
21
- img?: string
32
+ img?: string
33
+ timeAdded: number = 0 // timeAdded of being added to cart
22
34
 
23
- constructor(prod: Product) {
35
+ constructor(prod: Product, snap?: ActualLineItemSnapshot) {
24
36
  this.id = prod.id
25
37
  this.sku = prod.sku
26
38
  this.title = prod.title
@@ -30,8 +42,14 @@ class ObsLineItem implements LineItem {
30
42
  this.price = prod.price
31
43
  this.img = prod.img
32
44
 
45
+ if (snap) {
46
+ this.qu = snap.quantity
47
+ this.timeAdded = snap.quantity
48
+ }
49
+
33
50
  makeObservable(this, {
34
51
  qu: observable,
52
+ timeAdded: observable,
35
53
  canDecrement: computed,
36
54
  isInCart: computed,
37
55
 
@@ -40,15 +58,32 @@ class ObsLineItem implements LineItem {
40
58
  })
41
59
  }
42
60
 
61
+ takeSnapshot = (): ActualLineItemSnapshot => ({
62
+ sku: this.sku,
63
+ categoryId: this.categoryId,
64
+ title: this.title,
65
+ price: this.price,
66
+ quantity: this.qu,
67
+ timeAdded: this.timeAdded
68
+ } satisfies ActualLineItemSnapshot)
69
+
43
70
  get canDecrement(): boolean { return this.qu > 0 }
44
71
  get quantity(): number {return this.qu}
45
72
  get isInCart(): boolean {return this.qu > 0}
46
73
 
47
- increment(): void { this.qu++ }
74
+ increment(): void {
75
+ if (this.qu === 0) {
76
+ this.timeAdded = new Date().getTime()
77
+ }
78
+ this.qu++
79
+ }
48
80
 
49
81
  decrement(): void {
50
82
  if (this.canDecrement) {
51
83
  this.qu--
84
+ if (this.qu === 0) {
85
+ this.timeAdded = 0
86
+ }
52
87
  }
53
88
  }
54
89
 
@@ -59,4 +94,7 @@ class ObsLineItem implements LineItem {
59
94
 
60
95
  }
61
96
 
62
- export default ObsLineItem
97
+ export {
98
+ type ActualLineItemSnapshot,
99
+ ActualLineItem as default
100
+ }
@@ -0,0 +1,54 @@
1
+ import { enableStaticRendering } from 'mobx-react-lite'
2
+
3
+ import type { CommerceService, Category, FacetsDesc } from '../../../types'
4
+ import StandaloneService, {type StandaloneServiceOptions} from './standalone-service'
5
+
6
+ import { readSnapshot, listenAndWriteSnapshots } from './localStorage'
7
+
8
+ enableStaticRendering(typeof window === "undefined")
9
+
10
+ const _log = (s: string) => {
11
+ const d = new Date()
12
+ console.log(`TIMESTAMPED: ${d.getUTCMinutes()}:${d.getUTCSeconds()}:${d.getUTCMilliseconds()}`)
13
+ console.log(s)
14
+ }
15
+
16
+ // https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
17
+
18
+ let instance: StandaloneService | undefined = undefined
19
+
20
+ export const getInstance = (
21
+ categories: Category[],
22
+ facets: FacetsDesc,
23
+ options?: StandaloneServiceOptions
24
+ ): CommerceService => {
25
+
26
+ if (!options) {
27
+ throw new Error('cmmc getInstance(): Standalone Commerce Service requires config options!')
28
+ }
29
+
30
+ if (typeof window === "undefined") {
31
+ //_log("NEW INSTANCE FOR SERVER")
32
+ return new StandaloneService(
33
+ categories,
34
+ facets,
35
+ options
36
+ )
37
+ }
38
+
39
+ // Client side, create the store only once in the client
40
+ if (!instance) {
41
+ //_log("NEW INSTANCE FOR CLIENT")
42
+ const snapShot = readSnapshot()
43
+ instance = new StandaloneService(
44
+ categories,
45
+ facets,
46
+ options,
47
+ snapShot
48
+ )
49
+ listenAndWriteSnapshots(instance)
50
+ }
51
+
52
+ return instance
53
+ }
54
+
@@ -0,0 +1,34 @@
1
+ import { reaction } from 'mobx'
2
+ import StandaloneService, { type StandaloneServiceSnapshot } from './standalone-service'
3
+
4
+ const LS_KEY = 'lux-cart'
5
+
6
+ const readSnapshot = (): StandaloneServiceSnapshot | undefined => {
7
+ const snapshotAsStr = localStorage.getItem(LS_KEY)
8
+ return snapshotAsStr ? JSON.parse(snapshotAsStr) as StandaloneServiceSnapshot : undefined
9
+ }
10
+
11
+ const listenAndWriteSnapshots = (cmmc: StandaloneService): void => {
12
+
13
+ if (typeof window !== 'undefined') {
14
+ reaction(
15
+ () => (cmmc.cartTotal),
16
+ (total) => {
17
+ if (total > 0) {
18
+ const snapshot = cmmc.takeSnapshot()
19
+ // console.log(`CMMC LOCAL STORAGE UPDATE. (CART TOTAL: ${total}`)
20
+ localStorage.setItem(LS_KEY, JSON.stringify(snapshot) )
21
+ }
22
+ else {
23
+ localStorage.removeItem(LS_KEY)
24
+ }
25
+ }
26
+ )
27
+ }
28
+ }
29
+
30
+ export {
31
+ readSnapshot,
32
+ listenAndWriteSnapshots
33
+ }
34
+
@@ -0,0 +1,14 @@
1
+ import { initializeApp, getApps } from 'firebase/app'
2
+
3
+ const firebaseConfig = {
4
+ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
5
+ authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
6
+ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
7
+ storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
8
+ messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
9
+ appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
10
+ measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
11
+ }
12
+
13
+ // Initialize Firebase instance if there isn't one already
14
+ export default getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]
@@ -0,0 +1,102 @@
1
+ import firebaseApp from './firebase-app'
2
+
3
+ import {
4
+ getFirestore,
5
+ collection,
6
+ setDoc,
7
+ doc,
8
+ serverTimestamp,
9
+ type Firestore,
10
+ type FieldValue,
11
+ } from 'firebase/firestore'
12
+
13
+ import type { ActualLineItemSnapshot } from '../actual-line-item'
14
+
15
+ let dbInstance: Firestore | undefined = undefined
16
+
17
+ const getDBInstance = (name: string): Firestore => {
18
+ if (!dbInstance) {
19
+ dbInstance = getFirestore(firebaseApp, name)
20
+ }
21
+ return dbInstance
22
+ }
23
+
24
+ interface SavedOrder {
25
+ email: string
26
+ paymentMethod: string
27
+ status: string
28
+ timestamp: FieldValue
29
+ items: ActualLineItemSnapshot[]
30
+ }
31
+
32
+ const createOrder = async (
33
+ email: string,
34
+ paymentMethod: string,
35
+ items: ActualLineItemSnapshot[],
36
+ options: {
37
+ dbName: string
38
+ ordersTable: string
39
+ }
40
+ ): Promise<{
41
+ success: boolean,
42
+ error: any,
43
+ id?: string
44
+ }> => {
45
+
46
+ let error: any | null = null
47
+ const ordersRef = collection(getDBInstance(options.dbName), options.ordersTable)
48
+ const orderId = `${email}-${new Date().toISOString()}`
49
+
50
+ try {
51
+ await setDoc(doc(ordersRef, orderId), {
52
+ email,
53
+ paymentMethod,
54
+ status: 'open',
55
+ timestamp: serverTimestamp(),
56
+ items,
57
+ } satisfies SavedOrder)
58
+ return { success: !error, error, id: orderId }
59
+ }
60
+ catch (e) {
61
+ console.error('Error writing item document: ', e)
62
+ error = e
63
+ }
64
+
65
+ return { success: !error, error }
66
+ }
67
+
68
+ const updateOrder = async (
69
+ orderId: string,
70
+ email: string,
71
+ paymentMethod: string,
72
+ items: ActualLineItemSnapshot[],
73
+ options: {
74
+ dbName: string
75
+ ordersTable: string
76
+ }
77
+ ): Promise<{
78
+ success: boolean,
79
+ error: any
80
+ }> => {
81
+
82
+ let error: any | null = null
83
+ const ordersRef = collection(getDBInstance(options.dbName), options.ordersTable)
84
+
85
+ try {
86
+ await setDoc(doc(ordersRef, orderId), {
87
+ email,
88
+ paymentMethod,
89
+ status: 'open',
90
+ timestamp: serverTimestamp(),
91
+ items,
92
+ } satisfies SavedOrder)
93
+ }
94
+ catch (e) {
95
+ console.error('Error writing item document: ', e)
96
+ error = e
97
+ }
98
+
99
+ return { success: !error, error }
100
+ }
101
+
102
+ export { createOrder, updateOrder }
@@ -7,22 +7,32 @@ import {
7
7
  toJS
8
8
  } from 'mobx'
9
9
 
10
- import type {
10
+ import type {
11
+ CommerceService,
11
12
  Category,
12
13
  LineItem,
13
14
  FacetsValue,
14
15
  FacetsDesc
15
16
  } from '../../../types'
16
17
 
17
- import type CommerceService from '../../commerce-service'
18
+ import {
19
+ createOrder as createOrderHelper,
20
+ updateOrder as updateOrderHelper
21
+ } from './orders'
18
22
 
19
- import ObservableLineItem from './obs-line-item'
23
+ import ActualLineItem, { type ActualLineItemSnapshot } from './actual-line-item'
20
24
 
21
- export type StandaloneServiceOptions = {
25
+ type StandaloneServiceOptions = {
22
26
  levelZeroPrefix?: string
27
+ dbName: string
28
+ ordersTable: string
23
29
  }
24
30
 
25
- class StandaloneCommerceService
31
+ interface StandaloneServiceSnapshot {
32
+ items: ActualLineItemSnapshot[]
33
+ }
34
+
35
+ class StandaloneService
26
36
  implements CommerceService
27
37
  {
28
38
  private _categoryMap = new Map<string, Category>()
@@ -30,25 +40,33 @@ class StandaloneCommerceService
30
40
  private _selectedFacets: FacetsValue = {}
31
41
 
32
42
  private _options : StandaloneServiceOptions
33
-
34
- private _currentItem: LineItem | undefined = undefined
43
+ private _currentItem: ActualLineItem | undefined = undefined
35
44
 
36
45
  constructor(
37
46
  categories: Category[],
38
47
  facets: FacetsDesc,
39
- options: StandaloneServiceOptions={}
48
+ options: StandaloneServiceOptions,
49
+ serviceSnapshot?: StandaloneServiceSnapshot,
40
50
  ) {
41
51
 
42
52
  this._facetsDesc = facets
43
53
  this._options = options
44
54
 
45
55
  categories.forEach((c) => {
46
- c.products = c.products.map((p) => (new ObservableLineItem(p)))
56
+ c.products = c.products.map((p) => {
57
+ if (serviceSnapshot) {
58
+ const itemSnapshot = serviceSnapshot.items.find((is) => (is.sku === p.sku))
59
+ if (itemSnapshot) {
60
+ return new ActualLineItem(p, itemSnapshot)
61
+ }
62
+ }
63
+ return new ActualLineItem(p)
64
+ })
47
65
  this._categoryMap.set(c.id, c)
48
66
  })
49
67
 
50
68
  makeObservable<
51
- StandaloneCommerceService,
69
+ StandaloneService,
52
70
  '_selectedFacets' |
53
71
  '_currentItem'
54
72
  >(this, {
@@ -67,15 +85,29 @@ class StandaloneCommerceService
67
85
  facetsValue: computed
68
86
  /* NOT setFacets. It implements it's action mechanism */
69
87
  })
88
+ }
89
+
90
+ async createOrder(email: string, paymentMethod: string): Promise<string | undefined> {
91
+ const snapshot = this.takeSnapshot()
92
+ const order = await createOrderHelper(email, paymentMethod, snapshot.items, this._options) // didn't want to have two levels of 'items'
93
+ return order.id
94
+ }
70
95
 
96
+ async updateOrder(orderId: string, email: string, paymentMethod: string): Promise<void> {
97
+ const snapshot = this.takeSnapshot()
98
+ updateOrderHelper(orderId, email, paymentMethod, snapshot.items, this._options) // didn't want to have two levels of 'items'
71
99
  }
72
100
 
101
+ takeSnapshot = (): StandaloneServiceSnapshot => ({
102
+ items : (this.cartItems as ActualLineItem[]).map((it) => (it.takeSnapshot()))
103
+ })
104
+
73
105
  get cartItems(): LineItem[] {
74
106
  let result: LineItem[] = []
75
107
  this._categoryMap.forEach((cat) => {
76
108
  result = [...result, ...(cat.products as LineItem[]).filter((item) => (item.isInCart))]
77
109
  })
78
- return result
110
+ return result.sort((it1, it2) => ((it1 as ActualLineItem).timeAdded - (it2 as ActualLineItem).timeAdded))
79
111
  }
80
112
 
81
113
  get cartTotal(): number {
@@ -91,27 +123,27 @@ class StandaloneCommerceService
91
123
  this._currentItem = undefined
92
124
  return true
93
125
  }
94
-
95
- this._currentItem = ((): LineItem | undefined => {
126
+ // self calling function
127
+ this._currentItem = ((): ActualLineItem | undefined => {
128
+
96
129
  const categoriesTried: string[] = []
97
130
  if (this.specifiedCategories && this.specifiedCategories.length > 0) {
98
-
99
- for (let category of this.specifiedCategories) {
131
+ for (let category of this.specifiedCategories) {
100
132
  categoriesTried.push(category.id)
101
- const foundItem =
102
- (category.products as LineItem[]).find((item) => (item.sku === skuToFind))
133
+ const foundItem = category.products.find((p) => (p.sku === skuToFind))
103
134
  if (foundItem) {
104
- return foundItem
135
+ return foundItem as ActualLineItem
105
136
  }
106
137
  }
107
138
  }
108
-
109
- let foundItem: LineItem | undefined = undefined
110
- this._categoryMap.forEach((category, categoryId) => {
111
- if (foundItem) return
112
- if (categoriesTried.includes(categoryId)) return
113
- foundItem = (category.products as LineItem[]).find((item) => (item.sku === skuToFind))
114
- })
139
+ for( const [categoryId, category] of this._categoryMap.entries()) {
140
+ if (categoriesTried.includes(categoryId)) continue
141
+ const foundItem = category.products.find((p) => (p.sku === skuToFind)) as ActualLineItem | undefined
142
+ if (foundItem) {
143
+ return foundItem as ActualLineItem
144
+ }
145
+ }
146
+ return undefined
115
147
  })();
116
148
 
117
149
  return !!this._currentItem
@@ -120,7 +152,7 @@ class StandaloneCommerceService
120
152
 
121
153
  /* ObsLineItemRef */
122
154
  get item(): LineItem | undefined {
123
- return this.currentItem
155
+ return this._currentItem
124
156
  }
125
157
 
126
158
  get currentItem(): LineItem | undefined {
@@ -154,7 +186,7 @@ class StandaloneCommerceService
154
186
  // 1-base, visiting two per iteration
155
187
  let current: string[] = this._selectedFacets[1]
156
188
  for (let i = 2; i <= keysStr.length; i++) {
157
- current = StandaloneCommerceService._visit(current, this._selectedFacets[i])
189
+ current = StandaloneService._visit(current, this._selectedFacets[i])
158
190
  }
159
191
  const prefix = this._options.levelZeroPrefix ?? ''
160
192
  return current.map((almostTheCatId) => (this._categoryMap.get(prefix + almostTheCatId)!))
@@ -206,4 +238,8 @@ class StandaloneCommerceService
206
238
  }
207
239
  }
208
240
 
209
- export default StandaloneCommerceService
241
+ export {
242
+ type StandaloneServiceOptions,
243
+ type StandaloneServiceSnapshot,
244
+ StandaloneService as default
245
+ }
package/tsconfig.json CHANGED
@@ -6,6 +6,5 @@
6
6
  ],
7
7
  "exclude": [
8
8
  "node_modules",
9
- "service/impl/hanzo-adapter/commerceJs/Commerce.test.ts"
10
9
  ]
11
10
  }
@@ -0,0 +1,14 @@
1
+ import type Product from './product'
2
+
3
+ interface Category {
4
+ id: string // LXB-AU-B
5
+ title: string // Lux Gold, Minted Bar
6
+ desc?: string
7
+ img?: string
8
+ // inbound they're Products and then interally they become LineItems
9
+ products: Product[]
10
+ }
11
+
12
+ export {
13
+ type Category as default
14
+ }
@@ -1,9 +1,6 @@
1
- import type {
2
- Category,
3
- FacetsValue,
4
- LineItem ,
5
- ObsLineItemRef
6
- } from '../types'
1
+ import type { LineItem, ObsLineItemRef } from './line-item'
2
+ import type { FacetsValue } from './facet'
3
+ import type Category from './category'
7
4
 
8
5
  interface CommerceService extends ObsLineItemRef {
9
6
 
@@ -11,6 +8,9 @@ interface CommerceService extends ObsLineItemRef {
11
8
  get cartTotal(): number
12
9
  getCartCategorySubtotal(categoryId: string): number
13
10
 
11
+ createOrder(email: string, paymentMethod: string): Promise<string | undefined>
12
+ updateOrder(orderId: string, email: string, paymentMethod: string): Promise<void>
13
+
14
14
  /**
15
15
  * Sets the tokens at each level supplied.
16
16
  * If a level is specifed as [], nothing will be specified.
@@ -30,7 +30,7 @@ interface CommerceService extends ObsLineItemRef {
30
30
  * "current" is unrelated to what is "specified",
31
31
  * ie, facets' values
32
32
  * */
33
- setCurrentItem(sku: string | undefined): boolean // was valid sku and was set.
33
+ setCurrentItem(sku: string | undefined): boolean // valid sku and was set.
34
34
  /**
35
35
  * For convenience, so widgets can share state.
36
36
  * "current" is unrelated to what is "specified",
package/types/facet.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ interface FacetValueDesc {
4
+ value: string // a token in the sku
5
+ label: string
6
+ img : string | ReactNode // icon is required
7
+ imgAR? : number // helps with svgs
8
+ }
9
+
10
+ /* *** FOR EXAMPLE **
11
+ {
12
+ 1: [ {
13
+ token: 'AG',
14
+ label: 'Silver',
15
+ img: '/assets/img/cart/ui/facets/silver-swatch-200x200.png'
16
+ },
17
+ ... more FaceValues describing the "type" level (Silver, Gold)
18
+ ],
19
+ 2 [
20
+ {
21
+ token: 'B'
22
+ label: 'Minted Bar
23
+ },
24
+ ... more FaceValues describing the "form" level (Bar, Coin, )
25
+ ]
26
+ }
27
+ */
28
+ type FacetsDesc = Record<number, FacetValueDesc[]>
29
+
30
+ // Which facets tokens are on at each level
31
+ type FacetsValue = Record<number, string[]>
32
+
33
+ export type {
34
+ FacetValueDesc,
35
+ FacetsDesc,
36
+ FacetsValue
37
+ }
package/types/index.ts CHANGED
@@ -1,103 +1,10 @@
1
- import type { ReactNode } from 'react'
2
1
 
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
- }
2
+ export type { default as CommerceService } from './commerce-service'
3
+ export type { default as Product } from './product'
4
+ export type { default as Category } from './category'
5
+ export * from './line-item'
6
+ export * from './facet'
7
+ export * from './string-mutator'
101
8
 
102
9
 
103
10
 
@@ -0,0 +1,27 @@
1
+ import type Product from './product'
2
+
3
+ // Client code always
4
+ // has a LineItem, whether the Product
5
+ // is in the cart or not. Something is in the cart
6
+ // when its quantity > 0. That's the only difference.
7
+ // The ui, and as well as some Cart state, reacts to
8
+ // changes in this quantity.
9
+
10
+ // It could have more accurately been named
11
+ // 'QuantifiedProduct' but that sucked.
12
+ interface LineItem extends Product {
13
+
14
+ /** all observable */
15
+ get quantity(): number
16
+ get canDecrement(): boolean
17
+ get isInCart(): boolean
18
+
19
+ increment(): void
20
+ decrement(): void
21
+ }
22
+
23
+ interface ObsLineItemRef {
24
+ get item(): LineItem | undefined
25
+ }
26
+
27
+ export type { LineItem, ObsLineItemRef}
@@ -0,0 +1,15 @@
1
+ interface Product {
2
+ id: string // DB index // not a logical aspect of our domain. may not be necessary at all
3
+ sku: string // human visible on orders etc.
4
+ title: string
5
+ shortTitle?: string
6
+ titleAsOption: string
7
+ categoryId: string // skuPath, eg LXB-AU-B
8
+ desc?: string
9
+ price: number
10
+ img?: string // if undefined: (category's img exists) ? (use it) : (use generic placeholder)
11
+ }
12
+
13
+ export {
14
+ type Product as default
15
+ }
@@ -0,0 +1,14 @@
1
+ interface StringMutator {
2
+ get(): string | null
3
+ set(v: string | null): void
4
+ }
5
+
6
+ interface StringArrayMutator {
7
+ get(): string[] | null
8
+ set(v: string[] | null): void
9
+ }
10
+
11
+ export type {
12
+ StringMutator,
13
+ StringArrayMutator
14
+ }
@@ -1,3 +1,4 @@
1
+ "use client"
1
2
  import {
2
3
  useEffect,
3
4
  useRef,
@@ -11,8 +12,8 @@ import {
11
12
  parseAsBoolean,
12
13
  } from 'next-usequerystate'
13
14
 
14
- import type { Category, FacetsValue, StringMutator } from '../types'
15
- import { useCommerce } from '../service'
15
+ import type { FacetsValue, StringMutator } from '../types'
16
+ import { useCommerce } from '../service/context'
16
17
 
17
18
  const PLEASE_SELECT_FACETS = 'Please select an option from each group above.'
18
19
 
@@ -21,6 +22,7 @@ interface GetMutator {
21
22
  }
22
23
 
23
24
  const useSkuAndFacetParams = (
25
+ setMessage: (m: string | undefined) => void,
24
26
  setLoading?: (l: boolean) => void
25
27
  ) => {
26
28
 
@@ -49,8 +51,6 @@ const useSkuAndFacetParams = (
49
51
  parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
50
52
  )
51
53
 
52
- const [message, setMessage] = useState<string | undefined>(undefined)
53
-
54
54
  const directMutator: GetMutator = (level: 1 | 2): StringMutator => {
55
55
 
56
56
  const setLevel = (value: string, level: 1 | 2 ): void => {
@@ -83,6 +83,8 @@ const useSkuAndFacetParams = (
83
83
  }
84
84
  }
85
85
 
86
+ let message: string | undefined = undefined
87
+
86
88
  useEffect(() => {
87
89
 
88
90
  const setCurrentCategoryFromSku = (sku: string) => {
@@ -110,14 +112,14 @@ const useSkuAndFacetParams = (
110
112
  }
111
113
  })
112
114
  }
113
- setMessage(undefined)
114
115
  encRef.current.usingSkuMode = true
115
116
  }
116
117
  else {
118
+
117
119
  setSkuParam('')
118
120
  // if sent here w an invalid sku,
119
121
  // it will effectively put us in facet params mode
120
- setMessage('Invalid sku. ' + PLEASE_SELECT_FACETS)
122
+ message = 'Invalid sku. ' + PLEASE_SELECT_FACETS
121
123
  }
122
124
  }
123
125
 
@@ -146,8 +148,7 @@ const useSkuAndFacetParams = (
146
148
  if (level1 && level2) {
147
149
  const categories = cmmc.setFacets(facets)
148
150
  if (categories && categories.length > 0) {
149
- cmmc.setCurrentItem(categories?.[0].products[0].sku)
150
- setMessage(undefined)
151
+ cmmc.setCurrentItem(categories[0].products[0].sku)
151
152
  }
152
153
  else {
153
154
  cmmc.setCurrentItem(undefined)
@@ -155,17 +156,15 @@ const useSkuAndFacetParams = (
155
156
  }
156
157
  }
157
158
  else {
158
- setMessage(PLEASE_SELECT_FACETS)
159
+ setMessage(message ?? PLEASE_SELECT_FACETS)
159
160
  }
160
161
  setLoading && setLoading(false)
161
162
  }, [level1 , level2])
162
163
 
163
164
  return {
164
- message,
165
165
  getMutator: encRef.current.usingSkuMode ?
166
166
  directMutator : paramsMutator
167
167
  }
168
-
169
168
  }
170
169
 
171
170
  export default useSkuAndFacetParams
@@ -1,17 +0,0 @@
1
- // Import the functions you need from the SDKs you need
2
- import { initializeApp, getApps } from "firebase/app"
3
-
4
- const firebaseConfig = {
5
- apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
6
- authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
7
- projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
8
- storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
9
- messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
10
- appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
11
- measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
12
- }
13
-
14
- // Initialize Firebase
15
- let firebase_app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]
16
-
17
- export default firebase_app
@@ -1,3 +0,0 @@
1
- import { default as getInstance } from './standalone/get-singleton'
2
-
3
- export default getInstance
@@ -1,28 +0,0 @@
1
- import type CommerceService from '../../commerce-service'
2
-
3
- import StandaloneCommerceService, {type StandaloneServiceOptions} from './service'
4
-
5
- import type { Category, FacetsDesc } from '../../../types'
6
-
7
- // https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
8
-
9
- let instance: CommerceService | undefined = undefined
10
-
11
- export default (categories: Category[], facets: FacetsDesc, options?: any): CommerceService => {
12
-
13
- const _instance = instance ??
14
- new StandaloneCommerceService(
15
- categories,
16
- facets,
17
- options ? (options as StandaloneServiceOptions) : {}
18
- )
19
- // For server side rendering always create a new store
20
- if (typeof window === "undefined") return _instance
21
-
22
- // Create the store once in the client
23
- if (!instance) {
24
- instance = _instance
25
- }
26
-
27
- return _instance
28
- }
package/service/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export type { default as CommerceService } from './commerce-service'
2
- export { useCommerce, CommerceServiceProvider } from './context'
3
- export { default as persistCart } from './utils'
4
-
package/service/utils.ts DELETED
@@ -1,37 +0,0 @@
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
- }