@hanzo/commerce 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blocks/components/commerce-cat-and-item-block.tsx +13 -0
- package/blocks/def/commerce-cat-and-item-block.ts +9 -0
- package/components/Icons.tsx +35 -0
- package/components/add-to-cart-widget.tsx +83 -0
- package/components/cart-icon.tsx +10 -0
- package/components/cart-line-item.tsx +47 -0
- package/components/cart.tsx +64 -0
- package/components/facet-toggles-widget/facet-image.tsx +58 -0
- package/components/facet-toggles-widget/index.tsx +82 -0
- package/components/facets-widget.tsx +46 -0
- package/components/index.ts +14 -0
- package/components/product-card.tsx +79 -0
- package/components/product-selection-mobile-picker.tsx +78 -0
- package/components/product-selection-radio-group.tsx +34 -0
- package/components/select-category-and-item-widget.tsx +84 -0
- package/components/select-item-in-category-view.tsx +200 -0
- package/package.json +50 -0
- package/service/commerce-service.ts +49 -0
- package/service/context.tsx +39 -0
- package/service/firebase-config.ts +17 -0
- package/service/impl/index.ts +3 -0
- package/service/impl/standalone/get-singleton.ts +28 -0
- package/service/impl/standalone/obs-line-item.ts +62 -0
- package/service/impl/standalone/service.ts +198 -0
- package/service/index.ts +4 -0
- package/service/utils.ts +37 -0
- package/tsconfig.json +11 -0
- package/types/README.md +2 -0
- package/types/index.ts +103 -0
- package/util/index.ts +25 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { computed } from 'mobx'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
|
|
6
|
+
import { ApplyTypography, ListBox } from '@hanzo/ui/primitives'
|
|
7
|
+
import { cn } from '@hanzo/ui/util'
|
|
8
|
+
|
|
9
|
+
import type { FacetValueDesc, FacetsValue, LineItem } from '../types'
|
|
10
|
+
import { useCommerce } from '../service'
|
|
11
|
+
import { formatPrice } from '../util'
|
|
12
|
+
|
|
13
|
+
import FacetTogglesWidget from './facet-toggles-widget'
|
|
14
|
+
|
|
15
|
+
const formatItem = (item: LineItem, withQuantity: boolean = false): string => (
|
|
16
|
+
`${item.titleAsOption}, ${formatPrice(item.price)}${(withQuantity && item.quantity > 0) ? ` (${item.quantity})` : ''}`
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const SelectCategoryAndItemWidget: React.FC<{
|
|
20
|
+
categoryLevel: number
|
|
21
|
+
parentLevelToken: string
|
|
22
|
+
categoryLevelValues: FacetValueDesc[]
|
|
23
|
+
className?: string
|
|
24
|
+
}> = observer(({
|
|
25
|
+
categoryLevel,
|
|
26
|
+
parentLevelToken,
|
|
27
|
+
categoryLevelValues,
|
|
28
|
+
className=''
|
|
29
|
+
}) => {
|
|
30
|
+
const comm = useCommerce()
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const facets: FacetsValue = {}
|
|
34
|
+
facets[categoryLevel - 1] = [parentLevelToken]
|
|
35
|
+
facets[categoryLevel] = [categoryLevelValues[0].value]
|
|
36
|
+
comm.setFacets(facets)
|
|
37
|
+
comm.setCurrentItem(comm.specifiedCategories[0].products[0].sku)
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const onFacetTokenChanged = (token: string): void => {
|
|
41
|
+
const facets: FacetsValue = {}
|
|
42
|
+
facets[categoryLevel - 1] = [parentLevelToken]
|
|
43
|
+
facets[categoryLevel] = [token]
|
|
44
|
+
comm.setFacets(facets)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const currentFacetToken = computed((): string | null => {
|
|
48
|
+
if (comm.specifiedCategories.length === 0) return null
|
|
49
|
+
const skuPath = comm.specifiedCategories[0].id
|
|
50
|
+
const skuPathTokens = skuPath.split('-')
|
|
51
|
+
return skuPathTokens.length > 0 ? skuPathTokens[skuPathTokens.length - 1] : null
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn('flex flex-col justify-start gap-4 items-start pt-3', className)}>
|
|
56
|
+
<FacetTogglesWidget
|
|
57
|
+
facetValues={categoryLevelValues}
|
|
58
|
+
mutator={{
|
|
59
|
+
val: currentFacetToken.get(),
|
|
60
|
+
set: onFacetTokenChanged
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
{comm.specifiedItems.length === 0 ? (
|
|
64
|
+
<ApplyTypography>
|
|
65
|
+
<h3>No Items</h3>
|
|
66
|
+
</ApplyTypography>
|
|
67
|
+
) : comm.specifiedItems.length === 1 ? (
|
|
68
|
+
<ApplyTypography>
|
|
69
|
+
<h4>{formatItem(comm.specifiedItems[0])}</h4>
|
|
70
|
+
</ApplyTypography>
|
|
71
|
+
) : (
|
|
72
|
+
<ListBox<string>
|
|
73
|
+
values={comm.specifiedItems.map((it) => (it.sku))}
|
|
74
|
+
labels={comm.specifiedItems.map((it) => (formatItem(it)))}
|
|
75
|
+
isEqual={(v1: string, v2: string) => (v1 === v2)}
|
|
76
|
+
value={comm.currentItem?.sku}
|
|
77
|
+
onValueChange={comm.setCurrentItem.bind(comm)}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export default SelectCategoryAndItemWidget
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useState } from 'react'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
import { Skeleton } from '@hanzo/ui/primitives'
|
|
8
|
+
|
|
9
|
+
import { useCommerce } from '../service'
|
|
10
|
+
import type { Category, LineItem, ObsLineItemRef } from '../types'
|
|
11
|
+
import { formatPrice } from '../util'
|
|
12
|
+
import { Icons } from './Icons'
|
|
13
|
+
|
|
14
|
+
import AddToCartWidget from './add-to-cart-widget'
|
|
15
|
+
import ProductSelectionRadioGroup from './product-selection-radio-group'
|
|
16
|
+
import ProductSelectionMobilePicker from './product-selection-mobile-picker'
|
|
17
|
+
|
|
18
|
+
const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> & {
|
|
19
|
+
category: Category
|
|
20
|
+
lineItemRef: ObsLineItemRef
|
|
21
|
+
handleItemSelected: (sku: string) => void
|
|
22
|
+
isLoading?: boolean
|
|
23
|
+
mobile?: boolean
|
|
24
|
+
}> = ({
|
|
25
|
+
category,
|
|
26
|
+
lineItemRef,
|
|
27
|
+
handleItemSelected,
|
|
28
|
+
className,
|
|
29
|
+
isLoading = false,
|
|
30
|
+
mobile = false,
|
|
31
|
+
...props
|
|
32
|
+
}) => {
|
|
33
|
+
|
|
34
|
+
const waiting = (): boolean => (isLoading || !category)
|
|
35
|
+
|
|
36
|
+
const CategoryImage: React.FC<{ className?: string }> = ({ className = '' }) => {
|
|
37
|
+
|
|
38
|
+
if (waiting()) {
|
|
39
|
+
// deliberately not Skeleton to have a better overall pulse effect.
|
|
40
|
+
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',
|
|
43
|
+
className)} />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!category.img) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
aria-label='Placeholder'
|
|
50
|
+
role='img'
|
|
51
|
+
aria-roledescription='placeholder'
|
|
52
|
+
className={cn('h-60 flex items-center justify-center', className)}
|
|
53
|
+
>
|
|
54
|
+
<Icons.barcode className='h-9 w-9 text-muted' aria-hidden='true' />
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={cn('flex flex-col justify-start', className)}>
|
|
61
|
+
<div className={cn('w-full border rounded-xl p-6 ')}>
|
|
62
|
+
<div className={cn('w-full aspect-square relative')}>
|
|
63
|
+
<Image
|
|
64
|
+
src={category.img!}
|
|
65
|
+
fill
|
|
66
|
+
sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, (max-width: 1200px) 50vw, 20vw"
|
|
67
|
+
alt={category.title}
|
|
68
|
+
className=''
|
|
69
|
+
loading='lazy'
|
|
70
|
+
style={{ objectFit: 'contain' }} />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const AvailableAmounts: React.FC<{ className?: string }> = observer(({ className = '' }) => {
|
|
78
|
+
|
|
79
|
+
const soleOption = !waiting() && category.products.length === 1
|
|
80
|
+
const mobilePicker = !waiting() && (mobile && category.products.length > 8)
|
|
81
|
+
|
|
82
|
+
return waiting() ? (
|
|
83
|
+
<Skeleton className={'min-h-[120px] w-pr-60 mx-auto ' + className} />
|
|
84
|
+
) : (
|
|
85
|
+
<div /* id='CV_AVAIL_AMOUNTS' */ className={cn(
|
|
86
|
+
'w-full md:w-auto flex flex-col justify-start items-center',
|
|
87
|
+
(soleOption ? 'gap-2' : mobilePicker ? 'gap-4' : 'gap-8'),
|
|
88
|
+
className
|
|
89
|
+
)}>
|
|
90
|
+
<div className='w-full flex flex-col justify-start items-center'>
|
|
91
|
+
<h6 className='text-center font-semibold'>{`Available size${soleOption ? '' : 's'}`}</h6>
|
|
92
|
+
<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-70') } />
|
|
93
|
+
</div>
|
|
94
|
+
{soleOption ? (
|
|
95
|
+
<p>{category.products[0].titleAsOption}</p>
|
|
96
|
+
) : ( mobilePicker ? (
|
|
97
|
+
<ProductSelectionMobilePicker
|
|
98
|
+
products={category.products}
|
|
99
|
+
selectedSku={lineItemRef.item?.sku ?? undefined}
|
|
100
|
+
onValueChange={handleItemSelected}
|
|
101
|
+
height={180}
|
|
102
|
+
itemHeight={30}
|
|
103
|
+
outerClx='mb-4'
|
|
104
|
+
/>
|
|
105
|
+
) : (
|
|
106
|
+
<ProductSelectionRadioGroup
|
|
107
|
+
products={category.products}
|
|
108
|
+
selectedSku={lineItemRef.item?.sku ?? undefined}
|
|
109
|
+
onValueChange={handleItemSelected}
|
|
110
|
+
groupClx='block xs:columns-2 xs:px-3 columns-3 gap-2 lg:columns-2 lg:gap-6 w-full lg:auto'
|
|
111
|
+
itemClx='flex flex-row gap-2 items-center mb-3 xs:mb-5'
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const AddToCartArea: React.FC<{ className?: string }> = observer(({ className = '' }) => (
|
|
120
|
+
(lineItemRef.item && !isLoading) ? (
|
|
121
|
+
<AddToCartWidget size='default' item={lineItemRef.item} className={className}/>
|
|
122
|
+
) : (
|
|
123
|
+
<div className={cn('h-6 w-12 invisible', className)} />
|
|
124
|
+
)
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
const TitleArea: React.FC<{ className?: string }> = observer(({ className = '' }) => (
|
|
128
|
+
waiting() ? (<Skeleton className={'h-12 w-pr-80 mx-auto ' + className} />) : (
|
|
129
|
+
|
|
130
|
+
<div className={cn('flex flex-col justify-start items-center mb-6', className)}>
|
|
131
|
+
<h3 className='text-lg lg:text-xl font-heading text-center'>
|
|
132
|
+
<span className='xs:inline sm:hidden md:inline lg:hidden'>
|
|
133
|
+
{category.title.split(', ').map((s, i) => (<p key={i}>{s}</p>))}
|
|
134
|
+
</span>
|
|
135
|
+
<span className='xs:hidden sm:inline md:hidden lg:inline'>
|
|
136
|
+
{category.title}
|
|
137
|
+
</span>
|
|
138
|
+
</h3>
|
|
139
|
+
{lineItemRef.item?.sku ? (
|
|
140
|
+
<h6 className='text-center font-semibold'>
|
|
141
|
+
{lineItemRef.item.titleAsOption + ': ' + formatPrice(lineItemRef.item.price)}
|
|
142
|
+
</h6>
|
|
143
|
+
) : ''}
|
|
144
|
+
</div>
|
|
145
|
+
)))
|
|
146
|
+
|
|
147
|
+
const Desc: React.FC<{ className?: string }> = ({ className = '' }) => (
|
|
148
|
+
waiting() ? (
|
|
149
|
+
<Skeleton className={'min-h-20 w-full grow mx-auto ' + className} />
|
|
150
|
+
) : (
|
|
151
|
+
<p className={cn('text-base lg:text-lg mb-6 xs:mb-0', className)}>{category.desc}</p>
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return mobile ? (
|
|
156
|
+
<div /* id='CV_OUTER' */
|
|
157
|
+
className={cn(
|
|
158
|
+
'w-full h-[calc(100svh-96px)] max-h-[700px] flex flex-col justify-between ' +
|
|
159
|
+
'items-stretch gap-[4vh] mt-[2vh] pb-[6vh]',
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
>
|
|
164
|
+
<div /* id='CV_TITLE_AND_IMAGE_ROW' */ className='flex flex-row justify-between items-start w-full'>
|
|
165
|
+
{waiting() ? ( <Skeleton className={'min-h-30 w-full '} /> ) : (<>
|
|
166
|
+
<CategoryImage className='w-pr-33' />
|
|
167
|
+
<TitleArea className='grow pt-3 mb-0' />
|
|
168
|
+
</>)}
|
|
169
|
+
</div>
|
|
170
|
+
<Desc className='' />
|
|
171
|
+
<AvailableAmounts className='mb-[3vh]' />
|
|
172
|
+
<AddToCartArea className='w-pr-70 mx-auto' />
|
|
173
|
+
</div>
|
|
174
|
+
) : (
|
|
175
|
+
<div /* id='CV_OUTERMOST' */>
|
|
176
|
+
<div /* id='CV_OUTER' */ className={cn('w-full flex flex-row justify-between items-stretch gap-6 sm:gap-4', className)} {...props}>
|
|
177
|
+
<div /* id='CV_IMAGE_COL' */ className={'relative ' + (!isLoading ? 'w-pr-33' : '')}>
|
|
178
|
+
<CategoryImage className='' />
|
|
179
|
+
</div>
|
|
180
|
+
<div /* id='CV_CONTENT_COL */ className='w-pr-66'>
|
|
181
|
+
<div /* id='CV_CONTENT' */ className={'flex flex-col gap-2.5 ' + (isLoading ? 'justify-between h-full' : '')}>
|
|
182
|
+
<TitleArea className='' />
|
|
183
|
+
<Desc className='' />
|
|
184
|
+
</div>
|
|
185
|
+
<div /* id='CV_CTA_AREA_BIG' */ className='hidden lg:flex p-4 flex-col justify-start items-center gap-6'>
|
|
186
|
+
<AvailableAmounts />
|
|
187
|
+
<AddToCartArea className='' />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div /* id='CV_CTA_AREA_COMPACT' */ className='lg:hidden flex p-4 flex-col justify-start items-center gap-6'>
|
|
192
|
+
<AvailableAmounts />
|
|
193
|
+
<AddToCartArea className='' />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
export default SelectItemInCategoryView
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanzo/commerce",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Library with shopping cart components.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org/",
|
|
7
|
+
"access": "public",
|
|
8
|
+
"scope": "@hanzo"
|
|
9
|
+
},
|
|
10
|
+
"author": "Hanzo AI, Inc.",
|
|
11
|
+
"license": "BSD-3-Clause",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/hanzoai/react-sdk.git",
|
|
15
|
+
"directory": "packages/commerce"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cart",
|
|
19
|
+
"hanzoai",
|
|
20
|
+
"hanzo"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"lat": "npm show @hanzo/commerce version",
|
|
24
|
+
"pub": "npm publish",
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"tc": "tsc",
|
|
27
|
+
"clean": "rm -rf dist && rm -rf node_modules"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@hookform/resolvers": "^3.3.2",
|
|
31
|
+
"lucide-react": "^0.307.0",
|
|
32
|
+
"react-mobile-picker": "^1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@hanzo/ui": "^1.0.8",
|
|
36
|
+
"@hanzo/auth": "^1.0.9",
|
|
37
|
+
"next": "^14.1.0",
|
|
38
|
+
"react": "^18.2.0",
|
|
39
|
+
"react-dom": "^18.2.0",
|
|
40
|
+
"mobx": "^6.12.0",
|
|
41
|
+
"mobx-react-lite": "^4.0.5",
|
|
42
|
+
"firebase": "^10.8.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^18.2.48",
|
|
46
|
+
"@types/react-dom": "^18.2.18",
|
|
47
|
+
"cross-fetch": "^4.0.0",
|
|
48
|
+
"typescript": "^5.3.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Category,
|
|
3
|
+
FacetsValue,
|
|
4
|
+
LineItem ,
|
|
5
|
+
ObsLineItemRef
|
|
6
|
+
} from '../types'
|
|
7
|
+
|
|
8
|
+
interface CommerceService extends ObsLineItemRef {
|
|
9
|
+
|
|
10
|
+
get cartItems(): LineItem[]
|
|
11
|
+
get cartTotal(): number
|
|
12
|
+
getCartCategorySubtotal(categoryId: string): number
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sets the tokens at each level supplied.
|
|
16
|
+
* If a level is specifed as [], nothing will be specified.
|
|
17
|
+
* If a level is missing (undefined), everything at that level is included
|
|
18
|
+
*
|
|
19
|
+
* This specifies one or more Category's, and all the LineItem's in them
|
|
20
|
+
*
|
|
21
|
+
* An empty value object specifies all Category's and all LineItem's,
|
|
22
|
+
* */
|
|
23
|
+
setFacets(value: FacetsValue): Category[]
|
|
24
|
+
get facetsValue(): FacetsValue // returns a copy
|
|
25
|
+
get specifiedItems(): LineItem[]
|
|
26
|
+
get specifiedCategories(): Category[]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* For convenience, so widgets can share state.
|
|
30
|
+
* "current" is unrelated to what is "specified",
|
|
31
|
+
* ie, facets' values
|
|
32
|
+
* */
|
|
33
|
+
setCurrentItem(sku: string | undefined): void
|
|
34
|
+
/**
|
|
35
|
+
* For convenience, so widgets can share state.
|
|
36
|
+
* "current" is unrelated to what is "specified",
|
|
37
|
+
* ie, facets' values
|
|
38
|
+
*
|
|
39
|
+
* note: for ObsLineItemRef, there is also
|
|
40
|
+
* get item(): LineItem | undefined
|
|
41
|
+
* which simply delegates to this function
|
|
42
|
+
* */
|
|
43
|
+
get currentItem(): LineItem | undefined
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
type CommerceService as default
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { createContext, useContext, useRef, type PropsWithChildren } from 'react'
|
|
3
|
+
|
|
4
|
+
// https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
|
|
5
|
+
import { enableStaticRendering } from 'mobx-react-lite'
|
|
6
|
+
enableStaticRendering(typeof window === "undefined")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import type CommerceService from './commerce-service'
|
|
10
|
+
|
|
11
|
+
import getServiceSingleton from './impl'
|
|
12
|
+
import type { Category, FacetsDesc } from '../types'
|
|
13
|
+
|
|
14
|
+
const CommerceServiceContext = createContext<CommerceService | undefined>(undefined)
|
|
15
|
+
|
|
16
|
+
export const useCommerce = (): CommerceService => {
|
|
17
|
+
return useContext(CommerceServiceContext) as CommerceService
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CommerceServiceProvider: React.FC<PropsWithChildren & {
|
|
21
|
+
productsByCategory: Category[]
|
|
22
|
+
facets: FacetsDesc
|
|
23
|
+
options?: any
|
|
24
|
+
}> = ({
|
|
25
|
+
children,
|
|
26
|
+
productsByCategory,
|
|
27
|
+
facets,
|
|
28
|
+
options
|
|
29
|
+
}) => {
|
|
30
|
+
|
|
31
|
+
const serviceRef = useRef<CommerceService>(getServiceSingleton(productsByCategory, facets, options))
|
|
32
|
+
return (
|
|
33
|
+
<CommerceServiceContext.Provider
|
|
34
|
+
value={serviceRef.current}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</CommerceServiceContext.Provider>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
action,
|
|
3
|
+
computed,
|
|
4
|
+
makeObservable,
|
|
5
|
+
observable,
|
|
6
|
+
} from 'mobx'
|
|
7
|
+
|
|
8
|
+
import type { Product, LineItem } from '../../../types'
|
|
9
|
+
|
|
10
|
+
class ObsLineItem implements LineItem {
|
|
11
|
+
|
|
12
|
+
qu: number = 0
|
|
13
|
+
|
|
14
|
+
id: string
|
|
15
|
+
sku: string
|
|
16
|
+
title: string
|
|
17
|
+
titleAsOption: string
|
|
18
|
+
categoryId: string
|
|
19
|
+
desc?: string
|
|
20
|
+
price: number
|
|
21
|
+
img?: string
|
|
22
|
+
|
|
23
|
+
constructor(prod: Product) {
|
|
24
|
+
this.id = prod.id
|
|
25
|
+
this.sku = prod.sku
|
|
26
|
+
this.title = prod.title
|
|
27
|
+
this.titleAsOption = prod.titleAsOption
|
|
28
|
+
this.categoryId = prod.categoryId
|
|
29
|
+
this.desc = prod.desc
|
|
30
|
+
this.price = prod.price
|
|
31
|
+
this.img = prod.img
|
|
32
|
+
|
|
33
|
+
makeObservable(this, {
|
|
34
|
+
qu: observable,
|
|
35
|
+
canDecrement: computed,
|
|
36
|
+
isInCart: computed,
|
|
37
|
+
|
|
38
|
+
increment: action,
|
|
39
|
+
decrement: action,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get canDecrement(): boolean { return this.qu > 0 }
|
|
44
|
+
get quantity(): number {return this.qu}
|
|
45
|
+
get isInCart(): boolean {return this.qu > 0}
|
|
46
|
+
|
|
47
|
+
increment(): void { this.qu++ }
|
|
48
|
+
|
|
49
|
+
decrement(): void {
|
|
50
|
+
if (this.canDecrement) {
|
|
51
|
+
this.qu--
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
inCategory(id: string): boolean {
|
|
56
|
+
// TODO: will break for level one (which is ok for lux, but not generally)
|
|
57
|
+
return this.sku.includes(`-${id}-`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default ObsLineItem
|