@hanzo/commerce 1.0.11 → 1.1.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.
@@ -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,6 +17,7 @@ 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,
@@ -30,7 +31,7 @@ const Cart: React.FC<PropsWithChildren & {
30
31
  const checkout = async () => {
31
32
  setLoadingCheckout(true)
32
33
  if (auth.user) {
33
- await persistCart(cmmc.cartItems, auth.user.email)
34
+ await cmmc.createOrder(auth.user.email)
34
35
  }
35
36
  router.push('/checkout')
36
37
  }
@@ -39,18 +40,18 @@ const Cart: React.FC<PropsWithChildren & {
39
40
  <div className={cn('border p-6 rounded-lg', className)}>
40
41
  {children}
41
42
  <div className='mt-2'>
42
- {!!children && <div className='h-[1px] w-pr-80 mx-auto bg-muted-3'/>}
43
+ {!!children && <div className='h-[1px] w-pr-80 mb-3 mx-auto bg-muted-3'/>}
43
44
  {cmmc.cartItems.length === 0 ? (
44
45
  <p className='text-center mt-4'>No items in cart</p>
45
46
  ) : (<>
46
47
  {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>
48
+ <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
49
  </>)}
49
50
  </div>
50
51
  {cmmc.cartItems.length > 0 && !hideCheckout && (
51
52
  <>
52
53
  {!auth.loggedIn ? (
53
- <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login</Button>
54
+ <Button size='sm' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login to checkout</Button>
54
55
  ) : (
55
56
  <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={checkout} disabled={loadingCheckout}>Checkout</Button>
56
57
  )}
@@ -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
 
@@ -31,15 +30,15 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
31
30
  ...props
32
31
  }) => {
33
32
 
34
- const waiting = (): boolean => (isLoading || !category)
33
+ const waiting = (): boolean => (isLoading) // keep for convenience for now
35
34
 
36
35
  const CategoryImage: React.FC<{ className?: string }> = ({ className = '' }) => {
37
36
 
38
37
  if (waiting()) {
39
38
  // deliberately not Skeleton to have a better overall pulse effect.
40
39
  return <div className={cn(
41
- 'bg-level-1 rounded-xl aspect-square ' +
42
- ' min-h-[100px] sm:min-h-[200px] lg:aspect-auto lg:h-[300px] lg:w-[200px] 2xl:w-auto 2xl:aspect-square',
40
+ 'bg-level-1 rounded-xl aspect-square w-full min-h-1 ', // +
41
+ //' min-h-[100px] sm:min-h-[200px] lg:aspect-auto 2xl:w-auto 2xl:aspect-square',
43
42
  className)} />
44
43
  }
45
44
 
@@ -49,7 +48,7 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
49
48
  aria-label='Placeholder'
50
49
  role='img'
51
50
  aria-roledescription='placeholder'
52
- className={cn('h-60 flex items-center justify-center', className)}
51
+ className={cn('w-full flex items-center justify-center aspect-square' , className)}
53
52
  >
54
53
  <Icons.barcode className='h-9 w-9 text-muted' aria-hidden='true' />
55
54
  </div>
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.11",
3
+ "version": "1.1.0",
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,60 @@
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
+ timestamp: FieldValue
27
+ items: ActualLineItemSnapshot[]
28
+ }
29
+
30
+ const createOrder = async (
31
+ email: string,
32
+ items: ActualLineItemSnapshot[],
33
+ options: {
34
+ dbName: string
35
+ ordersTable: string
36
+ }
37
+ ): Promise<{
38
+ success: boolean,
39
+ error: any
40
+ }> => {
41
+
42
+ let error: any | null = null
43
+ const ordersRef = collection(getDBInstance(options.dbName), options.ordersTable)
44
+
45
+ try {
46
+ await setDoc(doc(ordersRef, `${email}-${new Date().toISOString()}`), {
47
+ email,
48
+ timestamp: serverTimestamp(),
49
+ items,
50
+ } satisfies SavedOrder)
51
+ }
52
+ catch (e) {
53
+ console.error('Error writing item document: ', e)
54
+ error = e
55
+ }
56
+
57
+ return { success: !error, error }
58
+ }
59
+
60
+ export { createOrder }
@@ -7,22 +7,29 @@ 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 { createOrder as createOrderHelper } from './orders'
18
19
 
19
- import ObservableLineItem from './obs-line-item'
20
+ import ActualLineItem, { type ActualLineItemSnapshot } from './actual-line-item'
20
21
 
21
- export type StandaloneServiceOptions = {
22
+ type StandaloneServiceOptions = {
22
23
  levelZeroPrefix?: string
24
+ dbName: string
25
+ ordersTable: string
23
26
  }
24
27
 
25
- class StandaloneCommerceService
28
+ interface StandaloneServiceSnapshot {
29
+ items: ActualLineItemSnapshot[]
30
+ }
31
+
32
+ class StandaloneService
26
33
  implements CommerceService
27
34
  {
28
35
  private _categoryMap = new Map<string, Category>()
@@ -30,52 +37,68 @@ class StandaloneCommerceService
30
37
  private _selectedFacets: FacetsValue = {}
31
38
 
32
39
  private _options : StandaloneServiceOptions
33
-
34
- private _currentItemSku: string | undefined = undefined
40
+ private _currentItem: ActualLineItem | undefined = undefined
35
41
 
36
42
  constructor(
37
43
  categories: Category[],
38
44
  facets: FacetsDesc,
39
- options: StandaloneServiceOptions={}
45
+ options: StandaloneServiceOptions,
46
+ serviceSnapshot?: StandaloneServiceSnapshot,
40
47
  ) {
41
48
 
42
49
  this._facetsDesc = facets
43
50
  this._options = options
44
51
 
45
52
  categories.forEach((c) => {
46
- c.products = c.products.map((p) => (new ObservableLineItem(p)))
53
+ c.products = c.products.map((p) => {
54
+ if (serviceSnapshot) {
55
+ const itemSnapshot = serviceSnapshot.items.find((is) => (is.sku === p.sku))
56
+ if (itemSnapshot) {
57
+ return new ActualLineItem(p, itemSnapshot)
58
+ }
59
+ }
60
+ return new ActualLineItem(p)
61
+ })
47
62
  this._categoryMap.set(c.id, c)
48
63
  })
49
64
 
50
65
  makeObservable<
51
- StandaloneCommerceService,
66
+ StandaloneService,
52
67
  '_selectedFacets' |
53
- '_currentItemSku'
68
+ '_currentItem'
54
69
  >(this, {
55
70
  _selectedFacets : observable.deep,
56
- _currentItemSku: observable,
71
+ _currentItem: observable,
57
72
  })
58
73
 
59
74
  makeObservable(this, {
60
75
  cartItems: computed,
61
76
  cartTotal: computed,
62
77
  specifiedItems: computed,
78
+ specifiedCategories: computed,
63
79
  setCurrentItem: action,
64
80
  currentItem: computed,
65
81
  item: computed,
66
- specifiedCategories: computed,
67
82
  facetsValue: computed
68
83
  /* NOT setFacets. It implements it's action mechanism */
69
84
  })
85
+ }
70
86
 
87
+ async createOrder(email: string): Promise<void> {
88
+ const snapshot = this.takeSnapshot()
89
+ createOrderHelper(email, snapshot.items, this._options) // didn't want to have two levels of 'items'
71
90
  }
72
91
 
92
+ takeSnapshot = (): StandaloneServiceSnapshot => ({
93
+ items : (this.cartItems as ActualLineItem[]).map((it) => (it.takeSnapshot()))
94
+ })
95
+
73
96
  get cartItems(): LineItem[] {
74
97
  let result: LineItem[] = []
75
98
  this._categoryMap.forEach((cat) => {
76
99
  result = [...result, ...(cat.products as LineItem[]).filter((item) => (item.isInCart))]
77
100
  })
78
- return result
101
+ return result.sort((it1, it2) => ((it1 as ActualLineItem).timeAdded - (it2 as ActualLineItem).timeAdded))
79
102
  }
80
103
 
81
104
  get cartTotal(): number {
@@ -85,39 +108,45 @@ class StandaloneCommerceService
85
108
  )
86
109
  }
87
110
 
88
- setCurrentItem(sku: string | undefined): void {
89
- this._currentItemSku = sku
111
+ setCurrentItem(skuToFind: string | undefined): boolean {
112
+
113
+ if (skuToFind === undefined || skuToFind.length === 0) {
114
+ this._currentItem = undefined
115
+ return true
116
+ }
117
+ // self calling function
118
+ this._currentItem = ((): ActualLineItem | undefined => {
119
+
120
+ const categoriesTried: string[] = []
121
+ if (this.specifiedCategories && this.specifiedCategories.length > 0) {
122
+ for (let category of this.specifiedCategories) {
123
+ categoriesTried.push(category.id)
124
+ const foundItem = category.products.find((p) => (p.sku === skuToFind))
125
+ if (foundItem) {
126
+ return foundItem as ActualLineItem
127
+ }
128
+ }
129
+ }
130
+
131
+ let foundItem: ActualLineItem | undefined = undefined
132
+ this._categoryMap.forEach((category, categoryId) => {
133
+ if (foundItem) return
134
+ if (categoriesTried.includes(categoryId)) return
135
+ foundItem = category.products.find((p) => (p.sku === skuToFind)) as ActualLineItem | undefined
136
+ })
137
+ })();
138
+
139
+ return !!this._currentItem
90
140
  }
91
141
 
142
+
92
143
  /* ObsLineItemRef */
93
144
  get item(): LineItem | undefined {
94
- return this.currentItem
145
+ return this._currentItem
95
146
  }
96
147
 
97
148
  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
149
+ return this._currentItem
121
150
  }
122
151
 
123
152
  setFacets(sel: FacetsValue): Category[] {
@@ -147,7 +176,7 @@ class StandaloneCommerceService
147
176
  // 1-base, visiting two per iteration
148
177
  let current: string[] = this._selectedFacets[1]
149
178
  for (let i = 2; i <= keysStr.length; i++) {
150
- current = StandaloneCommerceService._visit(current, this._selectedFacets[i])
179
+ current = StandaloneService._visit(current, this._selectedFacets[i])
151
180
  }
152
181
  const prefix = this._options.levelZeroPrefix ?? ''
153
182
  return current.map((almostTheCatId) => (this._categoryMap.get(prefix + almostTheCatId)!))
@@ -199,4 +228,8 @@ class StandaloneCommerceService
199
228
  }
200
229
  }
201
230
 
202
- export default StandaloneCommerceService
231
+ export {
232
+ type StandaloneServiceOptions,
233
+ type StandaloneServiceSnapshot,
234
+ StandaloneService as default
235
+ }
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,8 @@ interface CommerceService extends ObsLineItemRef {
11
8
  get cartTotal(): number
12
9
  getCartCategorySubtotal(categoryId: string): number
13
10
 
11
+ createOrder(email: string): Promise<void>
12
+
14
13
  /**
15
14
  * Sets the tokens at each level supplied.
16
15
  * If a level is specifed as [], nothing will be specified.
@@ -30,7 +29,7 @@ interface CommerceService extends ObsLineItemRef {
30
29
  * "current" is unrelated to what is "specified",
31
30
  * ie, facets' values
32
31
  * */
33
- setCurrentItem(sku: string | undefined): void
32
+ setCurrentItem(sku: string | undefined): boolean // was valid sku and was set.
34
33
  /**
35
34
  * For convenience, so widgets can share state.
36
35
  * "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,6 +1,8 @@
1
+ "use client"
1
2
  import {
2
3
  useEffect,
3
4
  useRef,
5
+ useState,
4
6
  } from 'react'
5
7
  import { autorun, toJS, type IReactionDisposer } from 'mobx'
6
8
 
@@ -10,8 +12,10 @@ import {
10
12
  parseAsBoolean,
11
13
  } from 'next-usequerystate'
12
14
 
13
- import type { Category, FacetsValue, StringMutator } from '../types'
14
- import { useCommerce } from '../service'
15
+ import type { FacetsValue, StringMutator } from '../types'
16
+ import { useCommerce } from '../service/context'
17
+
18
+ const PLEASE_SELECT_FACETS = 'Please select an option from each group above.'
15
19
 
16
20
  interface GetMutator {
17
21
  (level: 1 | 2): StringMutator
@@ -24,11 +28,9 @@ const useSkuAndFacetParams = (
24
28
  const cmmc = useCommerce()
25
29
 
26
30
  const encRef = useRef<{
27
- category: Category | undefined
28
31
  usingSkuMode: boolean
29
32
  autoRunDisposer: IReactionDisposer | undefined
30
33
  }>({
31
- category: undefined,
32
34
  usingSkuMode: false,
33
35
  autoRunDisposer: undefined,
34
36
  })
@@ -40,12 +42,15 @@ const useSkuAndFacetParams = (
40
42
  parseAsString.withDefault('').withOptions({ clearOnDefault: true })
41
43
  )
42
44
 
43
- const [skuParam, setSkuParam] = useQueryState('sku')
45
+ const [skuParam, setSkuParam] = useQueryState('sku',
46
+ parseAsString.withDefault('').withOptions({ clearOnDefault: true })
47
+ )
48
+
44
49
  const [addParam, setAddParam] = useQueryState('add',
45
50
  parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
46
51
  )
47
52
 
48
- let message = ''
53
+ const [message, setMessage] = useState<string | undefined>(undefined)
49
54
 
50
55
  const directMutator: GetMutator = (level: 1 | 2): StringMutator => {
51
56
 
@@ -83,70 +88,80 @@ const useSkuAndFacetParams = (
83
88
 
84
89
  const setCurrentCategoryFromSku = (sku: string) => {
85
90
  const toks: string[] = sku.split('-')
86
- const categories = cmmc.setFacets({
91
+ cmmc.setFacets({
87
92
  1: [toks[1]],
88
93
  2: [toks[2]]
89
94
  })
90
- encRef.current.category = categories[0]
91
95
  }
92
96
 
93
97
  if (skuParam) {
94
- cmmc.setCurrentItem(skuParam)
95
- if (addParam && cmmc.currentItem!.quantity === 0) {
96
- cmmc.currentItem!.increment()
97
- setAddParam(false)
98
+ // true if a valid sku
99
+ if (cmmc.setCurrentItem(skuParam)) {
100
+ if (addParam && cmmc.currentItem!.quantity === 0) {
101
+ cmmc.currentItem!.increment()
102
+ setAddParam(false)
103
+ }
104
+ // Marks that we've been here so no need to
105
+ // create an autorun() instance twice.
106
+ if (!encRef.current.usingSkuMode) {
107
+ setCurrentCategoryFromSku(skuParam)
108
+ encRef.current.autoRunDisposer = autorun(() => {
109
+ if (cmmc.currentItem && cmmc.currentItem.sku !== skuParam) {
110
+ setSkuParam(cmmc.currentItem.sku)
111
+ }
112
+ })
113
+ }
114
+ setMessage(undefined)
115
+ encRef.current.usingSkuMode = true
98
116
  }
99
- // Marks that we've been here so no need to
100
- // create an autorun() instance twice.
101
- if (!encRef.current.usingSkuMode) {
102
- setCurrentCategoryFromSku(skuParam)
103
- encRef.current.autoRunDisposer = autorun(() => {
104
- if (cmmc.currentItem && cmmc.currentItem.sku !== skuParam) {
105
- setSkuParam(cmmc.currentItem.sku)
106
- }
107
- })
117
+ else {
118
+ setSkuParam('')
119
+ // if sent here w an invalid sku,
120
+ // it will effectively put us in facet params mode
121
+ setMessage('Invalid sku. ' + PLEASE_SELECT_FACETS)
108
122
  }
109
- encRef.current.usingSkuMode = true
110
- }
111
- else if (encRef.current.category) {
112
- //const catMutators = [{val: level1, set: setLevel1} , {val: level2, set: setLevel2}]
113
- cmmc.setCurrentItem(encRef.current.category.products[0].sku)
114
123
  }
124
+
115
125
  setLoading && setLoading(false)
116
- }, [skuParam, encRef.current.category])
126
+ }, [skuParam])
117
127
 
118
128
  // supposed to be when component unmounts
129
+ /*
119
130
  useEffect(() => (() => {
120
131
  if (encRef.current.autoRunDisposer) {
121
132
  //encRef.current.autoRunDisposer() // no idea why this seems to be called prematurely and so many times <shrug>
122
133
  }
123
134
  }), [])
135
+ */
124
136
 
125
137
 
126
138
  // Level Params Mode
127
139
  useEffect(() => {
128
140
 
129
141
  if (encRef.current.usingSkuMode) return
130
-
142
+
131
143
  const facets: FacetsValue = { }
132
144
  if (level1) { facets[1] = [level1] }
133
145
  if (level2) { facets[2] = [level2] }
146
+ //console.log(`LEV1: ${level1}, LEV2: ${level2}`)
134
147
  if (level1 && level2) {
135
148
  const categories = cmmc.setFacets(facets)
136
- if (categories.length > 1) {
137
- console.error("CAT", categories.map((c) => (c.title)))
138
- throw new Error ( "CategoryContents: More than one specified Category should never be possible with this UI!")
149
+ if (categories && categories.length > 0) {
150
+ cmmc.setCurrentItem(categories[0].products[0].sku)
151
+ setMessage(undefined)
152
+ }
153
+ else {
154
+ cmmc.setCurrentItem(undefined)
155
+ setMessage('Unrecognized facets. ' + PLEASE_SELECT_FACETS)
139
156
  }
140
- encRef.current.category = categories[0]
141
157
  }
142
158
  else {
143
- message = 'Please select an option from each group above.'
159
+ setMessage(PLEASE_SELECT_FACETS)
144
160
  }
145
161
  setLoading && setLoading(false)
146
162
  }, [level1 , level2])
147
163
 
148
164
  return {
149
- category: encRef.current.category,
150
165
  message,
151
166
  getMutator: encRef.current.usingSkuMode ?
152
167
  directMutator : paramsMutator
@@ -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
- }