@hanzo/commerce 1.0.12 → 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.
- package/components/cart.tsx +6 -5
- package/components/index.ts +1 -1
- package/components/select-category-and-item-widget.tsx +1 -2
- package/components/select-item-in-category-view.tsx +2 -3
- package/index.ts +4 -0
- package/package.json +1 -1
- package/service/context.tsx +12 -9
- package/service/impls/index.ts +3 -0
- package/service/{impl/standalone/obs-line-item.ts → impls/standalone/actual-line-item.ts} +43 -5
- package/service/impls/standalone/index.ts +54 -0
- package/service/impls/standalone/localStorage.ts +34 -0
- package/service/impls/standalone/orders/firebase-app.ts +14 -0
- package/service/impls/standalone/orders/index.ts +60 -0
- package/service/{impl/standalone/service.ts → impls/standalone/standalone-service.ts} +49 -23
- package/tsconfig.json +0 -1
- package/types/category.ts +14 -0
- package/{service → types}/commerce-service.ts +5 -6
- package/types/facet.ts +37 -0
- package/types/index.ts +6 -99
- package/types/line-item.ts +27 -0
- package/types/product.ts +15 -0
- package/types/string-mutator.ts +14 -0
- package/util/use-sku-and-facet-params.ts +4 -3
- package/service/firebase-config.ts +0 -17
- package/service/impl/index.ts +0 -3
- package/service/impl/standalone/get-singleton.ts +0 -28
- package/service/index.ts +0 -4
- package/service/utils.ts +0 -37
package/components/cart.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
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='
|
|
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
|
)}
|
package/components/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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
package/package.json
CHANGED
package/service/context.tsx
CHANGED
|
@@ -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 '
|
|
10
|
-
|
|
11
|
-
import getServiceSingleton from './
|
|
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
|
-
|
|
16
|
+
|
|
17
|
+
const useCommerce = (): CommerceService => {
|
|
17
18
|
return useContext(CommerceServiceContext) as CommerceService
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
const CommerceServiceProvider: React.FC<PropsWithChildren & {
|
|
21
22
|
productsByCategory: Category[]
|
|
22
23
|
facets: FacetsDesc
|
|
23
|
-
options?:
|
|
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
|
+
}
|
|
@@ -7,7 +7,18 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import type { Product, LineItem } from '../../../types'
|
|
9
9
|
|
|
10
|
-
|
|
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 {
|
|
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
|
|
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
|
|
18
|
+
import { createOrder as createOrderHelper } from './orders'
|
|
18
19
|
|
|
19
|
-
import
|
|
20
|
+
import ActualLineItem, { type ActualLineItemSnapshot } from './actual-line-item'
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
type StandaloneServiceOptions = {
|
|
22
23
|
levelZeroPrefix?: string
|
|
24
|
+
dbName: string
|
|
25
|
+
ordersTable: string
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
|
|
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,25 +37,33 @@ class StandaloneCommerceService
|
|
|
30
37
|
private _selectedFacets: FacetsValue = {}
|
|
31
38
|
|
|
32
39
|
private _options : StandaloneServiceOptions
|
|
33
|
-
|
|
34
|
-
private _currentItem: LineItem | 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) =>
|
|
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
|
-
|
|
66
|
+
StandaloneService,
|
|
52
67
|
'_selectedFacets' |
|
|
53
68
|
'_currentItem'
|
|
54
69
|
>(this, {
|
|
@@ -67,15 +82,23 @@ class StandaloneCommerceService
|
|
|
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 {
|
|
@@ -91,26 +114,25 @@ class StandaloneCommerceService
|
|
|
91
114
|
this._currentItem = undefined
|
|
92
115
|
return true
|
|
93
116
|
}
|
|
94
|
-
|
|
95
|
-
this._currentItem = (():
|
|
117
|
+
// self calling function
|
|
118
|
+
this._currentItem = ((): ActualLineItem | undefined => {
|
|
119
|
+
|
|
96
120
|
const categoriesTried: string[] = []
|
|
97
121
|
if (this.specifiedCategories && this.specifiedCategories.length > 0) {
|
|
98
|
-
|
|
99
|
-
for (let category of this.specifiedCategories) {
|
|
122
|
+
for (let category of this.specifiedCategories) {
|
|
100
123
|
categoriesTried.push(category.id)
|
|
101
|
-
const foundItem =
|
|
102
|
-
(category.products as LineItem[]).find((item) => (item.sku === skuToFind))
|
|
124
|
+
const foundItem = category.products.find((p) => (p.sku === skuToFind))
|
|
103
125
|
if (foundItem) {
|
|
104
|
-
return foundItem
|
|
126
|
+
return foundItem as ActualLineItem
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
let foundItem:
|
|
131
|
+
let foundItem: ActualLineItem | undefined = undefined
|
|
110
132
|
this._categoryMap.forEach((category, categoryId) => {
|
|
111
133
|
if (foundItem) return
|
|
112
134
|
if (categoriesTried.includes(categoryId)) return
|
|
113
|
-
foundItem =
|
|
135
|
+
foundItem = category.products.find((p) => (p.sku === skuToFind)) as ActualLineItem | undefined
|
|
114
136
|
})
|
|
115
137
|
})();
|
|
116
138
|
|
|
@@ -120,7 +142,7 @@ class StandaloneCommerceService
|
|
|
120
142
|
|
|
121
143
|
/* ObsLineItemRef */
|
|
122
144
|
get item(): LineItem | undefined {
|
|
123
|
-
return this.
|
|
145
|
+
return this._currentItem
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
get currentItem(): LineItem | undefined {
|
|
@@ -154,7 +176,7 @@ class StandaloneCommerceService
|
|
|
154
176
|
// 1-base, visiting two per iteration
|
|
155
177
|
let current: string[] = this._selectedFacets[1]
|
|
156
178
|
for (let i = 2; i <= keysStr.length; i++) {
|
|
157
|
-
current =
|
|
179
|
+
current = StandaloneService._visit(current, this._selectedFacets[i])
|
|
158
180
|
}
|
|
159
181
|
const prefix = this._options.levelZeroPrefix ?? ''
|
|
160
182
|
return current.map((almostTheCatId) => (this._categoryMap.get(prefix + almostTheCatId)!))
|
|
@@ -206,4 +228,8 @@ class StandaloneCommerceService
|
|
|
206
228
|
}
|
|
207
229
|
}
|
|
208
230
|
|
|
209
|
-
export
|
|
231
|
+
export {
|
|
232
|
+
type StandaloneServiceOptions,
|
|
233
|
+
type StandaloneServiceSnapshot,
|
|
234
|
+
StandaloneService as default
|
|
235
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
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.
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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}
|
package/types/product.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -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 {
|
|
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
|
|
|
@@ -146,7 +147,7 @@ const useSkuAndFacetParams = (
|
|
|
146
147
|
if (level1 && level2) {
|
|
147
148
|
const categories = cmmc.setFacets(facets)
|
|
148
149
|
if (categories && categories.length > 0) {
|
|
149
|
-
cmmc.setCurrentItem(categories
|
|
150
|
+
cmmc.setCurrentItem(categories[0].products[0].sku)
|
|
150
151
|
setMessage(undefined)
|
|
151
152
|
}
|
|
152
153
|
else {
|
|
@@ -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
|
package/service/impl/index.ts
DELETED
|
@@ -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
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
|
-
}
|