@luxfi/core 5.0.4 → 5.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,6 +13,6 @@ export const getBullionFamilies = (videoMap: Map<string, VideoDef>) => (
13
13
  )
14
14
  )
15
15
 
16
- export { default as serviceOptions } from './lux-service-options'
16
+ export { default as serviceOptions } from '../../conf/lux-commerce-options'
17
17
  export { default as bullionPrice1oz } from './bullion-price-1oz'
18
18
 
@@ -0,0 +1,118 @@
1
+ import {
2
+ action,
3
+ computed,
4
+ makeObservable,
5
+ observable,
6
+ } from 'mobx'
7
+ import type { CommerceService, LineItem, ObsLineItemRef } from '@hanzo/commerce/types'
8
+
9
+
10
+ interface CommerceUI extends ObsLineItemRef {
11
+ showBuyOptions: (skuPath: string) => void
12
+ hideBuyOptions: () => void
13
+ get buyOptionsSkuPath(): string | undefined
14
+
15
+ itemQuantityChanged(sku: string, val: number, prevVal: number): void
16
+
17
+ get closed(): boolean
18
+ setClosed(b: boolean): void
19
+
20
+ }
21
+
22
+ class CommerceUIStore implements CommerceUI {
23
+
24
+ static readonly TIMEOUT = 1500
25
+ _buyOptionsSkuPath: string | undefined = undefined
26
+ _closed: boolean = false
27
+ _paused: boolean = false
28
+ _activeItem: LineItem | undefined = undefined
29
+ _lastActivity: number | undefined = undefined
30
+ _service: CommerceService
31
+
32
+ constructor(s: CommerceService) {
33
+ this._service = s
34
+ makeObservable(this, {
35
+ _buyOptionsSkuPath: observable,
36
+ _activeItem: observable.shallow,
37
+ _closed: observable,
38
+ showBuyOptions: action,
39
+ hideBuyOptions: action,
40
+ buyOptionsSkuPath: computed,
41
+ itemQuantityChanged: action,
42
+ setClosed: action,
43
+ closed: computed,
44
+ tick: action,
45
+ item: computed
46
+ })
47
+ }
48
+
49
+ showBuyOptions = (skuPath: string): void => {
50
+ this._service.setCurrentItem(undefined)
51
+ this._buyOptionsSkuPath = skuPath
52
+ this._paused = true
53
+ this._closed = false
54
+ }
55
+
56
+ hideBuyOptions = (): void => {
57
+ this._buyOptionsSkuPath = undefined
58
+ this._paused = false
59
+ if (this._lastActivity) {
60
+ this._lastActivity = Date.now()
61
+ }
62
+ }
63
+
64
+ get buyOptionsSkuPath(): string | undefined {
65
+ return this._buyOptionsSkuPath
66
+ }
67
+
68
+ tick = () => {
69
+ if (
70
+ !this._paused
71
+ &&
72
+ this._lastActivity
73
+ &&
74
+ (Date.now() - this._lastActivity >= CommerceUIStore.TIMEOUT)
75
+ ) {
76
+ this._activeItem = undefined
77
+ this._lastActivity = undefined
78
+ }
79
+ }
80
+
81
+ itemQuantityChanged = (sku: string, val: number, oldVal: number): void => {
82
+
83
+ if (val === 0) {
84
+ if (this._activeItem?.sku === sku) {
85
+ this._activeItem = undefined
86
+ this._lastActivity = undefined
87
+ }
88
+ // otherwise ignore
89
+ }
90
+ else if (val < oldVal) {
91
+ if (this._activeItem?.sku === sku) {
92
+ this._lastActivity = Date.now()
93
+ }
94
+ // otherwise ignore
95
+ }
96
+ else {
97
+ this._activeItem = this._service.getItemBySku(sku)
98
+ this._lastActivity = Date.now()
99
+ }
100
+ }
101
+
102
+ get item(): LineItem | undefined {
103
+ return this._activeItem
104
+ }
105
+
106
+ get closed(): boolean {
107
+ return this._closed
108
+ }
109
+
110
+ setClosed = (b: boolean): void => { this._closed = b}
111
+
112
+ dispose = () => {}
113
+ }
114
+
115
+ export {
116
+ CommerceUIStore,
117
+ type CommerceUI
118
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+ import React, {
3
+ createContext,
4
+ useContext,
5
+ useRef,
6
+ type PropsWithChildren,
7
+ useEffect
8
+ } from 'react'
9
+
10
+ // https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
11
+ import { enableStaticRendering } from 'mobx-react-lite'
12
+ enableStaticRendering(typeof window === "undefined")
13
+
14
+ import { type CommerceUI, CommerceUIStore } from './commerce-ui'
15
+ import { useCommerce } from '@hanzo/commerce'
16
+
17
+ const CommerceUIContext = createContext<CommerceUIStore | undefined>(undefined)
18
+
19
+ const useCommerceUI = (): CommerceUI => {
20
+ return useContext(CommerceUIContext) as CommerceUIStore
21
+ }
22
+
23
+ const CommerceUIProvider: React.FC<PropsWithChildren & {
24
+ DEBUG_NO_TICK?: boolean
25
+ }> = ({
26
+ children,
27
+ DEBUG_NO_TICK=false
28
+ }) => {
29
+
30
+ const cmmc = useCommerce()
31
+ const valueRef = useRef<CommerceUIStore>(new CommerceUIStore(cmmc))
32
+
33
+ useEffect(() => {
34
+
35
+ //valueRef.current = new CommerceUIStore(cmmc)
36
+ return () => { valueRef.current?.dispose() }
37
+ }, [])
38
+
39
+ return (
40
+ <CommerceUIContext.Provider value={valueRef.current}>
41
+ {children}
42
+ </CommerceUIContext.Provider>
43
+ )
44
+ }
45
+
46
+ export {
47
+ useCommerceUI,
48
+ CommerceUIProvider
49
+ }
50
+
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+ import React from 'react'
3
+
4
+ import type { LineItem } from '@hanzo/commerce/types'
5
+ import { AddToCartWidget } from '@hanzo/commerce'
6
+
7
+ import { useCommerceUI } from '../../commerce/ui-context'
8
+
9
+ const AddWidget: React.FC<{
10
+ item: LineItem
11
+ disabled?: boolean
12
+ className?: string
13
+ buttonClx?: string
14
+ variant?: 'minimal' | 'primary' | 'outline'
15
+ }> = (props) => {
16
+ const ui = useCommerceUI()
17
+ return <AddToCartWidget {...props} onQuantityChanged={ui.itemQuantityChanged}/>
18
+ }
19
+
20
+ export default AddWidget
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+ import React, {type PropsWithChildren} from 'react'
3
+
4
+ import { Button, buttonVariants } from '@hanzo/ui/primitives'
5
+ import { type VariantProps } from '@hanzo/ui/util'
6
+
7
+ import { cn } from '@hanzo/ui/util'
8
+ import { useCommerceUI } from '../../commerce/ui-context'
9
+
10
+ const BuyButton: React.FC<
11
+ PropsWithChildren &
12
+ VariantProps<typeof buttonVariants> &
13
+ {
14
+ skuPath: string
15
+ className?: string
16
+ }
17
+ > = ({
18
+ skuPath,
19
+ children,
20
+ className='',
21
+ ...rest
22
+ }) => {
23
+
24
+ const ui = useCommerceUI()
25
+ const handleClick = () => { ui.showBuyOptions(skuPath) }
26
+
27
+ return (
28
+ <Button onClick={handleClick} {...rest} className={cn(className, '')}>
29
+ {children}
30
+ </Button>
31
+ )
32
+ }
33
+
34
+ export default BuyButton
@@ -3,29 +3,70 @@ import React, {type PropsWithChildren } from 'react'
3
3
 
4
4
  import { X as LucideX} from 'lucide-react'
5
5
 
6
- import { Button, Drawer, DrawerContent, type DrawerProps } from '@hanzo/ui/primitives'
6
+ import {
7
+ Button,
8
+ Drawer,
9
+ DrawerContent,
10
+ DrawerHandle,
11
+ type DrawerProps,
12
+ useDrawerContext
13
+ } from '@hanzo/ui/primitives'
7
14
  import { cn } from '@hanzo/ui/util'
8
15
 
16
+ import '../../../style/drawer-handle-overrides.css'
17
+
9
18
  const CommerceDrawer: React.FC<PropsWithChildren &
10
19
  Omit<DrawerProps, 'onOpenChange'> &
11
20
  {
12
21
  setOpen: (b: boolean) => void
22
+ handleHandleClicked: () => void
13
23
  drawerClx?: string
24
+ setActiveSPIndexSetter?: (fn: (snapPoint: number | string | null) => void) => void
14
25
  }
15
26
  > = ({
16
27
  children,
17
28
  open,
18
29
  setOpen,
19
30
  modal,
31
+ snapPoints,
32
+ setActiveSnapPoint,
33
+ activeSnapPoint,
34
+ handleHandleClicked,
35
+ setActiveSPIndexSetter,
20
36
  drawerClx='',
21
37
  ...rest
22
- }) => (
38
+ }) => {
39
+
40
+
41
+ return (
23
42
  // @ts-ignore
24
- <Drawer open={open} onOpenChange={setOpen} modal={modal} {...rest}>
25
- <DrawerContent modal={modal} className={cn(
43
+ <Drawer
44
+ open={open}
45
+ onOpenChange={setOpen}
46
+ modal={modal}
47
+ snapPoints={snapPoints}
48
+ setActiveSnapPoint={setActiveSnapPoint}
49
+ activeSnapPoint={activeSnapPoint}
50
+ fastDragSkipsToEnd={false}
51
+ handleOnly={true}
52
+ setActiveSPIndexSetter={setActiveSPIndexSetter}
53
+
54
+
55
+ {...rest}
56
+ >
57
+ <DrawerContent defaultHandle={false} className={cn(
26
58
  'rounded-t-xl mt-6 pt-6',
27
59
  drawerClx
28
60
  )}>
61
+
62
+ <DrawerHandle
63
+ className={
64
+ 'absolute left-0 right-0 mx-auto top-2 ' +
65
+ 'w-[100px] h-3 rounded-full bg-level-3 hover:bg-level-2 shrink-0'
66
+ }
67
+ handleClick={handleHandleClicked}
68
+ />
69
+
29
70
  {children}
30
71
  <Button
31
72
  variant='ghost'
@@ -38,6 +79,7 @@ const CommerceDrawer: React.FC<PropsWithChildren &
38
79
  </DrawerContent>
39
80
  </Drawer>
40
81
  )
82
+ }
41
83
 
42
84
 
43
85
  export default CommerceDrawer
@@ -1,44 +1,242 @@
1
1
  'use client'
2
- import React from 'react'
3
- import { useRouter } from 'next/navigation'
2
+ import React, { useEffect, useRef, useState } from 'react'
3
+ import { usePathname, useRouter } from 'next/navigation'
4
+ import { action, computed, makeObservable, observable, reaction, type IReactionDisposer } from 'mobx'
4
5
  import { observer } from 'mobx-react-lite'
5
6
 
6
- import { useCommerceUI, CarouselBuyCard } from '@hanzo/commerce'
7
+ import { CarouselBuyCard, useCommerce } from '@hanzo/commerce'
8
+
9
+ import { useCommerceUI } from '../../../commerce/ui-context'
7
10
 
8
11
  import CommerceDrawer from './drawer'
9
12
  import CheckoutButton from '../checkout-button'
10
13
 
14
+ const BUY = '700px'
15
+ const MICRO = '120px'
16
+ const BOTH = [MICRO, BUY]
17
+ const BUY_ONLY = [BUY]
18
+ const MICRO_ONLY = [MICRO]
19
+
20
+ type DrawerMode = 'checkout' | 'added' | 'buy' | 'buy-added' | 'buy-checkout' | 'none' | 'closed' // manually
21
+ type DrawerState = 'micro' | 'buy' | 'closed'
22
+
23
+ const MODE_TO_STATE = {
24
+ checkout: 'micro',
25
+ added: 'micro',
26
+ buy: 'buy',
27
+ 'buy-checkout': 'buy',
28
+ 'buy-added': 'buy',
29
+ none: 'closed',
30
+ closed: 'closed'
31
+ } satisfies Record<DrawerMode, DrawerState>
32
+
33
+ const MODE_TO_POINTS = {
34
+ checkout: MICRO_ONLY,
35
+ added: BOTH,
36
+ buy: BUY_ONLY,
37
+ 'buy-checkout': BOTH,
38
+ 'buy-added': BOTH,
39
+ none: BOTH,
40
+ closed: BOTH
41
+ }
42
+
43
+
44
+ class ObsDrawerState {
45
+
46
+ _mode: DrawerMode = 'none'
47
+
48
+ constructor() {
49
+ makeObservable(this, {
50
+ _mode: observable,
51
+ setMode: action,
52
+ mode: computed,
53
+ state: computed,
54
+ points: computed,
55
+ modal: computed,
56
+ activePoint: computed
57
+ })
58
+ }
59
+
60
+ get mode(): DrawerMode {return this._mode}
61
+ get state(): DrawerState { return MODE_TO_STATE[this._mode] }
62
+ get points(): (number | string)[] { return MODE_TO_POINTS[this._mode] }
63
+ get modal(): boolean { return this.state !== 'micro' }
64
+ get activePoint(): number | string | null {
65
+ if (this.state === 'buy') return BUY
66
+ if (this.state === 'micro') return MICRO
67
+ return null
68
+ }
69
+
70
+ setMode = (m: DrawerMode) => {this._mode = m}
71
+ }
72
+
11
73
  const CommerceUIComponent: React.FC = observer(() => {
12
74
 
75
+ const cmmc = useCommerce()
13
76
  const ui = useCommerceUI()
14
77
  const router = useRouter()
78
+ const isCheckout = usePathname() === '/checkout'
79
+
80
+ const stateRef = useRef<ObsDrawerState>(new ObsDrawerState())
81
+ const reactionDisposers = useRef<IReactionDisposer[]>([])
82
+
83
+ const [activeSnapPoint, setActiveSnapPoint] = useState<string | number | null>(null)
84
+ const setterRef = useRef<((index: number ) => void) | undefined>(undefined)
85
+
86
+ useEffect(() => {
87
+
88
+ reactionDisposers.current.push(reaction(
89
+
90
+ () => ({
91
+ buy: !!ui.buyOptionsSkuPath,
92
+ added: !isCheckout && ui.item,
93
+ checkout: !isCheckout && !cmmc.cartEmpty,
94
+ closed: ui.closed
95
+ }),
96
+ ({buy, added, checkout, closed}) => {
97
+ let mode: DrawerMode = 'none' // TODO: 'closed'
98
+ if (buy) {
99
+ if (added) {
100
+ mode = 'buy-added'
101
+ }
102
+ else if (checkout) {
103
+ mode = 'buy-checkout'
104
+ }
105
+ else {
106
+ mode = 'buy'
107
+ }
108
+ }
109
+ else {
110
+ if (closed) {
111
+ mode = 'closed'
112
+ }
113
+ else if (added) {
114
+ mode = 'added'
115
+ }
116
+ else if (checkout) {
117
+ mode = 'checkout'
118
+ }
119
+ }
120
+ stateRef.current.setMode(mode)
121
+ },
122
+ {equals: (val, prev) => (
123
+ val.buy === prev.buy
124
+ &&
125
+ val.added === prev.added
126
+ &&
127
+ val.checkout === prev.checkout
128
+ &&
129
+ val.closed === prev.closed
130
+ )}
131
+ )),
132
+ reactionDisposers.current.push(reaction(
133
+ () => ( stateRef.current.state ),
134
+ (s) => {
135
+ if (s === 'buy') {
136
+ //setterRef.current?.(stateRef.current.points.length - 1)
137
+ setActiveSnapPoint(BUY)
138
+ }
139
+ else if (s === 'micro') {
140
+ //setterRef.current?.(0)
141
+ setActiveSnapPoint(MICRO)
142
+ }
143
+ }
144
+ ))
145
+ return () => {
146
+ reactionDisposers.current?.forEach((d) => {d()})
147
+ }
148
+ }, [isCheckout])
149
+
150
+ const _setActiveSnapPoint = (pt: string | number | null): void => {
151
+ console.log("ON CHANGE: ", pt)
152
+ setActiveSnapPoint(pt)
153
+ }
15
154
 
16
155
  const handleCheckout = () => {
17
156
  router.push('/checkout')
18
157
  }
19
158
 
20
- // Should only ever be called internally to close
21
159
  const reallyOnlyCloseDrawer = (b: boolean) => {
22
- if (!b ) {
23
- ui.hideBuyOptions()
160
+ // Should only ever be called internally to close
161
+ // Using handleCloseGesture()
162
+ }
163
+
164
+ const handleHandleClicked = () => {
165
+ console.log("HANDLE CLICKED")
166
+
167
+ if (stateRef.current.state === 'buy') {
168
+ const toks = stateRef.current.mode.split('-')
169
+ if (toks.length <= 1) {
170
+ console.log("CLOSING 'BUY' ... ")
171
+ ui.hideBuyOptions()
172
+ }
173
+ else {
174
+ console.log("CLOSING 'BUY' to ", toks[1])
175
+ ui.hideBuyOptions()
176
+ }
177
+ }
178
+ else if (stateRef.current.state === 'micro') {
179
+ if (stateRef.current.mode === 'checkout') {
180
+ console.log(" CLOSING 'CHECKOUT' ... ")
181
+ ui.setClosed(true)
182
+ }
183
+ else if (stateRef.current.mode === 'added') {
184
+ console.log(" OPENING 'ADDED' ... ")
185
+ ui.showBuyOptions(ui.item?.sku ?? '')
186
+ }
24
187
  }
25
188
  }
26
189
 
190
+ const handleCloseGesture = () => {
191
+ if (stateRef.current.state === 'buy') {
192
+ console.log(" CLOSING 'BUY' ... ")
193
+ const toks = stateRef.current.mode.split('-')
194
+ if (toks.length <= 1) {
195
+ stateRef.current.setMode('none')
196
+ }
197
+ else {
198
+ stateRef.current.setMode(toks[1] as DrawerMode) // 'checkout' or 'added'
199
+ }
200
+ return true // "handled!"
201
+ }
202
+ console.log("DEFAULT CLOSE ACTION")
203
+ return false
204
+ }
205
+
206
+
207
+ const setActiveSPIndexSetter = (fn: (index: number ) => void): void => {
208
+ setterRef.current = fn
209
+ }
210
+
211
+
27
212
  return (
28
213
  <CommerceDrawer
29
- open={!!ui.buyOptionsSkuPath}
214
+ open={!(stateRef.current.state === 'closed')}
30
215
  setOpen={reallyOnlyCloseDrawer}
31
- drawerClx={'w-full md:max-w-[550px] md:mx-auto lg:max-w-[50vw]'}
216
+ drawerClx={'w-full h-full'}
217
+ snapPoints={stateRef.current.points}
218
+ modal={stateRef.current.modal}
219
+ activeSnapPoint={activeSnapPoint}
220
+ setActiveSnapPoint={_setActiveSnapPoint}
221
+ handleHandleClicked={handleHandleClicked}
222
+ setActiveSPIndexSetter={setActiveSPIndexSetter}
223
+ handleCloseGesture={handleCloseGesture}
32
224
  >
33
- <CarouselBuyCard
34
- skuPath={ui.buyOptionsSkuPath!}
35
- checkoutButton={
36
- <CheckoutButton handleCheckout={handleCheckout} className='w-full min-w-[160px] sm:max-w-[320px]'/>
37
- }
38
- clx='w-full'
39
- addBtnClx='w-full min-w-[160px] sm:max-w-[320px]'
40
- selectorClx='max-w-[475px]'
41
- />
225
+ {stateRef.current.state === 'buy' && (
226
+ <CarouselBuyCard
227
+ skuPath={ui.buyOptionsSkuPath!}
228
+ checkoutButton={
229
+ <CheckoutButton handleCheckout={handleCheckout} className='w-full min-w-[160px] sm:max-w-[320px]'/>
230
+ }
231
+ onQuantityChanged={ui.itemQuantityChanged}
232
+ clx='w-full'
233
+ addBtnClx='w-full min-w-[160px] sm:max-w-[320px]'
234
+ selectorClx='max-w-[475px]'
235
+ />
236
+ )}
237
+ {stateRef.current.state === 'micro' && (
238
+ <p>Mode: {stateRef.current.mode}</p>
239
+ )}
42
240
  </CommerceDrawer>
43
241
  )
44
242
  })
@@ -2,7 +2,6 @@
2
2
  import React, { useEffect, useRef } from 'react'
3
3
  import { observable, type IObservableValue, reaction } from 'mobx'
4
4
  import { observer } from 'mobx-react-lite'
5
- import { type LucideProps } from 'lucide-react'
6
5
 
7
6
  import { Button, type ButtonProps } from '@hanzo/ui/primitives'
8
7
  import { cn } from '@hanzo/ui/util'
@@ -12,11 +11,15 @@ import * as Icons from '../icons'
12
11
 
13
12
  const IconAndQuantity: React.FC<{
14
13
  animateOnQuantityChange?: boolean
14
+ showArrow?: boolean
15
+ showQuantity?: boolean
15
16
  clx?: string
16
17
  iconClx?: string
17
18
  digitClx?: string
18
19
  }> = observer(({
19
- animateOnQuantityChange=true,
20
+ animateOnQuantityChange=false,
21
+ showArrow=true,
22
+ showQuantity=true,
20
23
  clx='',
21
24
  iconClx='',
22
25
  digitClx=''
@@ -47,6 +50,7 @@ const IconAndQuantity: React.FC<{
47
50
 
48
51
  return (
49
52
  <div className={cn('flex items-center justify-center', clx)}>
53
+ {showQuantity && (
50
54
  <div className={cn(
51
55
  'relative flex items-center justify-center mr-1',
52
56
  ((wiggleRef.current.get() === 'more') ?
@@ -60,12 +64,13 @@ const IconAndQuantity: React.FC<{
60
64
  'absolute left-0 right-0 top-0 bottom-0',
61
65
  digitClx
62
66
  )}>
63
- <div style={{color: 'white' /* tailwind bug? */, fontSize: '11px', position: 'relative', top: '1px' }}>{cmmc.cartQuantity}</div>
67
+ <div style={{/* color: 'white' tailwind bug? ,*/ fontSize: '11px', position: 'relative', top: '1px' }}>{cmmc.cartQuantity}</div>
64
68
  </div>
65
69
  )}
66
70
  <Icons.bag width='19' height='24' className={cn('relative -top-[3px] opacity-70' , iconClx)} aria-hidden="true" />
67
71
  </div>
68
- <span style={{fontSize: '17px',}}>&rsaquo;</span>
72
+ )}
73
+ {showArrow && (<span style={{fontSize: '17px',}}>&rsaquo;</span>)}
69
74
  </div>
70
75
  )
71
76
  })
@@ -73,6 +78,7 @@ const IconAndQuantity: React.FC<{
73
78
  const CheckoutButton: React.FC<ButtonProps & {
74
79
  handleCheckout: () => void
75
80
  showQuantity?: boolean
81
+ showArrow?: boolean
76
82
  animateOnQuantityChange?: boolean
77
83
  centerText?: boolean
78
84
  }> = ({
@@ -81,8 +87,10 @@ const CheckoutButton: React.FC<ButtonProps & {
81
87
  rounded='lg',
82
88
  className,
83
89
  showQuantity=true,
90
+ showArrow=true,
84
91
  animateOnQuantityChange=true,
85
92
  centerText=true,
93
+ children,
86
94
  ...rest
87
95
  }) => {
88
96
 
@@ -93,22 +101,27 @@ const CheckoutButton: React.FC<ButtonProps & {
93
101
  variant={variant}
94
102
  rounded={rounded}
95
103
  className={cn(
104
+ 'flex justify-between items-stretch group',
105
+ showQuantity ? (centerText ? 'px-1.5' : 'pl-2.5 pr-1.5') : '',
96
106
  className,
97
- 'flex justify-between items-stretch',
98
- showQuantity ? (centerText ? 'px-1.5' : 'pl-2.5 pr-1.5') : ''
99
107
  )}
100
108
  >
101
- {showQuantity && centerText && (
102
- <IconAndQuantity clx='invisible' />
103
- )}
104
- <div className='flex justify-center items-center'>Checkout</div>
105
- {showQuantity && (
109
+ {centerText && ( // must scale this one too, as it effects layout
106
110
  <IconAndQuantity
107
- animateOnQuantityChange={animateOnQuantityChange}
108
- iconClx='fill-fg-foreground'
109
- digitClx='text-primary-fg leading-none font-bold font-sans'
111
+ showArrow={showArrow}
112
+ showQuantity={showQuantity}
113
+ clx='invisible group-hover:scale-105 transition-scale transition-duration-300'
110
114
  />
111
115
  )}
116
+ {children ?? (<div className='flex justify-center items-center'>Checkout</div>)}
117
+ <IconAndQuantity
118
+ clx='group-hover:scale-105 transition-scale transition-duration-300'
119
+ animateOnQuantityChange={animateOnQuantityChange}
120
+ showArrow={showArrow}
121
+ showQuantity={showQuantity}
122
+ iconClx='fill-background'
123
+ digitClx='text-foreground group-hover:opacity-80 leading-none font-bold font-sans'
124
+ />
112
125
  </Button>
113
126
  )
114
127
  }
@@ -1,18 +1,104 @@
1
1
  'use client'
2
- import React from 'react'
2
+ import React, { useRef } from 'react'
3
3
  import { createPortal } from 'react-dom'
4
4
  import { usePathname, useRouter } from 'next/navigation'
5
5
  import { observer } from 'mobx-react-lite'
6
6
 
7
7
  import { cn } from '@hanzo/ui/util'
8
+ import { useStepAnimation } from '@hanzo/ui/util-client'
9
+
8
10
  import { Image } from '@hanzo/ui/primitives'
9
11
 
10
- import { useCommerceUI } from '@hanzo/commerce'
12
+ import { useCommerceUI } from '../../../commerce/ui-context'
11
13
 
12
14
  import CheckoutButton from '../checkout-button'
13
15
  import useAnimationClxSet from './use-anim-clx-set'
14
- import useLaggingItemRef from './use-lagging-item-ref'
15
16
  import CONST from './const'
17
+ import type { LineItem } from '@hanzo/commerce/types'
18
+
19
+ const transStyle = (t: { transition: string, from : string, to: string } | undefined) : any => (
20
+ t ? {
21
+ transitionProperty: t.transition,
22
+ transitionTimingFunction: CONST.animTimingFn,
23
+ transitionDuration: `${CONST.animDurationMs}ms`
24
+ } : {}
25
+ )
26
+
27
+ const transClx = (on: boolean, t: { transition: string, from : string, to: string } | undefined) : string => (
28
+ on ? (t?.from ?? '') : (t?.to ?? '')
29
+ )
30
+
31
+ const VARS: any = {
32
+ BR: {
33
+ pos: 'bottom-[24px] right-[66px]',
34
+ width: 'w-initial',
35
+ centerText: false,
36
+ coClx: 'w-auto',
37
+ infoClx: 'w-auto',
38
+ activeItemAnim: {
39
+ co: {
40
+ transition: 'none',
41
+ from : 'px-3 gap-2.5',
42
+ to: ''
43
+ },
44
+ coText: {
45
+ transition: 'max-width',
46
+ from : 'max-w-[100px]',
47
+ to: 'max-w-[0px]'
48
+ },
49
+ info: {
50
+ transition: 'transform, opacity',
51
+ from : 'scale-x-100 opacity-100 origin-right',
52
+ to: 'scale-x-0 opacity-0 origin-right'
53
+ }
54
+ },
55
+ showArrow: true
56
+ },
57
+ TR: {
58
+ pos: 'top-[48px] md:top-[80px] right-[28px]',
59
+ width: 'w-initial',
60
+ centerText: false,
61
+ showQuantity: false,
62
+ showArrow: true,
63
+ coClx: 'w-auto px-3 gap-1',
64
+ infoClx: 'w-auto',
65
+ activeItemAnim: {
66
+ co: {
67
+ transition: 'none',
68
+ from : 'px-3 gap-2.5',
69
+ to: ''
70
+ },
71
+ coText: {
72
+ transition: 'max-width',
73
+ from : 'max-w-[100px]',
74
+ to: 'max-w-[0px]'
75
+ },
76
+ info: {
77
+ transition: 'transform',
78
+ from : 'scale-x-100 origin-right',
79
+ to: 'scale-x-0 origin-right'
80
+ }
81
+ },
82
+ },
83
+ TRIO: {
84
+ pos: 'top-[48px] md:top-[70px] right-[28px]',
85
+ centerText: false,
86
+ showQuantity: true,
87
+ showArrow: true,
88
+ width: 'w-initial',
89
+ coClx: 'hidden',
90
+ infoClx: 'w-auto',
91
+ activeItemAnim: {
92
+ info: {
93
+ transition: 'transform, opacity',
94
+ from : 'scale-x-100 opacity-100',
95
+ to: 'scale-x-50 opacity-0'
96
+ }
97
+ },
98
+ }
99
+ }
100
+
101
+ const v = 'TR'
16
102
 
17
103
  const CheckoutWidget: React.FC<{
18
104
  clx?: string
@@ -26,58 +112,78 @@ const CheckoutWidget: React.FC<{
26
112
  const clxSet = useAnimationClxSet(isCheckout)
27
113
 
28
114
  const itemRef = useCommerceUI()
29
- const laggingRef = useLaggingItemRef(itemRef, CONST.animDurationMs)
115
+
116
+ // for rendering content after itemRef.item() would return false
117
+ const persistentRef = useRef<LineItem | undefined>(undefined)
118
+
119
+ // Doing double duty of being initial step fn for StepAnimation,
120
+ // and also capturing the item for persistentRef :)
121
+ const initialStepFn = (): boolean => {
122
+ if (!!itemRef.item && !persistentRef.current) {
123
+ persistentRef.current = itemRef.item
124
+ }
125
+ return !!itemRef.item
126
+ }
127
+ const steps = useStepAnimation(initialStepFn, [CONST.animDurationMs, CONST.animDurationMs, CONST.animDurationMs])
30
128
 
31
129
  const handleCheckout = () => { router.push('/checkout')}
32
130
 
33
131
  return globalThis?.document?.body && createPortal(
34
132
  (<div
35
133
  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',
134
+ VARS[v].width,
135
+ 'z-below-modal-2 fixed ',
136
+ VARS[v].pos,
137
+ 'rounded-lg',
39
138
  'flex',
40
- itemRef.item ? 'gap-2' : '',
139
+ steps.notPast(0) ? 'bg-background' : '',
140
+ steps.notPast(1) ? 'gap-2' : '',
41
141
  clxSet.asArray.join(' ')
42
142
  )}
43
- style={laggingRef.item ? {} : CONST.shadowStyle}
143
+ style={steps.notPast(1) ? {} : VARS[v].coClx?.includes('hidden') ? {} : CONST.shadowStyle}
44
144
  >
45
145
  <div
46
146
  className={cn(
47
147
  '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' : ''
148
+ transClx(steps.notPast(0), VARS[v].activeItemAnim.info),
149
+ VARS[v].itemClx,
150
+ steps.notPast(1) ? 'px-3 border rounded-lg bg-level-1 border-muted-3' : ''
50
151
  )}
51
- style={{
52
- transitionProperty: 'width',
53
- transitionTimingFunction: CONST.animTimingFn,
54
- transitionDuration: `${CONST.animDurationMs}ms`
55
- }}
152
+ style={transStyle(VARS[v].activeItemAnim.info)}
56
153
  >
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>
154
+ {steps.notPast(1) && persistentRef.current?.img && (
155
+ <Image def={persistentRef.current.img} constrainTo={CONST.itemImgConstraint} preload className='grow-0 shrink-0'/>
156
+ )}
157
+ {steps.notPast(1) && persistentRef.current && (<div className='text-foreground grow ml-1'>
158
+ <p className='whitespace-nowrap text-ellipsis text-sm'>{persistentRef.current.title}</p>
159
+ <p className='whitespace-nowrap text-clip text-xxs' >recently added...</p>
160
+ </div>)}
69
161
  </div>
70
162
  <CheckoutButton
71
163
  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
- />
164
+ centerText={VARS[v].centerText ?? !steps.notPast(0)}
165
+ variant='primary'
166
+ rounded='lg'
167
+ showQuantity={VARS[v].showQuantity ?? true}
168
+ showArrow={VARS[v].showArrow ?? true}
169
+ className={cn(
170
+ // for setting and unsetting 'gap'
171
+ transClx((VARS[v].activeItemAnim.coText ? steps.notPast(3) : true), VARS[v].activeItemAnim.co),
172
+ VARS[v].coClx
173
+ )}
174
+ style={transStyle(VARS[v].activeItemAnim.co)}
175
+ >
176
+ <div
177
+ className={cn(
178
+ 'overflow-hidden',
179
+ 'flex justify-center items-center',
180
+ transClx(steps.notPast(2), VARS[v].activeItemAnim.coText),
181
+ )}
182
+ style={transStyle(VARS[v].activeItemAnim.coText)}
183
+ >
184
+ Checkout
185
+ </div>
186
+ </CheckoutButton>
81
187
  </div>),
82
188
  globalThis?.document?.body
83
189
  )
@@ -1,8 +1,10 @@
1
1
  import { useEffect, useRef } from 'react'
2
2
  import { reaction, runInAction} from 'mobx'
3
3
 
4
+ import { useCommerce } from '@hanzo/commerce'
5
+
4
6
  import ObsStringSet from './obs-string-set'
5
- import { useCommerce, useCommerceUI } from '@hanzo/commerce'
7
+ import { useCommerceUI } from '../../../commerce/ui-context'
6
8
 
7
9
  export default (isCheckout: boolean): ObsStringSet => {
8
10
 
@@ -10,18 +10,12 @@ export { default as MiniChart } from './mini-chart'
10
10
  export { default as NotFound } from './not-found'
11
11
 
12
12
  export { default as AuthListener } from './auth/auth-listener'
13
+ export { default as AddWidget } from './commerce/add-widget'
13
14
  export { default as BuyDrawer } from './commerce/buy-drawer'
15
+ export { default as BuyButton } from './commerce/buy-button'
14
16
  export { default as CheckoutButton } from './commerce/checkout-button'
15
17
  export { default as CheckoutPanel } from './commerce/checkout-panel'
16
18
  export { default as CheckoutWidget } from './commerce/checkout-widget'
17
19
  export { default as LoginPanel } from './auth/login-panel'
18
20
  export { default as Scripts } from './scripts'
19
21
 
20
-
21
- /* PLEASE KEEP
22
- export {
23
- default as HeadMetadata,
24
- getTitleFromTemplateString,
25
- TwitterComponent
26
- } from './head-metadata'
27
- */
package/conf/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { SelectionUISpecifier } from '@hanzo/commerce/types'
2
2
 
3
+ export { default as commerceServiceOptions } from './lux-commerce-options'
4
+
3
5
  export const selectionUISpecifiers = {
4
6
 
5
7
  'LXM-CN': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxfi/core",
3
- "version": "5.0.4",
3
+ "version": "5.0.6",
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,7 +27,7 @@
27
27
  },
28
28
  "exports": {
29
29
  ".": "./components/index.ts",
30
- "./commerce": "./commerce/index.ts",
30
+ "./commerce-data": "./commerce/data/index.ts",
31
31
  "./root-layout": "./root-layout/index.tsx",
32
32
  "./server-actions": "./server-actions/index.ts",
33
33
  "./next": "./next/index.ts",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@hanzo/auth": "2.4.6",
41
- "@hanzo/commerce": "7.0.1",
42
- "@hanzo/ui": "3.7.0",
41
+ "@hanzo/commerce": "7.0.3",
42
+ "@hanzo/ui": "3.7.22",
43
43
  "@next/third-parties": "^14.1.0",
44
44
  "cookies-next": "^4.1.1",
45
45
  "date-fns": "^3.6.0",
@@ -50,13 +50,15 @@
50
50
  "peerDependencies": {
51
51
  "@hookform/resolvers": "^3.3.2",
52
52
  "lucide-react": "^0.344.0",
53
+ "mobx": "^6.12.0",
54
+ "mobx-react-lite": "^4.0.5",
53
55
  "next": "14.1.3",
54
56
  "next-themes": "^0.2.1",
55
- "react": "^18.2.0",
56
- "react-dom": "^18.2.0",
57
- "react-hook-form": "^7.51.3",
57
+ "react": "^18.3.1",
58
+ "react-dom": "^18.3.1",
59
+ "react-hook-form": "^7.51.4",
58
60
  "validator": "^13.11.0",
59
- "zod": "3.21.4"
61
+ "zod": "3.23.8"
60
62
  },
61
63
  "devDependencies": {
62
64
  "@mdx-js/loader": "^3.0.0",
@@ -64,8 +66,8 @@
64
66
  "@types/facebook-pixel": "^0.0.30",
65
67
  "@types/gtag.js": "^0.0.19",
66
68
  "@types/mdx": "^2.0.9",
67
- "@types/react": "^18.2.64",
68
- "@types/react-dom": "^18.2.18",
69
+ "@types/react": "^18.3.2",
70
+ "@types/react-dom": "^18.3.0",
69
71
  "tailwindcss": "^3.4.2",
70
72
  "typescript": "5.3.3"
71
73
  }
@@ -10,9 +10,10 @@ import { CommerceProvider } from '@hanzo/commerce'
10
10
  import getAppRouterBodyFontClasses from '../next/font/get-app-router-font-classes'
11
11
  import { FacebookPixelHead } from '../next/analytics/pixel-analytics'
12
12
 
13
+ import { CommerceUIProvider } from '../commerce/ui-context'
13
14
  import { AuthListener, ChatWidget, Header, Scripts } from '../components'
15
+
14
16
  import BuyDrawer from '../components/commerce/buy-drawer'
15
- import CheckoutWidget from '../components/commerce/checkout-widget'
16
17
 
17
18
  import { selectionUISpecifiers } from '../conf'
18
19
  import type SiteDef from '../types/site-def'
@@ -99,9 +100,10 @@ const RootLayout: React.FC<PropsWithChildren & {
99
100
  options={siteDef.commerce!.options}
100
101
  uiSpecs={selectionUISpecifiers}
101
102
  >
102
- <Guts />
103
- <BuyDrawer />
104
- <CheckoutWidget />
103
+ <CommerceUIProvider >
104
+ <Guts />
105
+ <BuyDrawer />
106
+ </CommerceUIProvider>
105
107
  </CommerceProvider>
106
108
  ) : (
107
109
  <Guts />
@@ -0,0 +1,154 @@
1
+ [vaul-drawer] {
2
+ touch-action: none;
3
+ transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
4
+ }
5
+
6
+ [vaul-drawer][vaul-drawer-direction='bottom'] {
7
+ transform: translate3d(0, 100%, 0);
8
+ }
9
+
10
+ [vaul-drawer][vaul-drawer-direction='top'] {
11
+ transform: translate3d(0, -100%, 0);
12
+ }
13
+
14
+ [vaul-drawer][vaul-drawer-direction='left'] {
15
+ transform: translate3d(-100%, 0, 0);
16
+ }
17
+
18
+ [vaul-drawer][vaul-drawer-direction='right'] {
19
+ transform: translate3d(100%, 0, 0);
20
+ }
21
+
22
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='top'] {
23
+ overflow-y: hidden !important;
24
+ }
25
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='bottom'] {
26
+ overflow-y: hidden !important;
27
+ }
28
+
29
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='left'] {
30
+ overflow-x: hidden !important;
31
+ }
32
+
33
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='right'] {
34
+ overflow-x: hidden !important;
35
+ }
36
+
37
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='top'] {
38
+ transform: translate3d(0, var(--snap-point-height, 0), 0);
39
+ }
40
+
41
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='bottom'] {
42
+ transform: translate3d(0, var(--snap-point-height, 0), 0);
43
+ }
44
+
45
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='left'] {
46
+ transform: translate3d(var(--snap-point-height, 0), 0, 0);
47
+ }
48
+
49
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='right'] {
50
+ transform: translate3d(var(--snap-point-height, 0), 0, 0);
51
+ }
52
+
53
+ [vaul-overlay] {
54
+ opacity: 0;
55
+ transition: opacity 0.5s cubic-bezier(0.32, 0.72, 0, 1);
56
+ }
57
+
58
+ [vaul-overlay][vaul-drawer-visible='true'] {
59
+ opacity: 1;
60
+ }
61
+
62
+ [vaul-drawer]::after {
63
+ content: '';
64
+ position: absolute;
65
+ background: inherit;
66
+ background-color: inherit;
67
+ }
68
+
69
+ [vaul-drawer][vaul-drawer-direction='top']::after {
70
+ top: initial;
71
+ bottom: 100%;
72
+ left: 0;
73
+ right: 0;
74
+ height: 200%;
75
+ }
76
+
77
+ [vaul-drawer][vaul-drawer-direction='bottom']::after {
78
+ top: 100%;
79
+ bottom: initial;
80
+ left: 0;
81
+ right: 0;
82
+ height: 200%;
83
+ }
84
+
85
+ [vaul-drawer][vaul-drawer-direction='left']::after {
86
+ left: initial;
87
+ right: 100%;
88
+ top: 0;
89
+ bottom: 0;
90
+ width: 200%;
91
+ }
92
+
93
+ [vaul-drawer][vaul-drawer-direction='right']::after {
94
+ left: 100%;
95
+ right: initial;
96
+ top: 0;
97
+ bottom: 0;
98
+ width: 200%;
99
+ }
100
+
101
+ [vaul-handle] {
102
+ /* opacity: 0.8; */
103
+ touch-action: pan-y;
104
+ cursor: grab;
105
+ }
106
+
107
+ /* [vaul-handle]:hover, */
108
+ [vaul-handle]:active {
109
+ opacity: 1;
110
+ }
111
+
112
+ [vaul-handle]:active {
113
+ cursor: grabbing;
114
+ }
115
+
116
+ [vaul-handle-hitarea] {
117
+ position: absolute;
118
+ left: 50%;
119
+ top: 50%;
120
+ transform: translate(-50%, -50%);
121
+ width: max(100%, 2.75rem); /* 44px */
122
+ height: max(100%, 2.75rem); /* 44px */
123
+ touch-action: inherit;
124
+ }
125
+
126
+ [vaul-overlay][vaul-snap-points='true']:not([vaul-snap-points-overlay='true']):not([data-state='closed']) {
127
+ opacity: 0;
128
+ }
129
+
130
+ [vaul-overlay][vaul-snap-points-overlay='true']:not([vaul-drawer-visible='false']) {
131
+ opacity: 1;
132
+ }
133
+
134
+ /* This will allow us to not animate via animation, but still benefit from delaying unmount via Radix. */
135
+ @keyframes fake-animation {
136
+ from {
137
+ }
138
+ to {
139
+ }
140
+ }
141
+
142
+ @media (pointer: fine) {
143
+ [vaul-handle-hitarea] {
144
+ width: 100%;
145
+ height: 100%;
146
+ }
147
+ }
148
+
149
+ @media (hover: hover) and (pointer: fine) {
150
+ [vaul-drawer] {
151
+ user-select: none;
152
+ }
153
+ }
154
+
package/tsconfig.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
- "extends": "../tsconfig.lux.base.json",
2
+ "extends": "../tsconfig.modules.base.json",
3
3
  "include": [
4
4
  "**/*.ts",
5
- "**/*.tsx",
5
+ "**/*.tsx"
6
6
  ],
7
7
  "exclude": [
8
- "node_modules",
9
- ],
8
+ "node_modules"
9
+ ]
10
10
  }
@@ -1,30 +0,0 @@
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
- }