@hanzo/commerce 7.1.5 → 7.1.6
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/Icons.tsx +34 -34
- package/components/add-to-cart-widget.tsx +181 -181
- package/components/buy/buy-card.tsx +259 -259
- package/components/buy/carousel-buy-card.tsx +245 -245
- package/components/buy/multi-family/all-variants-carousel.tsx +261 -261
- package/components/buy/multi-family/family-carousel/index.tsx +77 -77
- package/components/buy/multi-family/family-carousel/slide.tsx +83 -83
- package/components/buy/multi-family/family-carousel/state.ts +87 -87
- package/components/buy/multi-family/index.ts +2 -2
- package/components/buy/single-family-selector.tsx +90 -90
- package/components/buy/title-and-byline.tsx +25 -25
- package/components/cart/cart-accordian.tsx +59 -59
- package/components/cart/cart-panel/cart-line-item.tsx +76 -76
- package/components/cart/cart-panel/index.tsx +161 -159
- package/components/cart/cart-panel/promo-code.tsx +98 -98
- package/components/checkout/payment-step-form/card-icon-row.tsx +26 -26
- package/components/checkout/payment-step-form/card-icons/amex.tsx +32 -32
- package/components/checkout/payment-step-form/card-icons/diners-club.tsx +13 -13
- package/components/checkout/payment-step-form/card-icons/discover.tsx +25 -25
- package/components/checkout/payment-step-form/card-icons/jcb.tsx +26 -26
- package/components/checkout/payment-step-form/card-icons/mastercard.tsx +27 -27
- package/components/checkout/payment-step-form/card-icons/visa.tsx +25 -25
- package/components/checkout/payment-step-form/cc-button.tsx +17 -17
- package/components/checkout/payment-step-form/contact-form.tsx +49 -49
- package/components/checkout/payment-step-form/crypto-icons/btc.tsx +11 -11
- package/components/checkout/payment-step-form/crypto-icons/eth.tsx +20 -20
- package/components/checkout/payment-step-form/crypto-icons/usdt.tsx +13 -13
- package/components/checkout/payment-step-form/index.tsx +122 -122
- package/components/checkout/payment-step-form/methods/bank-transfer.tsx +79 -79
- package/components/checkout/payment-step-form/methods/card.tsx +232 -232
- package/components/checkout/payment-step-form/methods/crypto.tsx +227 -227
- package/components/checkout/payment-step-form/methods/index.ts +22 -22
- package/components/checkout/shipping-step-form.tsx +172 -172
- package/components/index.ts +12 -12
- package/components/item/product-card.tsx +48 -48
- package/components/item-selector/button.tsx +188 -188
- package/components/item-selector/carousel/index.tsx +197 -197
- package/components/item-selector/carousel/slider.tsx +40 -40
- package/components/item-selector/index.ts +5 -5
- package/components/item-selector/quantity-indicator.tsx +48 -48
- package/components/node-tabs/index.tsx +91 -91
- package/components/node-tabs/node-image.tsx +31 -31
- package/context/index.tsx +45 -45
- package/index.ts +12 -12
- package/package.json +60 -60
- package/service/debug.ts +41 -41
- package/service/get-instance.ts +2 -2
- package/service/impls/standalone/actual-line-item.ts +121 -121
- package/service/impls/standalone/index.ts +62 -62
- package/service/impls/standalone/localStorage.ts +34 -34
- package/service/impls/standalone/orders/firebase-app.ts +14 -14
- package/service/impls/standalone/orders/index.ts +128 -128
- package/service/impls/standalone/standalone-service.ts +550 -550
- package/service/path-utils.ts +25 -25
- package/service/sep.ts +6 -6
- package/tsconfig.json +10 -10
- package/types/README.md +1 -1
- package/types/category-node.ts +49 -49
- package/types/checkout.ts +46 -46
- package/types/commerce-config.ts +13 -13
- package/types/commerce-service.ts +121 -121
- package/types/family.ts +25 -25
- package/types/index.ts +15 -15
- package/types/item-selector.ts +96 -96
- package/types/line-item.ts +29 -29
- package/types/multi-family-selector-props.ts +19 -19
- package/types/product.ts +20 -20
- package/types/promo.ts +9 -9
- package/types/selection-ui-specifier.ts +51 -51
- package/types/string-mutator.ts +13 -13
- package/types/token-separators.ts +7 -7
- package/util/analytics.ts +20 -20
- package/util/countries.ts +195 -195
- package/util/error.ts +34 -34
- package/util/index.ts +70 -70
- package/util/item-selector-options-accessor.ts +34 -34
- package/util/line-item-ref.ts +23 -23
- package/util/multi-family-selector-options-accessor.ts +14 -14
- package/util/obs-string-mutator.ts +22 -22
- package/util/product-media-accessor.ts +58 -58
- package/util/promo-codes.ts +108 -108
- package/util/selection-ui-specifiers.ts +29 -29
- package/util/square-payment.ts +42 -42
- package/util/use-sync-sku-param-w-current-item.ts +88 -88
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import { observer } from 'mobx-react-lite'
|
|
4
|
-
|
|
5
|
-
import { ChevronRight } from 'lucide-react'
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
Accordion,
|
|
9
|
-
AccordionContent,
|
|
10
|
-
AccordionItem,
|
|
11
|
-
AccordionTrigger,
|
|
12
|
-
} from '@hanzo/ui/primitives'
|
|
13
|
-
|
|
14
|
-
import { formatCurrencyValue } from '../../util'
|
|
15
|
-
import { useCommerce } from '../../context'
|
|
16
|
-
|
|
17
|
-
import CartPanel from './cart-panel'
|
|
18
|
-
|
|
19
|
-
const CartAccordian: React.FC<{
|
|
20
|
-
icon?: React.ReactNode
|
|
21
|
-
className?: string
|
|
22
|
-
}> = observer(({
|
|
23
|
-
icon,
|
|
24
|
-
className=''
|
|
25
|
-
}) => {
|
|
26
|
-
const cmmc = useCommerce()
|
|
27
|
-
return (
|
|
28
|
-
<Accordion type="single" collapsible className={className}>
|
|
29
|
-
<AccordionItem value="cart" className='w-full border-b-0'>
|
|
30
|
-
<AccordionTrigger className='!no-underline group flex justify-between'>
|
|
31
|
-
<div className='flex gap-0 items-center'>
|
|
32
|
-
{icon}
|
|
33
|
-
<h5 className='text-sm sm:text-xl grow'>
|
|
34
|
-
<span className='group-data-[state=open]:hidden' >Order Total:</span>
|
|
35
|
-
<span className='group-data-[state=closed]:hidden' >Your Order</span>
|
|
36
|
-
</h5>
|
|
37
|
-
</div>
|
|
38
|
-
<div className='flex gap-1 items-center'>
|
|
39
|
-
<h5 className='text-sm sm:text-xl grow truncate'>{formatCurrencyValue(cmmc.promoAppliedCartTotal)}</h5>
|
|
40
|
-
<ChevronRight className="h-5 w-5 -mr-2 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
|
41
|
-
</div>
|
|
42
|
-
</AccordionTrigger>
|
|
43
|
-
<AccordionContent className='data-[state=open]:mb-4'>
|
|
44
|
-
<CartPanel
|
|
45
|
-
className='w-full '
|
|
46
|
-
scrollAfter={4}
|
|
47
|
-
scrollHeightClx='h-[350px]'
|
|
48
|
-
itemClx='mt-2'
|
|
49
|
-
totalClx='sticky px-1 pr-2 border-t -bottom-[1px] bg-background'
|
|
50
|
-
showShipping
|
|
51
|
-
showPromoCode
|
|
52
|
-
/>
|
|
53
|
-
</AccordionContent>
|
|
54
|
-
</AccordionItem>
|
|
55
|
-
</Accordion>
|
|
56
|
-
)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
export default CartAccordian
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { observer } from 'mobx-react-lite'
|
|
4
|
+
|
|
5
|
+
import { ChevronRight } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Accordion,
|
|
9
|
+
AccordionContent,
|
|
10
|
+
AccordionItem,
|
|
11
|
+
AccordionTrigger,
|
|
12
|
+
} from '@hanzo/ui/primitives'
|
|
13
|
+
|
|
14
|
+
import { formatCurrencyValue } from '../../util'
|
|
15
|
+
import { useCommerce } from '../../context'
|
|
16
|
+
|
|
17
|
+
import CartPanel from './cart-panel'
|
|
18
|
+
|
|
19
|
+
const CartAccordian: React.FC<{
|
|
20
|
+
icon?: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
}> = observer(({
|
|
23
|
+
icon,
|
|
24
|
+
className=''
|
|
25
|
+
}) => {
|
|
26
|
+
const cmmc = useCommerce()
|
|
27
|
+
return (
|
|
28
|
+
<Accordion type="single" collapsible className={className}>
|
|
29
|
+
<AccordionItem value="cart" className='w-full border-b-0'>
|
|
30
|
+
<AccordionTrigger className='!no-underline group flex justify-between'>
|
|
31
|
+
<div className='flex gap-0 items-center'>
|
|
32
|
+
{icon}
|
|
33
|
+
<h5 className='text-sm sm:text-xl grow'>
|
|
34
|
+
<span className='group-data-[state=open]:hidden' >Order Total:</span>
|
|
35
|
+
<span className='group-data-[state=closed]:hidden' >Your Order</span>
|
|
36
|
+
</h5>
|
|
37
|
+
</div>
|
|
38
|
+
<div className='flex gap-1 items-center'>
|
|
39
|
+
<h5 className='text-sm sm:text-xl grow truncate'>{formatCurrencyValue(cmmc.promoAppliedCartTotal)}</h5>
|
|
40
|
+
<ChevronRight className="h-5 w-5 -mr-2 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
|
41
|
+
</div>
|
|
42
|
+
</AccordionTrigger>
|
|
43
|
+
<AccordionContent className='data-[state=open]:mb-4'>
|
|
44
|
+
<CartPanel
|
|
45
|
+
className='w-full '
|
|
46
|
+
scrollAfter={4}
|
|
47
|
+
scrollHeightClx='h-[350px]'
|
|
48
|
+
itemClx='mt-2'
|
|
49
|
+
totalClx='sticky px-1 pr-2 border-t -bottom-[1px] bg-background'
|
|
50
|
+
showShipping
|
|
51
|
+
showPromoCode
|
|
52
|
+
/>
|
|
53
|
+
</AccordionContent>
|
|
54
|
+
</AccordionItem>
|
|
55
|
+
</Accordion>
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export default CartAccordian
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { observer } from 'mobx-react-lite'
|
|
5
|
-
|
|
6
|
-
import { cn } from '@hanzo/ui/util'
|
|
7
|
-
import { Image } from '@hanzo/ui/primitives'
|
|
8
|
-
|
|
9
|
-
import type { LineItem } from '../../../types'
|
|
10
|
-
import { formatCurrencyValue } from '../../../util'
|
|
11
|
-
import AddToCartWidget from '../../add-to-cart-widget'
|
|
12
|
-
import { useCommerce } from '../../../context'
|
|
13
|
-
|
|
14
|
-
const DEF_IMG_SIZE=40
|
|
15
|
-
|
|
16
|
-
const renderTitle = (t: string): React.ReactNode => {
|
|
17
|
-
const toks = t.split(', ')
|
|
18
|
-
if (toks.length === 2) {
|
|
19
|
-
return <><p>{toks[0]},</p><p className=''>{toks[1]}</p></>
|
|
20
|
-
}
|
|
21
|
-
return <p>{t}</p>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const CartLineItem: React.FC<{
|
|
25
|
-
item: LineItem
|
|
26
|
-
className?: string
|
|
27
|
-
imgSizePx?: number
|
|
28
|
-
showPromoCode?: boolean
|
|
29
|
-
itemClicked?: (item: LineItem) => void
|
|
30
|
-
selected?: boolean
|
|
31
|
-
}> = observer(({
|
|
32
|
-
item,
|
|
33
|
-
className='',
|
|
34
|
-
imgSizePx=DEF_IMG_SIZE,
|
|
35
|
-
showPromoCode=false,
|
|
36
|
-
itemClicked,
|
|
37
|
-
selected=false
|
|
38
|
-
}) => {
|
|
39
|
-
|
|
40
|
-
const cmmc = useCommerce()
|
|
41
|
-
const promoPrice = showPromoCode ? cmmc.itemPromoPrice(item) : undefined
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div className={cn('flex flex-col justify-start items-start text-sm font-sans', className)}>
|
|
45
|
-
<div
|
|
46
|
-
className={
|
|
47
|
-
'flex flex-row justify-between items-center gap-1 ' +
|
|
48
|
-
(itemClicked && !selected ? 'cursor-pointer ' : 'cursor-default ')
|
|
49
|
-
}
|
|
50
|
-
onClick={() => {itemClicked && itemClicked(item)}}
|
|
51
|
-
>
|
|
52
|
-
{/* 1px gap between image and border for better emphasis w small images */}
|
|
53
|
-
<div className={selected ? 'border-foreground p-[1px] border rounded-sm' : 'p-[2px]'}>
|
|
54
|
-
{item.img ? (
|
|
55
|
-
<Image def={item.img} constrainTo={{w: imgSizePx, h: imgSizePx}} />
|
|
56
|
-
) : ( // placeholder so things align
|
|
57
|
-
<div style={{height: imgSizePx, width: imgSizePx}} className='bg-level-3'/>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
<div className='grow leading-tight ml-1'>{renderTitle(item.title)}</div>
|
|
61
|
-
</div>
|
|
62
|
-
<div className='flex flex-row items-center justify-between w-full'>
|
|
63
|
-
<div className='flex flex-row items-center'>
|
|
64
|
-
<AddToCartWidget variant='minimal' item={item} buttonClx='!h-8 md:!h-6' />
|
|
65
|
-
{item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatCurrencyValue(item.price)}</span>)}
|
|
66
|
-
</div>
|
|
67
|
-
<div className='flex flex-row gap-1 items-center justify-end'>
|
|
68
|
-
<span className={promoPrice ? 'line-through text-muted-2' : ''}>{formatCurrencyValue(item.price * item.quantity)}</span>
|
|
69
|
-
{promoPrice && <span>{formatCurrencyValue(promoPrice * item.quantity)}</span>}
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
export default CartLineItem
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
import { Image } from '@hanzo/ui/primitives'
|
|
8
|
+
|
|
9
|
+
import type { LineItem } from '../../../types'
|
|
10
|
+
import { formatCurrencyValue } from '../../../util'
|
|
11
|
+
import AddToCartWidget from '../../add-to-cart-widget'
|
|
12
|
+
import { useCommerce } from '../../../context'
|
|
13
|
+
|
|
14
|
+
const DEF_IMG_SIZE=40
|
|
15
|
+
|
|
16
|
+
const renderTitle = (t: string): React.ReactNode => {
|
|
17
|
+
const toks = t.split(', ')
|
|
18
|
+
if (toks.length === 2) {
|
|
19
|
+
return <><p>{toks[0]},</p><p className=''>{toks[1]}</p></>
|
|
20
|
+
}
|
|
21
|
+
return <p>{t}</p>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CartLineItem: React.FC<{
|
|
25
|
+
item: LineItem
|
|
26
|
+
className?: string
|
|
27
|
+
imgSizePx?: number
|
|
28
|
+
showPromoCode?: boolean
|
|
29
|
+
itemClicked?: (item: LineItem) => void
|
|
30
|
+
selected?: boolean
|
|
31
|
+
}> = observer(({
|
|
32
|
+
item,
|
|
33
|
+
className='',
|
|
34
|
+
imgSizePx=DEF_IMG_SIZE,
|
|
35
|
+
showPromoCode=false,
|
|
36
|
+
itemClicked,
|
|
37
|
+
selected=false
|
|
38
|
+
}) => {
|
|
39
|
+
|
|
40
|
+
const cmmc = useCommerce()
|
|
41
|
+
const promoPrice = showPromoCode ? cmmc.itemPromoPrice(item) : undefined
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('flex flex-col justify-start items-start text-sm font-sans', className)}>
|
|
45
|
+
<div
|
|
46
|
+
className={
|
|
47
|
+
'flex flex-row justify-between items-center gap-1 ' +
|
|
48
|
+
(itemClicked && !selected ? 'cursor-pointer ' : 'cursor-default ')
|
|
49
|
+
}
|
|
50
|
+
onClick={() => {itemClicked && itemClicked(item)}}
|
|
51
|
+
>
|
|
52
|
+
{/* 1px gap between image and border for better emphasis w small images */}
|
|
53
|
+
<div className={selected ? 'border-foreground p-[1px] border rounded-sm' : 'p-[2px]'}>
|
|
54
|
+
{item.img ? (
|
|
55
|
+
<Image def={item.img} constrainTo={{w: imgSizePx, h: imgSizePx}} />
|
|
56
|
+
) : ( // placeholder so things align
|
|
57
|
+
<div style={{height: imgSizePx, width: imgSizePx}} className='bg-level-3'/>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<div className='grow leading-tight ml-1'>{renderTitle(item.title)}</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className='flex flex-row items-center justify-between w-full'>
|
|
63
|
+
<div className='flex flex-row items-center'>
|
|
64
|
+
<AddToCartWidget variant='minimal' item={item} buttonClx='!h-8 md:!h-6' />
|
|
65
|
+
{item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatCurrencyValue(item.price)}</span>)}
|
|
66
|
+
</div>
|
|
67
|
+
<div className='flex flex-row gap-1 items-center justify-end'>
|
|
68
|
+
<span className={promoPrice ? 'line-through text-muted-2' : ''}>{formatCurrencyValue(item.price * item.quantity)}</span>
|
|
69
|
+
{promoPrice && <span>{formatCurrencyValue(promoPrice * item.quantity)}</span>}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export default CartLineItem
|
|
@@ -1,159 +1,161 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React, { Suspense, type PropsWithChildren } from 'react'
|
|
3
|
-
import { observer } from 'mobx-react-lite'
|
|
4
|
-
|
|
5
|
-
import { Button, ScrollArea } from '@hanzo/ui/primitives'
|
|
6
|
-
import { cn } from '@hanzo/ui/util'
|
|
7
|
-
|
|
8
|
-
import { useCommerce } from '../../../context'
|
|
9
|
-
import { formatCurrencyValue } from '../../../util'
|
|
10
|
-
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
11
|
-
|
|
12
|
-
import CartLineItem from './cart-line-item'
|
|
13
|
-
import PromoCode from './promo-code'
|
|
14
|
-
import type { LineItem } from '../../../types'
|
|
15
|
-
|
|
16
|
-
const CartPanel: React.FC<PropsWithChildren & {
|
|
17
|
-
/** fix size and scroll after 'scrollAfter items in teh cart. */
|
|
18
|
-
scrollAfter?: number
|
|
19
|
-
scrollHeightClx?: string
|
|
20
|
-
imgSizePx?: number
|
|
21
|
-
className?: string
|
|
22
|
-
itemClx?: string
|
|
23
|
-
listClx?: string
|
|
24
|
-
noItemsClx?: string
|
|
25
|
-
totalClx?: string
|
|
26
|
-
buttonClx?: string
|
|
27
|
-
showPromoCode?: boolean
|
|
28
|
-
showShipping?: boolean
|
|
29
|
-
selectItems?: boolean
|
|
30
|
-
/** if not provided, 'checkout' button will be rendered */
|
|
31
|
-
handleCheckout?: () => void
|
|
32
|
-
}> = observer(({
|
|
33
|
-
/** If provided, 'children' is rendered above the items. eg, a heading. */
|
|
34
|
-
children,
|
|
35
|
-
scrollAfter=5,
|
|
36
|
-
scrollHeightClx='h-[70vh]',
|
|
37
|
-
imgSizePx=40,
|
|
38
|
-
className='',
|
|
39
|
-
itemClx='',
|
|
40
|
-
listClx='',
|
|
41
|
-
noItemsClx='',
|
|
42
|
-
totalClx='',
|
|
43
|
-
buttonClx='',
|
|
44
|
-
showPromoCode=false,
|
|
45
|
-
showShipping=false,
|
|
46
|
-
selectItems=false,
|
|
47
|
-
handleCheckout,
|
|
48
|
-
}) => {
|
|
49
|
-
|
|
50
|
-
const cmmc = useCommerce()
|
|
51
|
-
if (!cmmc) {
|
|
52
|
-
return <div />
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const _handleCheckout = () => {
|
|
56
|
-
sendGAEvent('begin_checkout', {
|
|
57
|
-
currency: 'USD',
|
|
58
|
-
value: cmmc.cartTotal,
|
|
59
|
-
items: cmmc.cartItems.map((item) => ({
|
|
60
|
-
item_id: item.sku,
|
|
61
|
-
item_name: item.title,
|
|
62
|
-
item_category: item.familyId,
|
|
63
|
-
price: item.price,
|
|
64
|
-
quantity: item.quantity
|
|
65
|
-
})),
|
|
66
|
-
})
|
|
67
|
-
sendFBEvent('InitiateCheckout', {
|
|
68
|
-
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
69
|
-
contents: cmmc.cartItems.map(item => ({
|
|
70
|
-
id: item.sku,
|
|
71
|
-
quantity: item.quantity
|
|
72
|
-
})),
|
|
73
|
-
num_items: cmmc.cartItems.length,
|
|
74
|
-
value: cmmc.cartTotal,
|
|
75
|
-
currency: 'USD',
|
|
76
|
-
})
|
|
77
|
-
handleCheckout && handleCheckout()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const itemClicked = (item: LineItem) => {
|
|
81
|
-
cmmc.setCurrentItem(item.sku)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const MainStuff: React.FC = observer(() => (<>
|
|
85
|
-
{cmmc.cartEmpty ? (
|
|
86
|
-
<p className={cn('text-center my-3', noItemsClx)}>No items</p>
|
|
87
|
-
) : (<>
|
|
88
|
-
{cmmc.cartItems.map((item) => (
|
|
89
|
-
<CartLineItem
|
|
90
|
-
key={`mobile-${item.sku}`}
|
|
91
|
-
imgSizePx={imgSizePx}
|
|
92
|
-
item={item}
|
|
93
|
-
className={cn('mb-2', itemClx)}
|
|
94
|
-
showPromoCode={showPromoCode}
|
|
95
|
-
itemClicked={selectItems ? itemClicked : undefined}
|
|
96
|
-
selected={selectItems ? cmmc.currentItem?.sku === item.sku : false}
|
|
97
|
-
/>
|
|
98
|
-
))}
|
|
99
|
-
</>)}
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<span className='
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<span className='
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<span className='
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { Suspense, type PropsWithChildren } from 'react'
|
|
3
|
+
import { observer } from 'mobx-react-lite'
|
|
4
|
+
|
|
5
|
+
import { Button, ScrollArea } from '@hanzo/ui/primitives'
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
import { useCommerce } from '../../../context'
|
|
9
|
+
import { formatCurrencyValue } from '../../../util'
|
|
10
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
11
|
+
|
|
12
|
+
import CartLineItem from './cart-line-item'
|
|
13
|
+
import PromoCode from './promo-code'
|
|
14
|
+
import type { LineItem } from '../../../types'
|
|
15
|
+
|
|
16
|
+
const CartPanel: React.FC<PropsWithChildren & {
|
|
17
|
+
/** fix size and scroll after 'scrollAfter items in teh cart. */
|
|
18
|
+
scrollAfter?: number
|
|
19
|
+
scrollHeightClx?: string
|
|
20
|
+
imgSizePx?: number
|
|
21
|
+
className?: string
|
|
22
|
+
itemClx?: string
|
|
23
|
+
listClx?: string
|
|
24
|
+
noItemsClx?: string
|
|
25
|
+
totalClx?: string
|
|
26
|
+
buttonClx?: string
|
|
27
|
+
showPromoCode?: boolean
|
|
28
|
+
showShipping?: boolean
|
|
29
|
+
selectItems?: boolean
|
|
30
|
+
/** if not provided, 'checkout' button will be rendered */
|
|
31
|
+
handleCheckout?: () => void
|
|
32
|
+
}> = observer(({
|
|
33
|
+
/** If provided, 'children' is rendered above the items. eg, a heading. */
|
|
34
|
+
children,
|
|
35
|
+
scrollAfter=5,
|
|
36
|
+
scrollHeightClx='h-[70vh]',
|
|
37
|
+
imgSizePx=40,
|
|
38
|
+
className='',
|
|
39
|
+
itemClx='',
|
|
40
|
+
listClx='',
|
|
41
|
+
noItemsClx='',
|
|
42
|
+
totalClx='',
|
|
43
|
+
buttonClx='',
|
|
44
|
+
showPromoCode=false,
|
|
45
|
+
showShipping=false,
|
|
46
|
+
selectItems=false,
|
|
47
|
+
handleCheckout,
|
|
48
|
+
}) => {
|
|
49
|
+
|
|
50
|
+
const cmmc = useCommerce()
|
|
51
|
+
if (!cmmc) {
|
|
52
|
+
return <div />
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const _handleCheckout = () => {
|
|
56
|
+
sendGAEvent('begin_checkout', {
|
|
57
|
+
currency: 'USD',
|
|
58
|
+
value: cmmc.cartTotal,
|
|
59
|
+
items: cmmc.cartItems.map((item) => ({
|
|
60
|
+
item_id: item.sku,
|
|
61
|
+
item_name: item.title,
|
|
62
|
+
item_category: item.familyId,
|
|
63
|
+
price: item.price,
|
|
64
|
+
quantity: item.quantity
|
|
65
|
+
})),
|
|
66
|
+
})
|
|
67
|
+
sendFBEvent('InitiateCheckout', {
|
|
68
|
+
content_ids: cmmc.cartItems.map((item) => item.sku),
|
|
69
|
+
contents: cmmc.cartItems.map(item => ({
|
|
70
|
+
id: item.sku,
|
|
71
|
+
quantity: item.quantity
|
|
72
|
+
})),
|
|
73
|
+
num_items: cmmc.cartItems.length,
|
|
74
|
+
value: cmmc.cartTotal,
|
|
75
|
+
currency: 'USD',
|
|
76
|
+
})
|
|
77
|
+
handleCheckout && handleCheckout()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const itemClicked = (item: LineItem) => {
|
|
81
|
+
cmmc.setCurrentItem(item.sku)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const MainStuff: React.FC = observer(() => (<>
|
|
85
|
+
{cmmc.cartEmpty ? (
|
|
86
|
+
<p className={cn('text-center my-3', noItemsClx)}>No items</p>
|
|
87
|
+
) : (<>
|
|
88
|
+
{cmmc.cartItems.map((item) => (
|
|
89
|
+
<CartLineItem
|
|
90
|
+
key={`mobile-${item.sku}`}
|
|
91
|
+
imgSizePx={imgSizePx}
|
|
92
|
+
item={item}
|
|
93
|
+
className={cn('mb-2', itemClx)}
|
|
94
|
+
showPromoCode={showPromoCode}
|
|
95
|
+
itemClicked={selectItems ? itemClicked : undefined}
|
|
96
|
+
selected={selectItems ? cmmc.currentItem?.sku === item.sku : false}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</>)}
|
|
100
|
+
<div className={totalClx}>
|
|
101
|
+
{showPromoCode && (
|
|
102
|
+
<Suspense>
|
|
103
|
+
<PromoCode/>
|
|
104
|
+
</Suspense>
|
|
105
|
+
)}
|
|
106
|
+
{(showShipping || showPromoCode) && (
|
|
107
|
+
<div className='flex flex-col gap-1 py-2 border-t text-sm'>
|
|
108
|
+
<p className='flex justify-between'>
|
|
109
|
+
<span className='text-muted-1'>Subtotal</span>
|
|
110
|
+
<span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatCurrencyValue(cmmc.cartTotal)}</span>
|
|
111
|
+
</p>
|
|
112
|
+
{cmmc.promoAppliedCartTotal !== cmmc.cartTotal && (
|
|
113
|
+
<p className='flex justify-between'>
|
|
114
|
+
<span className='text-muted-1'>Promo Discount</span>
|
|
115
|
+
<span className='font-semibold'>-{formatCurrencyValue(cmmc.cartTotal - cmmc.promoAppliedCartTotal)}</span>
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
{showShipping && (
|
|
119
|
+
<p className='flex justify-between'>
|
|
120
|
+
<span className='text-muted-1'>Shipping</span>
|
|
121
|
+
<span className='font-semibold'>Free Global Shipping</span>
|
|
122
|
+
</p>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
<p className={cn('border-t py-2 flex justify-between')}>
|
|
127
|
+
TOTAL
|
|
128
|
+
<span className='font-semibold'>{formatCurrencyValue(showPromoCode ? cmmc.promoAppliedCartTotal : cmmc.cartTotal)}</span>
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
</>))
|
|
132
|
+
|
|
133
|
+
const scrolling = (): boolean => (cmmc.cartItems.length > scrollAfter)
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={cn('border p-4 rounded-lg flex flex-col', className, scrolling() ? scrollHeightClx : 'h-auto')}>
|
|
137
|
+
{children}
|
|
138
|
+
{scrolling() ? (
|
|
139
|
+
<ScrollArea className={cn('mt-2 w-full shrink py-0', listClx)}>
|
|
140
|
+
<MainStuff />
|
|
141
|
+
</ScrollArea>
|
|
142
|
+
) : (
|
|
143
|
+
<div className={cn('mt-2 w-full', listClx)}>
|
|
144
|
+
<MainStuff />
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
{handleCheckout && !cmmc.cartEmpty && (
|
|
148
|
+
<Button
|
|
149
|
+
onClick={_handleCheckout}
|
|
150
|
+
variant='primary'
|
|
151
|
+
rounded='lg'
|
|
152
|
+
className={cn('mt-12 mx-auto w-full sm:max-w-[220px]', buttonClx)}
|
|
153
|
+
>
|
|
154
|
+
Checkout
|
|
155
|
+
</Button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
export default CartPanel
|