@luxfi/core 5.0.2 → 5.0.4
Sign up to get free protection for your applications and to get access to all the features.
- package/commerce/AUTO-GEN-bullion-by-family.json +34 -0
- package/commerce/EDIT-ME-bullion-market-prices.ts +12 -0
- package/commerce/assign-prices.ts +50 -0
- package/commerce/assign-videos-by-family-group.ts +14 -0
- package/commerce/bullion-price-1oz.ts +5 -0
- package/commerce/index.ts +18 -0
- package/commerce/lux-service-options.ts +6 -0
- package/components/chat-widget.tsx +4 -2
- package/components/commerce/buy-drawer/index.tsx +5 -149
- package/components/commerce/checkout-widget/const.ts +13 -0
- package/components/commerce/checkout-widget/index.tsx +86 -0
- package/components/commerce/checkout-widget/obs-string-set.ts +48 -0
- package/components/commerce/checkout-widget/use-anim-clx-set.ts +57 -0
- package/components/commerce/checkout-widget/use-lagging-item-ref.ts +30 -0
- package/components/index.ts +3 -4
- package/next/index.ts +1 -1
- package/package.json +5 -4
- package/root-layout/index.tsx +8 -7
- package/site-def/index.ts +1 -1
- package/tsconfig.json +1 -1
- package/types/chatbot-config.ts +7 -0
- package/types/chatbot-suggested-question.ts +7 -0
- package/types/commerce-config.ts +10 -0
- package/types/index.ts +5 -1
- package/{site-def → types}/site-def.ts +14 -6
- package/components/commerce/buy-drawer/checkout.svg +0 -4
- /package/next/{determine-device-mw.ts → middleware/determine-device-mw.ts} +0 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"id": "LXM-AG-B",
|
4
|
+
"title": "Minted Bar",
|
5
|
+
"parentTitle": "Lux Silver",
|
6
|
+
"desc": "Get unprecedented access to silver with 1:1 asset-backed Lux Silver NFTs, sovereign ownership of physical silver without management fees, and mine-direct discount pricing.",
|
7
|
+
"img": {
|
8
|
+
"src": "/assets/commerce/silver/product/silver-bar-pt-800x800.png",
|
9
|
+
"dim": {
|
10
|
+
"w": 800,
|
11
|
+
"h": 800
|
12
|
+
}
|
13
|
+
},
|
14
|
+
"products": [
|
15
|
+
{
|
16
|
+
"id": "fce17569-a86c-4f69-bc61-ab435ed0c46f",
|
17
|
+
"sku": "LXM-AG-B-1_OZ",
|
18
|
+
"fullTitle": "Lux Silver, 1oz Minted Bar",
|
19
|
+
"optionLabel": "1oz",
|
20
|
+
"familyTitle": "Minted Bar",
|
21
|
+
"familyId": "LXM-AG-B",
|
22
|
+
"desc": "Get unprecedented access to silver with 1:1 asset-backed Lux Silver NFTs, sovereign ownership of physical silver without management fees, and mine-direct discount pricing.",
|
23
|
+
"price": 0,
|
24
|
+
"img": {
|
25
|
+
"src": "/assets/commerce/silver/product/silver-bar-pt-800x800.png",
|
26
|
+
"dim": {
|
27
|
+
"w": 800,
|
28
|
+
"h": 800
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
]
|
33
|
+
}
|
34
|
+
]
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import type { Family } from '@hanzo/commerce/types'
|
2
|
+
|
3
|
+
import prices from './EDIT-ME-bullion-market-prices'
|
4
|
+
|
5
|
+
const sep = {
|
6
|
+
tok: '-',
|
7
|
+
subTok: '_',
|
8
|
+
decimal: '.'
|
9
|
+
}
|
10
|
+
|
11
|
+
const GRAMS_PER_OZ = 28.3495
|
12
|
+
|
13
|
+
const tree: any = {}
|
14
|
+
for (let key in prices) {
|
15
|
+
const values = prices[key as keyof typeof prices]
|
16
|
+
tree[key] = {
|
17
|
+
oz: values.market1oz * (1 - values.discount),
|
18
|
+
g: values.market1oz * (1 - values.discount) / GRAMS_PER_OZ
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
// LXB-AU-B-1_OZ, LXB-AU-B-2.5_g
|
23
|
+
const priceFromSKU = (
|
24
|
+
sku: string
|
25
|
+
) => {
|
26
|
+
const tokens = sku.split(sep.tok)
|
27
|
+
const type_ = tokens[1].toLowerCase()
|
28
|
+
|
29
|
+
const quanAndUnit = tokens[tokens.length - 1]
|
30
|
+
const quanAndUnitToks = quanAndUnit.split(sep.subTok)
|
31
|
+
let quantity = quanAndUnitToks[0].includes(sep.decimal) ? parseFloat(quanAndUnitToks[0].split(sep.decimal).join('.')) : parseInt(quanAndUnitToks[0])
|
32
|
+
let unit = quanAndUnitToks[1].toLowerCase()
|
33
|
+
if (unit === 'kg') {
|
34
|
+
quantity *= 1000
|
35
|
+
unit = 'g'
|
36
|
+
}
|
37
|
+
else if (unit === 'lb') {
|
38
|
+
quantity *= 16
|
39
|
+
unit = 'oz'
|
40
|
+
}
|
41
|
+
|
42
|
+
return tree[type_][unit] * quantity
|
43
|
+
}
|
44
|
+
|
45
|
+
export default (c: Family): Family => {
|
46
|
+
for (let prod of c.products) {
|
47
|
+
prod.price = priceFromSKU(prod.sku)
|
48
|
+
}
|
49
|
+
return c
|
50
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import type { VideoDef } from '@hanzo/ui/types'
|
2
|
+
import type { Family } from '@hanzo/commerce/types'
|
3
|
+
|
4
|
+
export default (fam: Family, map: Map<string, VideoDef>): Family => {
|
5
|
+
if (fam.parentTitle) {
|
6
|
+
for (let prod of fam.products) {
|
7
|
+
const video = map.get(fam.parentTitle)
|
8
|
+
if (video) {
|
9
|
+
prod.video = video
|
10
|
+
}
|
11
|
+
}
|
12
|
+
}
|
13
|
+
return fam
|
14
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import type { VideoDef } from '@hanzo/ui/types'
|
2
|
+
|
3
|
+
import bullionByFamily from './AUTO-GEN-bullion-by-family.json'
|
4
|
+
|
5
|
+
import assignPrices from './assign-prices'
|
6
|
+
import assignVideosByFamilyGroup from './assign-videos-by-family-group'
|
7
|
+
|
8
|
+
export const getBullionFamilies = (videoMap: Map<string, VideoDef>) => (
|
9
|
+
bullionByFamily.map(
|
10
|
+
(fam) => (assignPrices(fam))).map(
|
11
|
+
(fam) => (assignVideosByFamilyGroup(fam, videoMap)
|
12
|
+
)
|
13
|
+
)
|
14
|
+
)
|
15
|
+
|
16
|
+
export { default as serviceOptions } from './lux-service-options'
|
17
|
+
export { default as bullionPrice1oz } from './bullion-price-1oz'
|
18
|
+
|
@@ -2,9 +2,10 @@
|
|
2
2
|
import React from 'react'
|
3
3
|
|
4
4
|
import { Button, Card } from '@hanzo/ui/primitives'
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
5
6
|
|
6
7
|
import LuxLogo from './icons/lux-logo'
|
7
|
-
import {
|
8
|
+
import type { ChatbotSuggestedQuestion } from '../types'
|
8
9
|
|
9
10
|
const ChatWidget: React.FC<{
|
10
11
|
title: string,
|
@@ -12,6 +13,7 @@ const ChatWidget: React.FC<{
|
|
12
13
|
subtitle?: string,
|
13
14
|
question?: string,
|
14
15
|
/*
|
16
|
+
ChatBotSuggestQuestion.icon
|
15
17
|
Currently supports these icons from remix icons (https://remixicon.com/):
|
16
18
|
GlobalLineIcon,
|
17
19
|
ShieldFlashLineIcon,
|
@@ -19,7 +21,7 @@ const ChatWidget: React.FC<{
|
|
19
21
|
GroupLineIcon,
|
20
22
|
QuestionnaireLineIcon
|
21
23
|
*/
|
22
|
-
suggestedQuestions?:
|
24
|
+
suggestedQuestions?: ChatbotSuggestedQuestion[]
|
23
25
|
}> = ({
|
24
26
|
title,
|
25
27
|
chatbotUrl,
|
@@ -1,109 +1,17 @@
|
|
1
1
|
'use client'
|
2
|
-
import React
|
3
|
-
import {
|
4
|
-
import { useRouter, usePathname } from 'next/navigation'
|
5
|
-
import { ObservableSet, observable, reaction, type IReactionDisposer, runInAction} from 'mobx'
|
2
|
+
import React from 'react'
|
3
|
+
import { useRouter } from 'next/navigation'
|
6
4
|
import { observer } from 'mobx-react-lite'
|
7
5
|
|
8
|
-
import {
|
9
|
-
import { Image } from '@hanzo/ui/primitives'
|
10
|
-
|
11
|
-
import { useCommerce, useCommerceUI, CarouselBuyCard, LineItemRef } from '@hanzo/commerce'
|
12
|
-
import type { LineItem } from '@hanzo/commerce/types'
|
6
|
+
import { useCommerceUI, CarouselBuyCard } from '@hanzo/commerce'
|
13
7
|
|
14
8
|
import CommerceDrawer from './drawer'
|
15
9
|
import CheckoutButton from '../checkout-button'
|
16
10
|
|
17
|
-
const DEF_IMG_CONSTRAINT={w: 40, h: 24}
|
18
|
-
const CO_ANIM_DURATION = 400
|
19
|
-
const CO_ANIM_TIMING_FN = 'cubic-bezier(0.4, 0, 0.2, 1)'
|
20
|
-
const CO_WIDGET_W_CLX = {
|
21
|
-
checkout: 'w-pr-40',
|
22
|
-
itemInfo: 'w-pr-60'
|
23
|
-
}
|
24
|
-
|
25
|
-
const CO_WIDGET_SHADOW_STYLE = {
|
26
|
-
border: '1px solid rgb(100 100 100)',
|
27
|
-
boxShadow: '2px 4px 4px -3px rgb(125 125 125 / 0.7), 4px -4px 8px -4px rgb(125 125 125 / 0.7)'
|
28
|
-
}
|
29
|
-
|
30
11
|
const CommerceUIComponent: React.FC = observer(() => {
|
31
12
|
|
32
13
|
const ui = useCommerceUI()
|
33
|
-
const cmmc = useCommerce()
|
34
14
|
const router = useRouter()
|
35
|
-
const isCheckout = usePathname() === '/checkout'
|
36
|
-
|
37
|
-
const animClxRef = useRef<ObservableSet<string>>(observable.set(new Set<string>(
|
38
|
-
(cmmc.cartEmpty || ui.buyOptionsSkuPath || isCheckout) ? ['hidden'] : []
|
39
|
-
)))
|
40
|
-
|
41
|
-
// a ref that is synced to ui.activeItem, but persists for CO_ANIM_DURATION longer
|
42
|
-
// so ui does not jump while animating "out"
|
43
|
-
const laggingActiveItemRef = useRef<LineItemRef>(new LineItemRef())
|
44
|
-
const disposersRef = useRef<IReactionDisposer[]>([])
|
45
|
-
|
46
|
-
useEffect(() => {
|
47
|
-
disposersRef.current.push(
|
48
|
-
reaction(
|
49
|
-
() => (ui.activeItem),
|
50
|
-
(item: LineItem | undefined) => {
|
51
|
-
if (item) {
|
52
|
-
laggingActiveItemRef.current.set(item)
|
53
|
-
}
|
54
|
-
else {
|
55
|
-
setTimeout(() => { laggingActiveItemRef.current.set(undefined) }, CO_ANIM_DURATION)
|
56
|
-
}
|
57
|
-
},
|
58
|
-
{equals: (val, prev) => (val?.sku === prev?.sku)}
|
59
|
-
)
|
60
|
-
)
|
61
|
-
|
62
|
-
disposersRef.current.push(
|
63
|
-
reaction(
|
64
|
-
() => ({
|
65
|
-
microOpen: !(cmmc.cartEmpty || !!ui.buyOptionsSkuPath || isCheckout),
|
66
|
-
buyOpen: !!ui.buyOptionsSkuPath
|
67
|
-
}),
|
68
|
-
(val, prev) => {
|
69
|
-
|
70
|
-
runInAction(() => {
|
71
|
-
if (!val.microOpen && prev.microOpen) {
|
72
|
-
animClxRef.current.add('checkout-widget-disappears')
|
73
|
-
}
|
74
|
-
else if (val.microOpen && !prev.microOpen) {
|
75
|
-
animClxRef.current.delete('hidden')
|
76
|
-
animClxRef.current.add('checkout-widget-appears')
|
77
|
-
}
|
78
|
-
if (!val.buyOpen && prev.buyOpen) {
|
79
|
-
animClxRef.current.add('checkout-widget-appears-after-buy-drawer-closes')
|
80
|
-
}
|
81
|
-
else {
|
82
|
-
animClxRef.current.delete('checkout-widget-appears-after-buy-drawer-closes')
|
83
|
-
}
|
84
|
-
})
|
85
|
-
|
86
|
-
setTimeout(() => {runInAction(() => {
|
87
|
-
animClxRef.current.delete('checkout-widget-appears')
|
88
|
-
animClxRef.current.delete('checkout-widget-appears-after-buy-drawer-closes')
|
89
|
-
if (animClxRef.current.has('checkout-widget-disappears') ) {
|
90
|
-
animClxRef.current.delete('checkout-widget-disappears')
|
91
|
-
animClxRef.current.add('hidden')
|
92
|
-
}
|
93
|
-
})}, 800)
|
94
|
-
},
|
95
|
-
{equals: (val, prev) => (
|
96
|
-
val.microOpen === prev.microOpen
|
97
|
-
&&
|
98
|
-
val.buyOpen === prev.buyOpen
|
99
|
-
)}
|
100
|
-
)
|
101
|
-
)
|
102
|
-
|
103
|
-
return () => {
|
104
|
-
disposersRef.current.forEach((d) => {d()})
|
105
|
-
}
|
106
|
-
}, [])
|
107
15
|
|
108
16
|
const handleCheckout = () => {
|
109
17
|
router.push('/checkout')
|
@@ -116,11 +24,10 @@ const CommerceUIComponent: React.FC = observer(() => {
|
|
116
24
|
}
|
117
25
|
}
|
118
26
|
|
119
|
-
return (
|
27
|
+
return (
|
120
28
|
<CommerceDrawer
|
121
29
|
open={!!ui.buyOptionsSkuPath}
|
122
30
|
setOpen={reallyOnlyCloseDrawer}
|
123
|
-
modal={true}
|
124
31
|
drawerClx={'w-full md:max-w-[550px] md:mx-auto lg:max-w-[50vw]'}
|
125
32
|
>
|
126
33
|
<CarouselBuyCard
|
@@ -133,58 +40,7 @@ const CommerceUIComponent: React.FC = observer(() => {
|
|
133
40
|
selectorClx='max-w-[475px]'
|
134
41
|
/>
|
135
42
|
</CommerceDrawer>
|
136
|
-
|
137
|
-
<div
|
138
|
-
className={cn(
|
139
|
-
'min-w-[160px] sm:max-w-[320px] w-[calc(100%-72px)] ml-2 !h-10',
|
140
|
-
'z-below-modal-2 fixed bottom-[20px] left-0 right-0',
|
141
|
-
'rounded-lg bg-background',
|
142
|
-
'flex',
|
143
|
-
ui.activeItem ? 'gap-2' : '',
|
144
|
-
Array.from(animClxRef.current).join(' ')
|
145
|
-
)}
|
146
|
-
style={laggingActiveItemRef.current.item ? {} : CO_WIDGET_SHADOW_STYLE}
|
147
|
-
>
|
148
|
-
<div
|
149
|
-
className={cn(
|
150
|
-
'flex flex-row justify-between items-center',
|
151
|
-
ui.activeItem ? CO_WIDGET_W_CLX.itemInfo : 'w-0',
|
152
|
-
laggingActiveItemRef.current.item ? 'px-3 border rounded-lg border-muted-3' : ''
|
153
|
-
)}
|
154
|
-
style={{
|
155
|
-
transitionProperty: 'width',
|
156
|
-
transitionTimingFunction: CO_ANIM_TIMING_FN,
|
157
|
-
transitionDuration: `${CO_ANIM_DURATION}ms`
|
158
|
-
}}
|
159
|
-
>
|
160
|
-
{laggingActiveItemRef.current.item?.img ? (
|
161
|
-
<Image def={laggingActiveItemRef.current.item.img} constrainTo={DEF_IMG_CONSTRAINT} preload className='grow-0 shrink-0'/>
|
162
|
-
) : ( // placeholder so things align
|
163
|
-
<div style={{height: DEF_IMG_CONSTRAINT.h, width: DEF_IMG_CONSTRAINT.w}} className='bg-level-3 grow-0 shrink-0'/>
|
164
|
-
)}
|
165
|
-
|
166
|
-
<div className='text-muted grow ml-1'>
|
167
|
-
{laggingActiveItemRef.current.item && (<>
|
168
|
-
<p className='whitespace-nowrap text-sm'>{laggingActiveItemRef.current.item.title}</p>
|
169
|
-
<p className='whitespace-nowrap text-xxs' >recently added...</p>
|
170
|
-
</>)}
|
171
|
-
</div>
|
172
|
-
</div>
|
173
|
-
<CheckoutButton
|
174
|
-
handleCheckout={handleCheckout}
|
175
|
-
centerText={!!!ui.activeItem}
|
176
|
-
variant='primary' rounded='lg'
|
177
|
-
className={cn(ui.activeItem ? CO_WIDGET_W_CLX.checkout : 'w-full')}
|
178
|
-
style={{
|
179
|
-
transitionProperty: 'width',
|
180
|
-
transitionTimingFunction: CO_ANIM_TIMING_FN,
|
181
|
-
transitionDuration: `${CO_ANIM_DURATION}ms`
|
182
|
-
}}
|
183
|
-
/>
|
184
|
-
</div>,
|
185
|
-
globalThis?.document?.body
|
186
|
-
)}
|
187
|
-
</>)
|
43
|
+
)
|
188
44
|
})
|
189
45
|
|
190
46
|
export default CommerceUIComponent
|
@@ -0,0 +1,13 @@
|
|
1
|
+
export default {
|
2
|
+
itemImgConstraint: { w: 40, h: 24 },
|
3
|
+
animDurationMs: 400,
|
4
|
+
animTimingFn: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
5
|
+
compWidthClx: {
|
6
|
+
checkout: 'w-pr-40',
|
7
|
+
itemInfo: 'w-pr-60'
|
8
|
+
},
|
9
|
+
shadowStyle: {
|
10
|
+
border: '1px solid rgb(100 100 100)',
|
11
|
+
boxShadow: '2px 4px 4px -3px rgb(125 125 125 / 0.7), 4px -4px 8px -4px rgb(125 125 125 / 0.7)'
|
12
|
+
}
|
13
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
'use client'
|
2
|
+
import React from 'react'
|
3
|
+
import { createPortal } from 'react-dom'
|
4
|
+
import { usePathname, useRouter } from 'next/navigation'
|
5
|
+
import { observer } from 'mobx-react-lite'
|
6
|
+
|
7
|
+
import { cn } from '@hanzo/ui/util'
|
8
|
+
import { Image } from '@hanzo/ui/primitives'
|
9
|
+
|
10
|
+
import { useCommerceUI } from '@hanzo/commerce'
|
11
|
+
|
12
|
+
import CheckoutButton from '../checkout-button'
|
13
|
+
import useAnimationClxSet from './use-anim-clx-set'
|
14
|
+
import useLaggingItemRef from './use-lagging-item-ref'
|
15
|
+
import CONST from './const'
|
16
|
+
|
17
|
+
const CheckoutWidget: React.FC<{
|
18
|
+
clx?: string
|
19
|
+
}> = observer(({
|
20
|
+
clx=''
|
21
|
+
}) => {
|
22
|
+
|
23
|
+
const router = useRouter()
|
24
|
+
|
25
|
+
const isCheckout = usePathname() === '/checkout'
|
26
|
+
const clxSet = useAnimationClxSet(isCheckout)
|
27
|
+
|
28
|
+
const itemRef = useCommerceUI()
|
29
|
+
const laggingRef = useLaggingItemRef(itemRef, CONST.animDurationMs)
|
30
|
+
|
31
|
+
const handleCheckout = () => { router.push('/checkout')}
|
32
|
+
|
33
|
+
return globalThis?.document?.body && createPortal(
|
34
|
+
(<div
|
35
|
+
className={cn(
|
36
|
+
'min-w-[160px] sm:max-w-[320px] w-[calc(100%-72px)] ml-2 !h-10',
|
37
|
+
'z-below-modal-2 fixed bottom-[20px] left-0 right-0',
|
38
|
+
'rounded-lg bg-background',
|
39
|
+
'flex',
|
40
|
+
itemRef.item ? 'gap-2' : '',
|
41
|
+
clxSet.asArray.join(' ')
|
42
|
+
)}
|
43
|
+
style={laggingRef.item ? {} : CONST.shadowStyle}
|
44
|
+
>
|
45
|
+
<div
|
46
|
+
className={cn(
|
47
|
+
'flex flex-row justify-between items-center',
|
48
|
+
itemRef.item ? CONST.compWidthClx.itemInfo : 'w-0',
|
49
|
+
laggingRef.item ? 'px-3 border rounded-lg border-muted-3' : ''
|
50
|
+
)}
|
51
|
+
style={{
|
52
|
+
transitionProperty: 'width',
|
53
|
+
transitionTimingFunction: CONST.animTimingFn,
|
54
|
+
transitionDuration: `${CONST.animDurationMs}ms`
|
55
|
+
}}
|
56
|
+
>
|
57
|
+
{laggingRef.item?.img ? (
|
58
|
+
<Image def={laggingRef.item.img} constrainTo={CONST.itemImgConstraint} preload className='grow-0 shrink-0'/>
|
59
|
+
) : ( // placeholder so things align
|
60
|
+
<div style={{height: CONST.itemImgConstraint.h, width: CONST.itemImgConstraint.w}} className='bg-level-3 grow-0 shrink-0'/>
|
61
|
+
)}
|
62
|
+
|
63
|
+
<div className='text-muted grow ml-1'>
|
64
|
+
{laggingRef.item && (<>
|
65
|
+
<p className='whitespace-nowrap text-sm'>{laggingRef.item.title}</p>
|
66
|
+
<p className='whitespace-nowrap text-xxs' >recently added...</p>
|
67
|
+
</>)}
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
<CheckoutButton
|
71
|
+
handleCheckout={handleCheckout}
|
72
|
+
centerText={!!!itemRef.item}
|
73
|
+
variant='primary' rounded='lg'
|
74
|
+
className={cn(itemRef.item ? CONST.compWidthClx.checkout : 'w-full')}
|
75
|
+
style={{
|
76
|
+
transitionProperty: 'width',
|
77
|
+
transitionTimingFunction: CONST.animTimingFn,
|
78
|
+
transitionDuration: `${CONST.animDurationMs}ms`
|
79
|
+
}}
|
80
|
+
/>
|
81
|
+
</div>),
|
82
|
+
globalThis?.document?.body
|
83
|
+
)
|
84
|
+
})
|
85
|
+
|
86
|
+
export default CheckoutWidget
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import {
|
2
|
+
action,
|
3
|
+
computed,
|
4
|
+
makeObservable,
|
5
|
+
observable
|
6
|
+
} from 'mobx'
|
7
|
+
|
8
|
+
class ObsStringSet {
|
9
|
+
|
10
|
+
private _set = observable.set(new Set<string>())
|
11
|
+
|
12
|
+
constructor(initial: string[] = []) {
|
13
|
+
initial.forEach((el) => {this._set.add(el)})
|
14
|
+
makeObservable(this, {
|
15
|
+
add: action,
|
16
|
+
remove: action,
|
17
|
+
asArray: computed
|
18
|
+
})
|
19
|
+
}
|
20
|
+
|
21
|
+
add = (v: string | string[]): void => {
|
22
|
+
if (Array.isArray(v)) {
|
23
|
+
v.forEach((el) => {this._set.add(el)})
|
24
|
+
}
|
25
|
+
else {
|
26
|
+
this._set.add(v)
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
remove = (v: string | string[]): void => {
|
31
|
+
if (Array.isArray(v)) {
|
32
|
+
v.forEach((el) => {this._set.delete(el)})
|
33
|
+
}
|
34
|
+
else {
|
35
|
+
this._set.delete(v)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
has = (v: string): boolean => (
|
40
|
+
this._set.has(v)
|
41
|
+
)
|
42
|
+
|
43
|
+
get asArray() {
|
44
|
+
return Array.from(this._set)
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
export default ObsStringSet
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
2
|
+
import { reaction, runInAction} from 'mobx'
|
3
|
+
|
4
|
+
import ObsStringSet from './obs-string-set'
|
5
|
+
import { useCommerce, useCommerceUI } from '@hanzo/commerce'
|
6
|
+
|
7
|
+
export default (isCheckout: boolean): ObsStringSet => {
|
8
|
+
|
9
|
+
const ui = useCommerceUI()
|
10
|
+
const cmmc = useCommerce()
|
11
|
+
|
12
|
+
const clxSetRef = useRef<ObsStringSet>(new ObsStringSet(
|
13
|
+
(cmmc.cartEmpty || ui.buyOptionsSkuPath || isCheckout) ? ['hidden'] : []
|
14
|
+
))
|
15
|
+
|
16
|
+
useEffect(() => (
|
17
|
+
reaction(
|
18
|
+
() => ({
|
19
|
+
microOpen: !(cmmc.cartEmpty || !!ui.buyOptionsSkuPath || isCheckout),
|
20
|
+
buyOpen: !!ui.buyOptionsSkuPath
|
21
|
+
}),
|
22
|
+
(val, prev) => {
|
23
|
+
|
24
|
+
runInAction(() => {
|
25
|
+
if (!val.microOpen && prev.microOpen) {
|
26
|
+
clxSetRef.current.add('checkout-widget-disappears')
|
27
|
+
}
|
28
|
+
else if (val.microOpen && !prev.microOpen) {
|
29
|
+
clxSetRef.current.remove('hidden')
|
30
|
+
clxSetRef.current.add('checkout-widget-appears')
|
31
|
+
}
|
32
|
+
if (!val.buyOpen && prev.buyOpen) {
|
33
|
+
clxSetRef.current.add('checkout-widget-appears-after-buy-drawer-closes')
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
clxSetRef.current.remove('checkout-widget-appears-after-buy-drawer-closes')
|
37
|
+
}
|
38
|
+
})
|
39
|
+
|
40
|
+
setTimeout(() => {runInAction(() => {
|
41
|
+
clxSetRef.current.remove(['checkout-widget-appears', 'checkout-widget-appears-after-buy-drawer-closes'])
|
42
|
+
if (clxSetRef.current.has('checkout-widget-disappears') ) {
|
43
|
+
clxSetRef.current.remove('checkout-widget-disappears')
|
44
|
+
clxSetRef.current.add('hidden')
|
45
|
+
}
|
46
|
+
})}, 800)
|
47
|
+
},
|
48
|
+
{equals: (val, prev) => (
|
49
|
+
val.microOpen === prev.microOpen
|
50
|
+
&&
|
51
|
+
val.buyOpen === prev.buyOpen
|
52
|
+
)}
|
53
|
+
)
|
54
|
+
), [isCheckout])
|
55
|
+
|
56
|
+
return clxSetRef.current
|
57
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
2
|
+
import { reaction } from 'mobx'
|
3
|
+
|
4
|
+
import type { LineItem, ObsLineItemRef } from "@hanzo/commerce/types"
|
5
|
+
import { LineItemRef } from '@hanzo/commerce'
|
6
|
+
|
7
|
+
export default (orig: ObsLineItemRef, lagMs: number): ObsLineItemRef => {
|
8
|
+
|
9
|
+
// a ref that is synced to 'orig', but persists for lagMs longer
|
10
|
+
// so ui does not jump while animating out.
|
11
|
+
// (Fascilitates for start and end states in animation)
|
12
|
+
const laggingRef = useRef<LineItemRef>(new LineItemRef())
|
13
|
+
|
14
|
+
useEffect(() => (
|
15
|
+
reaction(
|
16
|
+
() => (orig.item),
|
17
|
+
(item: LineItem | undefined) => {
|
18
|
+
if (item) {
|
19
|
+
laggingRef.current.set(item)
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
setTimeout(() => { laggingRef.current.set(undefined) }, lagMs)
|
23
|
+
}
|
24
|
+
},
|
25
|
+
{equals: (val, prev) => (val?.sku === prev?.sku)}
|
26
|
+
)
|
27
|
+
), [])
|
28
|
+
|
29
|
+
return laggingRef.current
|
30
|
+
}
|
package/components/index.ts
CHANGED
@@ -9,13 +9,12 @@ export { default as Logo } from './logo'
|
|
9
9
|
export { default as MiniChart } from './mini-chart'
|
10
10
|
export { default as NotFound } from './not-found'
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
export { default as AuthListener } from './auth/auth-listener'
|
14
13
|
export { default as BuyDrawer } from './commerce/buy-drawer'
|
15
|
-
export { default as CheckoutPanel } from './commerce/checkout-panel'
|
16
14
|
export { default as CheckoutButton } from './commerce/checkout-button'
|
15
|
+
export { default as CheckoutPanel } from './commerce/checkout-panel'
|
16
|
+
export { default as CheckoutWidget } from './commerce/checkout-widget'
|
17
17
|
export { default as LoginPanel } from './auth/login-panel'
|
18
|
-
export { default as AuthListener } from './auth/auth-listener'
|
19
18
|
export { default as Scripts } from './scripts'
|
20
19
|
|
21
20
|
|
package/next/index.ts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export { default as determineDeviceMW } from './determine-device-mw'
|
1
|
+
export { default as determineDeviceMW } from './middleware/determine-device-mw'
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@luxfi/core",
|
3
|
-
"version": "5.0.
|
3
|
+
"version": "5.0.4",
|
4
4
|
"description": "Library that contains shared UI primitives, support for a common design system, and other boilerplate support.",
|
5
5
|
"publishConfig": {
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
@@ -27,6 +27,7 @@
|
|
27
27
|
},
|
28
28
|
"exports": {
|
29
29
|
".": "./components/index.ts",
|
30
|
+
"./commerce": "./commerce/index.ts",
|
30
31
|
"./root-layout": "./root-layout/index.tsx",
|
31
32
|
"./server-actions": "./server-actions/index.ts",
|
32
33
|
"./next": "./next/index.ts",
|
@@ -36,6 +37,9 @@
|
|
36
37
|
"./conf": "./conf/index.ts"
|
37
38
|
},
|
38
39
|
"dependencies": {
|
40
|
+
"@hanzo/auth": "2.4.6",
|
41
|
+
"@hanzo/commerce": "7.0.1",
|
42
|
+
"@hanzo/ui": "3.7.0",
|
39
43
|
"@next/third-parties": "^14.1.0",
|
40
44
|
"cookies-next": "^4.1.1",
|
41
45
|
"date-fns": "^3.6.0",
|
@@ -44,9 +48,6 @@
|
|
44
48
|
"react-social-icons": "^6.4.0"
|
45
49
|
},
|
46
50
|
"peerDependencies": {
|
47
|
-
"@hanzo/auth": "2.4.6",
|
48
|
-
"@hanzo/commerce": "7.0.0",
|
49
|
-
"@hanzo/ui": "3.7.0",
|
50
51
|
"@hookform/resolvers": "^3.3.2",
|
51
52
|
"lucide-react": "^0.344.0",
|
52
53
|
"next": "14.1.3",
|
package/root-layout/index.tsx
CHANGED
@@ -12,9 +12,10 @@ import { FacebookPixelHead } from '../next/analytics/pixel-analytics'
|
|
12
12
|
|
13
13
|
import { AuthListener, ChatWidget, Header, Scripts } from '../components'
|
14
14
|
import BuyDrawer from '../components/commerce/buy-drawer'
|
15
|
+
import CheckoutWidget from '../components/commerce/checkout-widget'
|
15
16
|
|
16
17
|
import { selectionUISpecifiers } from '../conf'
|
17
|
-
import type SiteDef from '../
|
18
|
+
import type SiteDef from '../types/site-def'
|
18
19
|
|
19
20
|
import '../style/lux-global.css'
|
20
21
|
import '../style/cart-animation.css'
|
@@ -58,7 +59,6 @@ const RootLayout: React.FC<PropsWithChildren & {
|
|
58
59
|
}) => {
|
59
60
|
|
60
61
|
const currentUser = await getUserServerSide()
|
61
|
-
const usingCommerce = siteDef?.ext?.commerce && siteDef.ext.commerce.rootNode && siteDef.ext.commerce.families
|
62
62
|
|
63
63
|
const Guts: React.FC = () => (<>
|
64
64
|
{showHeader && <Header siteDef={siteDef}/>}
|
@@ -68,7 +68,7 @@ const RootLayout: React.FC<PropsWithChildren & {
|
|
68
68
|
title='LUX'
|
69
69
|
subtitle='AI'
|
70
70
|
chatbotUrl='https://lux.chat/iframe'
|
71
|
-
suggestedQuestions={siteDef.
|
71
|
+
suggestedQuestions={siteDef.chatbot?.suggestedQuestions ?? []}
|
72
72
|
/>
|
73
73
|
)}
|
74
74
|
</>)
|
@@ -92,15 +92,16 @@ const RootLayout: React.FC<PropsWithChildren & {
|
|
92
92
|
}}>
|
93
93
|
<Scripts/>
|
94
94
|
<AuthServiceProvider user={currentUser} conf={{} as AuthServiceConf}>
|
95
|
-
{
|
95
|
+
{siteDef?.commerce ? (
|
96
96
|
<CommerceProvider
|
97
|
-
rootNode={siteDef.
|
98
|
-
families={siteDef.
|
99
|
-
options={siteDef.
|
97
|
+
rootNode={siteDef.commerce!.rootNode}
|
98
|
+
families={siteDef.commerce!.families}
|
99
|
+
options={siteDef.commerce!.options}
|
100
100
|
uiSpecs={selectionUISpecifiers}
|
101
101
|
>
|
102
102
|
<Guts />
|
103
103
|
<BuyDrawer />
|
104
|
+
<CheckoutWidget />
|
104
105
|
</CommerceProvider>
|
105
106
|
) : (
|
106
107
|
<Guts />
|
package/site-def/index.ts
CHANGED
package/tsconfig.json
CHANGED
@@ -0,0 +1,10 @@
|
|
1
|
+
import type { ServiceOptions } from '@hanzo/commerce'
|
2
|
+
import type { CategoryNode, Family } from '@hanzo/commerce/types'
|
3
|
+
|
4
|
+
interface CommerceConfig {
|
5
|
+
families: Family[]
|
6
|
+
rootNode: CategoryNode
|
7
|
+
options?: ServiceOptions
|
8
|
+
}
|
9
|
+
|
10
|
+
export { type CommerceConfig as default }
|
package/types/index.ts
CHANGED
@@ -1 +1,5 @@
|
|
1
|
-
export type { ContactInfo, ContactInfoFields } from './contact-info'
|
1
|
+
export type { ContactInfo, ContactInfoFields } from './contact-info'
|
2
|
+
export type { default as SiteDef } from './site-def'
|
3
|
+
export type { default as ChatbotSuggestedQuestion } from './chatbot-suggested-question'
|
4
|
+
export type { default as ChatbotConfig } from './chatbot-config'
|
5
|
+
export type { default as CommerceConfig } from './commerce-config'
|
@@ -1,9 +1,15 @@
|
|
1
1
|
import React from 'react'
|
2
|
+
|
2
3
|
import type { LinkDef } from '@hanzo/ui/types'
|
3
4
|
|
5
|
+
import type CommerceConfig from './commerce-config'
|
6
|
+
import type ChatbotConfig from './chatbot-config'
|
7
|
+
|
4
8
|
interface SiteDef {
|
9
|
+
|
5
10
|
/** url of this site. All nav links in the system will show it in 'current' state */
|
6
11
|
currentAs?: string
|
12
|
+
|
7
13
|
nav: {
|
8
14
|
/** common elements (will auto-select currentAs if it's provide) */
|
9
15
|
/** optional feature element. right-most after 'elements' (any min-w is ignored) */
|
@@ -25,13 +31,15 @@ interface SiteDef {
|
|
25
31
|
*/
|
26
32
|
footer: LinkDef[][]
|
27
33
|
|
28
|
-
/**
|
29
|
-
|
30
|
-
|
34
|
+
/**
|
35
|
+
* optional override of default 'above copyright' horizantal links
|
36
|
+
default (undefined / absent): links in side-def/footer/legal are rendered
|
37
|
+
[] renders nothing above the copyright
|
38
|
+
*/
|
31
39
|
aboveCopyright?: LinkDef[]
|
32
40
|
|
33
|
-
|
34
|
-
|
41
|
+
commerce?: CommerceConfig
|
42
|
+
chatbot?: ChatbotConfig
|
35
43
|
}
|
36
44
|
|
37
|
-
export { type SiteDef as default }
|
45
|
+
export { type SiteDef as default }
|
@@ -1,4 +0,0 @@
|
|
1
|
-
<svg viewBox="0 0 32 40" >
|
2
|
-
<path d="M27.724,27.85a3.4,3.4,0,0,0,.825-2.672l-1.8-14.4A3.405,3.405,0,0,0,23.375,7.8h-.521a5.173,5.173,0,0,0-10.32,0H3.894a1,1,0,0,0,0,2H8.929a3.316,3.316,0,0,0-.29.978l-.382,3.055a.889.889,0,0,0-.163-.033h-3a1,1,0,0,0,0,2H8.011L6.839,25.178A3.4,3.4,0,0,0,10.212,29H25.175A3.4,3.4,0,0,0,27.724,27.85ZM17.694,5a3.2,3.2,0,0,1,3.16,2.8h-6.32A3.194,3.194,0,0,1,17.694,5ZM9.163,26.526a1.384,1.384,0,0,1-.34-1.1l1.8-14.4A1.4,1.4,0,0,1,12.013,9.8h.481V13a1,1,0,0,0,2,0V9.8h6.4V13a1,1,0,0,0,2,0V9.8h.481a1.4,1.4,0,0,1,1.39,1.226l1.8,14.4A1.4,1.4,0,0,1,25.175,27H10.212A1.38,1.38,0,0,1,9.163,26.526Z"/>
|
3
|
-
<path d="M7.894,11.8a1,1,0,0,0-1-1h-3a1,1,0,0,0,0,2h3A1,1,0,0,0,7.894,11.8Z"/>
|
4
|
-
</svg>
|
File without changes
|