@hanzo/commerce 4.9.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/add-to-cart-widget.tsx +0 -1
- package/components/buy-item/select-category-and-item-widget.tsx +2 -2
- package/components/buy-item/select-category-item-card.tsx +2 -2
- package/components/{checkout-panel/cart-accordian.tsx → cart-accordian.tsx} +21 -10
- package/components/cart-panel/cart-line-item.tsx +22 -16
- package/components/cart-panel/index.tsx +90 -34
- package/components/cart-panel/promo-code.tsx +98 -0
- package/components/category-item-radio-selector.tsx +2 -2
- package/components/category-item-scroll-selector.tsx +2 -2
- package/components/index.ts +4 -1
- package/components/item-carousel.tsx +80 -0
- package/components/{checkout-panel/steps/payment/cards/index.tsx → payment-step-form/card-icon-row.tsx} +8 -7
- package/components/{checkout-panel/steps/payment → payment-step-form}/cc-button.tsx +1 -1
- package/components/payment-step-form/contact-form.tsx +49 -0
- package/components/{checkout-panel/steps/payment → payment-step-form}/index.tsx +29 -56
- package/components/payment-step-form/methods/bank-transfer.tsx +79 -0
- package/components/payment-step-form/methods/card.tsx +237 -0
- package/components/{checkout-panel/steps/payment/pay-with-crypto.tsx → payment-step-form/methods/crypto.tsx} +10 -24
- package/components/payment-step-form/methods/index.ts +23 -0
- package/components/product-card.tsx +2 -2
- package/components/select-category-item-panel.tsx +2 -2
- package/components/{checkout-panel/steps/shipping-info.tsx → shipping-step-form.tsx} +6 -8
- package/index.ts +1 -1
- package/package.json +2 -2
- package/service/context.tsx +1 -0
- package/service/impls/standalone/standalone-service.ts +56 -4
- package/types/checkout.ts +42 -1
- package/types/commerce-service.ts +9 -1
- package/types/index.ts +2 -1
- package/types/promo.ts +10 -0
- package/util/index.ts +3 -2
- package/util/promo-codes.ts +46 -0
- package/components/cart-panel/products-carousel.tsx +0 -60
- package/components/checkout-panel/_confirm-order.tsx_unused +0 -143
- package/components/checkout-panel/close-button.tsx +0 -24
- package/components/checkout-panel/desktop.tsx +0 -46
- package/components/checkout-panel/icons/bag-icon.tsx +0 -10
- package/components/checkout-panel/index.tsx +0 -108
- package/components/checkout-panel/mobile.tsx +0 -41
- package/components/checkout-panel/steps/payment/contact-info.tsx +0 -56
- package/components/checkout-panel/steps/payment/pay-with-bank-transfer.tsx +0 -110
- package/components/checkout-panel/steps/payment/pay-with-card.tsx +0 -246
- package/components/checkout-panel/steps/thank-you.tsx +0 -18
- package/components/checkout-panel/steps/types.ts +0 -14
- /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/amex.tsx +0 -0
- /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/diners-club.tsx +0 -0
- /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/discover.tsx +0 -0
- /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/jcb.tsx +0 -0
- /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/mastercard.tsx +0 -0
- /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/visa.tsx +0 -0
- /package/components/{checkout-panel/icons → payment-step-form/crypto-icons}/btc.tsx +0 -0
- /package/components/{checkout-panel/icons → payment-step-form/crypto-icons}/eth.tsx +0 -0
- /package/components/{checkout-panel/icons → payment-step-form/crypto-icons}/usdt.tsx +0 -0
|
@@ -7,12 +7,12 @@ import { cn } from '@hanzo/ui/util'
|
|
|
7
7
|
|
|
8
8
|
import type { FacetValueDesc, FacetsValue, LineItem } from '../../types'
|
|
9
9
|
import { useCommerce } from '../../service/context'
|
|
10
|
-
import {
|
|
10
|
+
import { formatCurrencyValue } from '../../util'
|
|
11
11
|
|
|
12
12
|
import FacetTogglesWidget from '../facet-values-widget'
|
|
13
13
|
|
|
14
14
|
const formatItem = (item: LineItem, withQuantity: boolean = false): string => (
|
|
15
|
-
`${item.titleAsOption}, ${
|
|
15
|
+
`${item.titleAsOption}, ${formatCurrencyValue(item.price)}${(withQuantity && item.quantity > 0) ? ` (${item.quantity})` : ''}`
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
const SelectCategoryAndItemWidget: React.FC<{
|
|
@@ -10,7 +10,7 @@ import type { ItemSelector, LineItem } from '../../types'
|
|
|
10
10
|
import AddToCartWidget from '../add-to-cart-widget'
|
|
11
11
|
import CategoryItemRadioSelector from '../category-item-radio-selector'
|
|
12
12
|
import CategoryItemScrollSelector from '../category-item-scroll-selector'
|
|
13
|
-
import {
|
|
13
|
+
import { formatCurrencyValue } from '../../util'
|
|
14
14
|
|
|
15
15
|
const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & ItemSelector & {
|
|
16
16
|
isLoading?: boolean
|
|
@@ -40,7 +40,7 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
|
|
|
40
40
|
const item = category.products[0] as LineItem
|
|
41
41
|
return (
|
|
42
42
|
<div className={cn('flex flex-col justify-center items-center ' + (mobilePicker ? 'h-[180px] ' : 'h-auto min-h-24'), className)}>
|
|
43
|
-
<p className='text-lg text-center font-semibold'>{item.titleAsOption + ', ' +
|
|
43
|
+
<p className='text-lg text-center font-semibold'>{item.titleAsOption + ', ' + formatCurrencyValue(item.price)}</p>
|
|
44
44
|
</div>
|
|
45
45
|
)
|
|
46
46
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { observer } from 'mobx-react-lite'
|
|
4
4
|
|
|
5
|
+
import { ChevronRight } from 'lucide-react'
|
|
6
|
+
|
|
5
7
|
import {
|
|
6
8
|
Accordion,
|
|
7
9
|
AccordionContent,
|
|
@@ -9,35 +11,44 @@ import {
|
|
|
9
11
|
AccordionTrigger,
|
|
10
12
|
} from '@hanzo/ui/primitives'
|
|
11
13
|
|
|
12
|
-
import {
|
|
14
|
+
import { formatCurrencyValue, useCommerce } from '..'
|
|
13
15
|
|
|
14
|
-
import CartPanel from '
|
|
15
|
-
import BagIcon from './icons/bag-icon'
|
|
16
|
-
import { ChevronRight } from 'lucide-react'
|
|
16
|
+
import CartPanel from './cart-panel'
|
|
17
17
|
|
|
18
|
-
const CartAccordian: React.FC<{
|
|
18
|
+
const CartAccordian: React.FC<{
|
|
19
|
+
icon?: React.ReactNode
|
|
20
|
+
className?: string
|
|
21
|
+
}> = observer(({
|
|
22
|
+
icon,
|
|
19
23
|
className=''
|
|
20
24
|
}) => {
|
|
21
|
-
|
|
22
25
|
const cmmc = useCommerce()
|
|
23
26
|
return (
|
|
24
27
|
<Accordion type="single" collapsible className={className}>
|
|
25
28
|
<AccordionItem value="cart" className='w-full border-b-0'>
|
|
26
29
|
<AccordionTrigger className='!no-underline group flex justify-between'>
|
|
27
|
-
<div className='flex gap-
|
|
28
|
-
|
|
30
|
+
<div className='flex gap-0 items-center'>
|
|
31
|
+
{icon}
|
|
29
32
|
<h5 className='text-sm sm:text-xl grow'>
|
|
30
33
|
<span className='group-data-[state=open]:hidden' >Order Total:</span>
|
|
31
34
|
<span className='group-data-[state=closed]:hidden' >Your Order</span>
|
|
32
35
|
</h5>
|
|
33
36
|
</div>
|
|
34
37
|
<div className='flex gap-1 items-center'>
|
|
35
|
-
<h5 className='text-sm sm:text-xl grow truncate'>{
|
|
38
|
+
<h5 className='text-sm sm:text-xl grow truncate'>{formatCurrencyValue(cmmc.promoAppliedCartTotal)}</h5>
|
|
36
39
|
<ChevronRight className="h-5 w-5 -mr-2 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
|
37
40
|
</div>
|
|
38
41
|
</AccordionTrigger>
|
|
39
42
|
<AccordionContent className='data-[state=open]:mb-4'>
|
|
40
|
-
<CartPanel
|
|
43
|
+
<CartPanel
|
|
44
|
+
className='w-full '
|
|
45
|
+
scrollAfter={4}
|
|
46
|
+
scrollHeightClx='h-[350px]'
|
|
47
|
+
itemClx='mt-2'
|
|
48
|
+
totalClx='sticky px-1 pr-2 border-t -bottom-[1px] bg-background'
|
|
49
|
+
showShipping
|
|
50
|
+
showPromoCode
|
|
51
|
+
/>
|
|
41
52
|
</AccordionContent>
|
|
42
53
|
</AccordionItem>
|
|
43
54
|
</Accordion>
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
1
3
|
import React from 'react'
|
|
2
|
-
import { observer } from 'mobx-react-lite'
|
|
3
4
|
import Image from 'next/image'
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
4
6
|
|
|
5
7
|
import { cn } from '@hanzo/ui/util'
|
|
6
8
|
|
|
7
9
|
import type { LineItem } from '../../types'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
+
import { formatCurrencyValue } from '../../util'
|
|
10
11
|
import AddToCartWidget from '../add-to-cart-widget'
|
|
12
|
+
import { useCommerce } from '../..'
|
|
11
13
|
|
|
12
|
-
const
|
|
14
|
+
const DEF_IMG_SIZE=40
|
|
13
15
|
|
|
14
16
|
const renderTitle = (t: string): React.ReactNode => {
|
|
15
17
|
const toks = t.split(', ')
|
|
@@ -22,32 +24,36 @@ const renderTitle = (t: string): React.ReactNode => {
|
|
|
22
24
|
const CartLineItem: React.FC<{
|
|
23
25
|
item: LineItem
|
|
24
26
|
className?: string
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
imgSizePx?: number
|
|
28
|
+
showPromoCode?: boolean
|
|
29
|
+
}> = observer(({
|
|
27
30
|
item,
|
|
28
31
|
className='',
|
|
29
|
-
|
|
32
|
+
imgSizePx=DEF_IMG_SIZE,
|
|
33
|
+
showPromoCode
|
|
30
34
|
}) => {
|
|
31
35
|
|
|
32
|
-
const
|
|
33
|
-
|
|
36
|
+
const cmmc = useCommerce()
|
|
37
|
+
const promoPrice = showPromoCode ? cmmc.itemPromoPrice(item) : undefined
|
|
38
|
+
|
|
34
39
|
return (
|
|
35
40
|
<div className={cn('flex flex-col justify-start items-start text-sm font-sans', className)}>
|
|
36
41
|
<div className='flex flex-row justify-between items-center gap-2'>
|
|
37
42
|
{!!item.img ? (
|
|
38
|
-
<Image src={item.img} alt={item.title + ' image'} height={
|
|
39
|
-
) : (
|
|
40
|
-
<div style={{height:
|
|
43
|
+
<Image src={item.img} alt={item.title + ' image'} height={imgSizePx} width={imgSizePx} className=''/>
|
|
44
|
+
) : ( // placeholder so things align
|
|
45
|
+
<div style={{height: imgSizePx, width: imgSizePx}}/>
|
|
41
46
|
)}
|
|
42
47
|
<div className='grow leading-tight'>{renderTitle(item.title)}</div>
|
|
43
48
|
</div>
|
|
44
49
|
<div className='flex flex-row items-center justify-between w-full'>
|
|
45
50
|
<div className='flex flex-row items-center'>
|
|
46
|
-
<AddToCartWidget
|
|
47
|
-
{item.quantity > 1 && (<span className='pl-2.5'>{'@' +
|
|
51
|
+
<AddToCartWidget className='' item={item} size='xs' buttonClx='h-8 md:h-6' ghost/>
|
|
52
|
+
{item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatCurrencyValue(item.price)}</span>)}
|
|
48
53
|
</div>
|
|
49
|
-
<div className='flex flex-row items-center justify-end'>
|
|
50
|
-
{
|
|
54
|
+
<div className='flex flex-row gap-1 items-center justify-end'>
|
|
55
|
+
<span className={promoPrice ? 'line-through text-muted-2' : ''}>{formatCurrencyValue(item.price * item.quantity)}</span>
|
|
56
|
+
{promoPrice && <span>{formatCurrencyValue(promoPrice * item.quantity)}</span>}
|
|
51
57
|
</div>
|
|
52
58
|
</div>
|
|
53
59
|
</div>
|
|
@@ -2,28 +2,45 @@
|
|
|
2
2
|
import React, { type PropsWithChildren } from 'react'
|
|
3
3
|
import { observer } from 'mobx-react-lite'
|
|
4
4
|
|
|
5
|
-
import { Button } from '@hanzo/ui/primitives'
|
|
5
|
+
import { Button, ScrollArea } from '@hanzo/ui/primitives'
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
|
|
8
8
|
import { useCommerce } from '../../service/context'
|
|
9
|
-
import {
|
|
9
|
+
import { formatCurrencyValue } from '../../util'
|
|
10
|
+
import { sendFBEvent, sendGAEvent } from '../../util/analytics'
|
|
10
11
|
|
|
11
12
|
import CartLineItem from './cart-line-item'
|
|
12
|
-
import
|
|
13
|
-
import ProductsCarousel from './products-carousel'
|
|
13
|
+
import PromoCode from './promo-code'
|
|
14
14
|
|
|
15
15
|
const CartPanel: React.FC<PropsWithChildren & {
|
|
16
|
+
/** fix size and scroll after 'scrollAfter items in teh cart. */
|
|
17
|
+
scrollAfter?: number
|
|
18
|
+
scrollHeightClx?: string
|
|
19
|
+
imgSizePx?: number
|
|
16
20
|
className?: string
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
itemClx?: string
|
|
22
|
+
listClx?: string
|
|
23
|
+
noItemsClx?: string
|
|
24
|
+
totalClx?: string
|
|
25
|
+
buttonClx?: string
|
|
26
|
+
showPromoCode?: boolean
|
|
27
|
+
showShipping?: boolean
|
|
28
|
+
/** if not provided, 'checkout' button will be rendered */
|
|
20
29
|
handleCheckout?: () => void
|
|
21
30
|
}> = observer(({
|
|
22
|
-
/**
|
|
31
|
+
/** If provided, 'children' is rendered above the items. eg, a heading. */
|
|
23
32
|
children,
|
|
33
|
+
scrollAfter=5,
|
|
34
|
+
scrollHeightClx='h-[70vh]',
|
|
35
|
+
imgSizePx=40,
|
|
24
36
|
className='',
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
itemClx='',
|
|
38
|
+
listClx='',
|
|
39
|
+
noItemsClx='',
|
|
40
|
+
totalClx='',
|
|
41
|
+
buttonClx='',
|
|
42
|
+
showPromoCode=false,
|
|
43
|
+
showShipping=false,
|
|
27
44
|
handleCheckout,
|
|
28
45
|
}) => {
|
|
29
46
|
|
|
@@ -57,32 +74,71 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
57
74
|
handleCheckout && handleCheckout()
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
const Main: React.FC = observer(() => (<>
|
|
78
|
+
{cmmc.cartEmpty ? (
|
|
79
|
+
<p className={cn('text-center mt-4', noItemsClx)}>No items in cart</p>
|
|
80
|
+
) : (<>
|
|
81
|
+
{cmmc.cartItems.map((item) => (
|
|
82
|
+
<CartLineItem
|
|
83
|
+
key={`mobile-${item.sku}`}
|
|
84
|
+
imgSizePx={imgSizePx}
|
|
85
|
+
item={item}
|
|
86
|
+
className={cn('mb-2', itemClx)}
|
|
87
|
+
showPromoCode={showPromoCode}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</>)}
|
|
91
|
+
{showPromoCode && <PromoCode/>}
|
|
92
|
+
{(showShipping || showPromoCode) && (
|
|
93
|
+
<div className='flex flex-col gap-1 py-2 border-t'>
|
|
94
|
+
<p className='flex justify-between'>
|
|
95
|
+
<span className='text-muted-1'>Subtotal</span>
|
|
96
|
+
<span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatCurrencyValue(cmmc.cartTotal)}</span>
|
|
97
|
+
</p>
|
|
98
|
+
{cmmc.promoAppliedCartTotal !== cmmc.cartTotal && (
|
|
99
|
+
<p className='flex justify-between'>
|
|
100
|
+
<span className='text-muted-1'>Promo Discount</span>
|
|
101
|
+
<span className='font-semibold'>-{formatCurrencyValue(cmmc.cartTotal - cmmc.promoAppliedCartTotal)}</span>
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
{showShipping && (
|
|
105
|
+
<p className='flex justify-between'>
|
|
106
|
+
<span className='text-muted-1'>Shipping</span>
|
|
107
|
+
<span className='font-semibold'>Free Global Shipping</span>
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<p className={cn('border-t py-2 flex justify-between', totalClx)}>
|
|
113
|
+
TOTAL
|
|
114
|
+
<span className='font-semibold'>{formatCurrencyValue(showPromoCode ? cmmc.promoAppliedCartTotal : cmmc.cartTotal)}</span>
|
|
115
|
+
</p>
|
|
116
|
+
</>))
|
|
117
|
+
|
|
118
|
+
const scrolling = (): boolean => (cmmc.cartItems.length > scrollAfter)
|
|
119
|
+
|
|
60
120
|
return (
|
|
61
|
-
<div className={cn('border p-4 rounded-lg', className)}>
|
|
121
|
+
<div className={cn('border p-4 rounded-lg flex flex-col', className, scrolling() ? scrollHeightClx : 'h-auto')}>
|
|
62
122
|
{children}
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Checkout
|
|
83
|
-
</Button>
|
|
84
|
-
)}
|
|
85
|
-
</>)}
|
|
123
|
+
{scrolling() ? (
|
|
124
|
+
<ScrollArea className={cn('mt-2 w-full shrink py-0', listClx)}>
|
|
125
|
+
<Main />
|
|
126
|
+
</ScrollArea>
|
|
127
|
+
) : (
|
|
128
|
+
<div className={cn('mt-2 w-full', listClx)}>
|
|
129
|
+
<Main />
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
{handleCheckout && !cmmc.cartEmpty && (
|
|
133
|
+
<Button
|
|
134
|
+
onClick={_handleCheckout}
|
|
135
|
+
variant='primary'
|
|
136
|
+
rounded='lg'
|
|
137
|
+
className={cn('mt-12 mx-auto w-full sm:max-w-[220px]', buttonClx)}
|
|
138
|
+
>
|
|
139
|
+
Checkout
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
86
142
|
</div>
|
|
87
143
|
)
|
|
88
144
|
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
import { useForm } from 'react-hook-form'
|
|
6
|
+
import * as z from 'zod'
|
|
7
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
8
|
+
import { Check } from 'lucide-react'
|
|
9
|
+
import { observer } from 'mobx-react-lite'
|
|
10
|
+
|
|
11
|
+
import { Button, Form, FormControl, FormField, FormItem, Input } from '@hanzo/ui/primitives'
|
|
12
|
+
|
|
13
|
+
import { useCommerce } from '../..'
|
|
14
|
+
import type { Promo } from '../../types'
|
|
15
|
+
import getPromoFromApi from '../../util/promo-codes'
|
|
16
|
+
|
|
17
|
+
const formSchema = z.object({
|
|
18
|
+
code: z.string().min(1, 'Invalid code'),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const PromoCode = observer(() => {
|
|
22
|
+
const cmmc = useCommerce()
|
|
23
|
+
const searchParams = useSearchParams()
|
|
24
|
+
|
|
25
|
+
const [codeAccepted, setCodeAccepted] = useState<boolean>(false)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const code = searchParams.get('code')
|
|
29
|
+
if (code) {
|
|
30
|
+
getPromoFromApi(code).then((promo?: Promo) => {
|
|
31
|
+
if (promo) {
|
|
32
|
+
form.setValue('code', code)
|
|
33
|
+
setCodeAccepted(true)
|
|
34
|
+
cmmc.setAppliedPromo(promo)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}, [searchParams])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (cmmc.appliedPromo) {
|
|
42
|
+
form.setValue('code', cmmc.appliedPromo.code)
|
|
43
|
+
setCodeAccepted(true)
|
|
44
|
+
}
|
|
45
|
+
}, [cmmc.appliedPromo])
|
|
46
|
+
|
|
47
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
48
|
+
resolver: zodResolver(formSchema),
|
|
49
|
+
defaultValues: {
|
|
50
|
+
code: '',
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const applyPromoCode = async (values: z.infer<typeof formSchema>) => {
|
|
55
|
+
const { code } = values
|
|
56
|
+
const promo = await getPromoFromApi(code)
|
|
57
|
+
if (!promo) {
|
|
58
|
+
form.setError('code', { message: 'Invalid code' })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
setCodeAccepted(true)
|
|
62
|
+
cmmc.setAppliedPromo(promo)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const removePromoCode = () => {
|
|
66
|
+
cmmc.setAppliedPromo(null)
|
|
67
|
+
setCodeAccepted(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className='flex flex-col gap-2 border-t py-2'>
|
|
72
|
+
<Form {...form}>
|
|
73
|
+
<form onSubmit={form.handleSubmit(applyPromoCode)}>
|
|
74
|
+
<div className='flex gap-2 items-center'>
|
|
75
|
+
<FormField
|
|
76
|
+
control={form.control}
|
|
77
|
+
name='code'
|
|
78
|
+
render={({ field }) => (
|
|
79
|
+
<FormItem className='w-full'>
|
|
80
|
+
<FormControl>
|
|
81
|
+
<Input {...field} placeholder='Discount or invite code' onInput={removePromoCode}/>
|
|
82
|
+
</FormControl>
|
|
83
|
+
</FormItem>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
{codeAccepted ? (
|
|
87
|
+
<Check/>
|
|
88
|
+
) : (
|
|
89
|
+
<Button variant='outline' className='!w-fit !min-w-0 font-inter'>Apply</Button>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</form>
|
|
93
|
+
</Form>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export default PromoCode
|
|
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite'
|
|
|
4
4
|
|
|
5
5
|
import { Label, RadioGroup, RadioGroupItem } from '@hanzo/ui/primitives'
|
|
6
6
|
import type { ItemSelector, LineItem, Product } from '../types'
|
|
7
|
-
import {
|
|
7
|
+
import { formatCurrencyValue } from '../util'
|
|
8
8
|
import { cn } from '@hanzo/ui/util'
|
|
9
9
|
|
|
10
10
|
const CategoryItemRadioSelector: React.FC<ItemSelector & {
|
|
@@ -29,7 +29,7 @@ const CategoryItemRadioSelector: React.FC<ItemSelector & {
|
|
|
29
29
|
}) => (
|
|
30
30
|
<div className={cn(className, itemClx)}>
|
|
31
31
|
<RadioGroupItem value={item.sku} id={item.sku} />
|
|
32
|
-
<Label htmlFor={item.sku}>{item.titleAsOption + (showPrice ? (', ' +
|
|
32
|
+
<Label htmlFor={item.sku}>{item.titleAsOption + (showPrice ? (', ' + formatCurrencyValue(item.price)) : '')}</Label>
|
|
33
33
|
</div>
|
|
34
34
|
)
|
|
35
35
|
|
|
@@ -6,7 +6,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
6
6
|
import { ScrollArea } from '@hanzo/ui/primitives'
|
|
7
7
|
|
|
8
8
|
import type { ItemSelector } from '../types'
|
|
9
|
-
import {
|
|
9
|
+
import { formatCurrencyValue } from '../util'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
const CategoryItemScrollSelector: React.FC<ItemSelector & {
|
|
@@ -29,7 +29,7 @@ const CategoryItemScrollSelector: React.FC<ItemSelector & {
|
|
|
29
29
|
(iRef.item?.sku === prod.sku) ? 'font-semibold text-accent' : 'text-muted',
|
|
30
30
|
itemClx
|
|
31
31
|
)}>
|
|
32
|
-
{prod.titleAsOption + ', ' +
|
|
32
|
+
{prod.titleAsOption + ', ' + formatCurrencyValue(prod.price)}
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
))}
|
package/components/index.ts
CHANGED
|
@@ -3,13 +3,16 @@ export { default as BuyItemButtonWrapper } from './buy-item/buy-item-button-wrap
|
|
|
3
3
|
export { default as BuyItemButton } from './buy-item/buy-item-button'
|
|
4
4
|
export { default as BuyItemCard } from './buy-item/buy-item-card'
|
|
5
5
|
export { default as BuyItemPopup } from './buy-item/buy-item-popup'
|
|
6
|
+
export { default as CartAccordian } from './cart-accordian'
|
|
6
7
|
export { default as CartPanel } from './cart-panel'
|
|
7
|
-
export { default as CheckoutPanel } from './checkout-panel'
|
|
8
8
|
export { default as CategoryItemIOSWheelSelector } from './category-item-scroll-selector'
|
|
9
9
|
export { default as CategoryItemRadioSelector } from './category-item-radio-selector'
|
|
10
10
|
export { default as FacetValuesWidget } from './facet-values-widget'
|
|
11
|
+
export { default as ItemCarousel } from './item-carousel'
|
|
12
|
+
export { default as PaymentStepForm } from './payment-step-form'
|
|
11
13
|
export { default as ProductCard } from './product-card'
|
|
12
14
|
export { default as SelectCategoryAndItemWidget } from './buy-item/select-category-and-item-widget'
|
|
13
15
|
export { default as SelectCategoryItemCard } from './buy-item/select-category-item-card'
|
|
14
16
|
export { default as SelectCategoryItemPanel } from './select-category-item-panel'
|
|
17
|
+
export { default as ShippingStepForm } from './shipping-step-form'
|
|
15
18
|
export { Icons } from './Icons'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import Spline from '@splinetool/react-spline'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type CarouselOptionsType,
|
|
9
|
+
Carousel,
|
|
10
|
+
CarouselContent,
|
|
11
|
+
CarouselItem,
|
|
12
|
+
CarouselPrevious,
|
|
13
|
+
CarouselNext
|
|
14
|
+
} from '@hanzo/ui/primitives'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
VideoBlockComponent,
|
|
18
|
+
ImageBlockComponent,
|
|
19
|
+
type ImageBlock,
|
|
20
|
+
type Block,
|
|
21
|
+
type VideoBlock
|
|
22
|
+
} from '@hanzo/ui/blocks'
|
|
23
|
+
|
|
24
|
+
import type { LineItem } from '../types'
|
|
25
|
+
import type { Dimensions } from '@hanzo/ui/types'
|
|
26
|
+
|
|
27
|
+
// Order of precedence of visuals: 3D > MP4 > Image
|
|
28
|
+
const ItemCarousel: React.FC<{
|
|
29
|
+
items: LineItem[]
|
|
30
|
+
constrainTo: Dimensions
|
|
31
|
+
options?: CarouselOptionsType
|
|
32
|
+
className?: string
|
|
33
|
+
itemClx?: string
|
|
34
|
+
}> = ({
|
|
35
|
+
items,
|
|
36
|
+
options,
|
|
37
|
+
className='',
|
|
38
|
+
constrainTo,
|
|
39
|
+
itemClx=''
|
|
40
|
+
}) => (
|
|
41
|
+
// className: 'w-full max-w-sm mx-auto
|
|
42
|
+
// itemClx: 'flex aspect-square items-center justify-center '
|
|
43
|
+
<Carousel options={options} className={cn('px-2', className)} >
|
|
44
|
+
<CarouselContent>
|
|
45
|
+
{items.map(({title, img, video, animation}, index) => (
|
|
46
|
+
<CarouselItem key={index} className={cn('p-2 flex flex-row justify-center items-center', itemClx)}>
|
|
47
|
+
{animation ? (
|
|
48
|
+
<Spline
|
|
49
|
+
scene={animation}
|
|
50
|
+
className='pointer-events-none' // !aspect-[12/10]
|
|
51
|
+
style={{
|
|
52
|
+
width: (6/5 * (typeof constrainTo.h === 'number' ? constrainTo.h as number : parseInt(constrainTo.h as string)) ),
|
|
53
|
+
height: constrainTo.h
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
) : video ? (
|
|
57
|
+
<VideoBlockComponent
|
|
58
|
+
constraint={constrainTo}
|
|
59
|
+
block={{blockType: 'video', ...video} satisfies VideoBlock as Block}
|
|
60
|
+
/>
|
|
61
|
+
) : (
|
|
62
|
+
<ImageBlockComponent
|
|
63
|
+
block={{
|
|
64
|
+
blockType: 'image',
|
|
65
|
+
src: img ?? '',
|
|
66
|
+
alt: title + ' image',
|
|
67
|
+
dim: constrainTo
|
|
68
|
+
} satisfies ImageBlock as Block}
|
|
69
|
+
className='m-auto'
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</CarouselItem>
|
|
73
|
+
))}
|
|
74
|
+
</CarouselContent>
|
|
75
|
+
<CarouselPrevious className='left-1'/>
|
|
76
|
+
<CarouselNext className='right-1'/>
|
|
77
|
+
</Carousel>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export default ItemCarousel
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { LockKeyhole } from 'lucide-react'
|
|
2
|
-
import Amex from './amex'
|
|
3
|
-
import Discover from './discover'
|
|
4
|
-
import Mastercard from './mastercard'
|
|
5
|
-
import Visa from './visa'
|
|
6
|
-
import DinersClub from './diners-club'
|
|
7
|
-
import Jcb from './jcb'
|
|
2
|
+
import Amex from './card-icons/amex'
|
|
3
|
+
import Discover from './card-icons/discover'
|
|
4
|
+
import Mastercard from './card-icons/mastercard'
|
|
5
|
+
import Visa from './card-icons/visa'
|
|
6
|
+
import DinersClub from './card-icons/diners-club'
|
|
7
|
+
import Jcb from './card-icons/jcb'
|
|
8
8
|
|
|
9
9
|
// <span className='hidden sm:flex text-sm'>Secure payments with</span>
|
|
10
10
|
|
|
11
11
|
const PaymentMethods: React.FC = () => {
|
|
12
12
|
return (
|
|
13
|
-
<div className='flex gap-1 items-center text-muted-1'>
|
|
13
|
+
<div className='flex gap-1 items-center text-muted-1 pb-3'>
|
|
14
14
|
<LockKeyhole className='w-4 h-4'/>
|
|
15
|
+
<span className='hidden sm:flex text-sm'>Secure payments with</span>
|
|
15
16
|
<Amex className='w-9 h-5'/>
|
|
16
17
|
<Discover className='w-9 h-5'/>
|
|
17
18
|
<Mastercard className='w-9 h-5'/>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Form,
|
|
3
|
+
FormControl,
|
|
4
|
+
FormField,
|
|
5
|
+
FormItem,
|
|
6
|
+
FormMessage,
|
|
7
|
+
} from '@hanzo/ui/primitives/form'
|
|
8
|
+
import { Input } from '@hanzo/ui/primitives'
|
|
9
|
+
|
|
10
|
+
import type { ContactFormType } from '../../types'
|
|
11
|
+
|
|
12
|
+
const ContactForm: React.FC<{
|
|
13
|
+
form: ContactFormType,
|
|
14
|
+
}> = ({
|
|
15
|
+
form,
|
|
16
|
+
}) => (
|
|
17
|
+
<Form {...form}>
|
|
18
|
+
<form className='text-left'>
|
|
19
|
+
<div className='flex flex-col gap-1 sm:gap-2'>
|
|
20
|
+
<FormField
|
|
21
|
+
control={form.control}
|
|
22
|
+
name='name'
|
|
23
|
+
render={({ field }) => (
|
|
24
|
+
<FormItem className='w-full'>
|
|
25
|
+
<FormControl>
|
|
26
|
+
<Input {...field} className='border-muted-4' placeholder='Full name'/>
|
|
27
|
+
</FormControl>
|
|
28
|
+
<FormMessage className='py-0 space-y-0 !m-0'/>
|
|
29
|
+
</FormItem>
|
|
30
|
+
)}
|
|
31
|
+
/>
|
|
32
|
+
<FormField
|
|
33
|
+
control={form.control}
|
|
34
|
+
name='email'
|
|
35
|
+
render={({ field }) => (
|
|
36
|
+
<FormItem className='w-full'>
|
|
37
|
+
<FormControl>
|
|
38
|
+
<Input {...field} className='border-muted-4' placeholder='Email'/>
|
|
39
|
+
</FormControl>
|
|
40
|
+
<FormMessage className='py-0 space-y-0 !m-0'/>
|
|
41
|
+
</FormItem>
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</form>
|
|
46
|
+
</Form>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
export default ContactForm
|