@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.
- 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 +6 -7
- 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} +75 -42
- package/tsconfig.json +0 -1
- package/types/category.ts +14 -0
- package/{service → types}/commerce-service.ts +6 -7
- 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 +49 -34
- 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
|
|
|
@@ -31,15 +30,15 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
|
|
|
31
30
|
...props
|
|
32
31
|
}) => {
|
|
33
32
|
|
|
34
|
-
const waiting = (): boolean => (isLoading
|
|
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
|
|
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('
|
|
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
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,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) =>
|
|
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, {
|
|
55
70
|
_selectedFacets : observable.deep,
|
|
56
|
-
|
|
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(
|
|
89
|
-
|
|
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.
|
|
145
|
+
return this._currentItem
|
|
95
146
|
}
|
|
96
147
|
|
|
97
148
|
get currentItem(): LineItem | undefined {
|
|
98
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
@@ -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):
|
|
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
|
-
|
|
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,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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
if (
|
|
96
|
-
cmmc.currentItem!.
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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 >
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
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
|
-
}
|