@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.
Files changed (53) hide show
  1. package/components/add-to-cart-widget.tsx +0 -1
  2. package/components/buy-item/select-category-and-item-widget.tsx +2 -2
  3. package/components/buy-item/select-category-item-card.tsx +2 -2
  4. package/components/{checkout-panel/cart-accordian.tsx → cart-accordian.tsx} +21 -10
  5. package/components/cart-panel/cart-line-item.tsx +22 -16
  6. package/components/cart-panel/index.tsx +90 -34
  7. package/components/cart-panel/promo-code.tsx +98 -0
  8. package/components/category-item-radio-selector.tsx +2 -2
  9. package/components/category-item-scroll-selector.tsx +2 -2
  10. package/components/index.ts +4 -1
  11. package/components/item-carousel.tsx +80 -0
  12. package/components/{checkout-panel/steps/payment/cards/index.tsx → payment-step-form/card-icon-row.tsx} +8 -7
  13. package/components/{checkout-panel/steps/payment → payment-step-form}/cc-button.tsx +1 -1
  14. package/components/payment-step-form/contact-form.tsx +49 -0
  15. package/components/{checkout-panel/steps/payment → payment-step-form}/index.tsx +29 -56
  16. package/components/payment-step-form/methods/bank-transfer.tsx +79 -0
  17. package/components/payment-step-form/methods/card.tsx +237 -0
  18. package/components/{checkout-panel/steps/payment/pay-with-crypto.tsx → payment-step-form/methods/crypto.tsx} +10 -24
  19. package/components/payment-step-form/methods/index.ts +23 -0
  20. package/components/product-card.tsx +2 -2
  21. package/components/select-category-item-panel.tsx +2 -2
  22. package/components/{checkout-panel/steps/shipping-info.tsx → shipping-step-form.tsx} +6 -8
  23. package/index.ts +1 -1
  24. package/package.json +2 -2
  25. package/service/context.tsx +1 -0
  26. package/service/impls/standalone/standalone-service.ts +56 -4
  27. package/types/checkout.ts +42 -1
  28. package/types/commerce-service.ts +9 -1
  29. package/types/index.ts +2 -1
  30. package/types/promo.ts +10 -0
  31. package/util/index.ts +3 -2
  32. package/util/promo-codes.ts +46 -0
  33. package/components/cart-panel/products-carousel.tsx +0 -60
  34. package/components/checkout-panel/_confirm-order.tsx_unused +0 -143
  35. package/components/checkout-panel/close-button.tsx +0 -24
  36. package/components/checkout-panel/desktop.tsx +0 -46
  37. package/components/checkout-panel/icons/bag-icon.tsx +0 -10
  38. package/components/checkout-panel/index.tsx +0 -108
  39. package/components/checkout-panel/mobile.tsx +0 -41
  40. package/components/checkout-panel/steps/payment/contact-info.tsx +0 -56
  41. package/components/checkout-panel/steps/payment/pay-with-bank-transfer.tsx +0 -110
  42. package/components/checkout-panel/steps/payment/pay-with-card.tsx +0 -246
  43. package/components/checkout-panel/steps/thank-you.tsx +0 -18
  44. package/components/checkout-panel/steps/types.ts +0 -14
  45. /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/amex.tsx +0 -0
  46. /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/diners-club.tsx +0 -0
  47. /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/discover.tsx +0 -0
  48. /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/jcb.tsx +0 -0
  49. /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/mastercard.tsx +0 -0
  50. /package/components/{checkout-panel/steps/payment/cards → payment-step-form/card-icons}/visa.tsx +0 -0
  51. /package/components/{checkout-panel/icons → payment-step-form/crypto-icons}/btc.tsx +0 -0
  52. /package/components/{checkout-panel/icons → payment-step-form/crypto-icons}/eth.tsx +0 -0
  53. /package/components/{checkout-panel/icons → payment-step-form/crypto-icons}/usdt.tsx +0 -0
@@ -15,7 +15,6 @@ const AddToCartWidget: React.FC<{
15
15
  disabled?: boolean
16
16
  className?: string
17
17
  buttonClx?: string
18
- isMobile?: boolean
19
18
  size?: ButtonSizes
20
19
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
21
20
  }> = observer(({
@@ -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 { formatPrice } from '../../util'
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}, ${formatPrice(item.price)}${(withQuantity && item.quantity > 0) ? ` (${item.quantity})` : ''}`
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 { formatPrice } from '../../util'
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 + ', ' + formatPrice(item.price)}</p>
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 { formatPrice, useCommerce } from '../..'
14
+ import { formatCurrencyValue, useCommerce } from '..'
13
15
 
14
- import CartPanel from '../cart-panel'
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<{className?: string}> = observer(({
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-1 items-center'>
28
- <BagIcon className='w-4 h-4 relative -top-0.5 sm:w-6 shrink-0 sm:h-6'/>
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'>{formatPrice(cmmc.cartTotal)}</h5>
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 className='w-full'/>
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 { formatPrice } from '../../util'
9
-
10
+ import { formatCurrencyValue } from '../../util'
10
11
  import AddToCartWidget from '../add-to-cart-widget'
12
+ import { useCommerce } from '../..'
11
13
 
12
- const IMG_SIZE=40
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
- isMobile?: boolean
26
- }> = observer(({
27
+ imgSizePx?: number
28
+ showPromoCode?: boolean
29
+ }> = observer(({
27
30
  item,
28
31
  className='',
29
- isMobile=false
32
+ imgSizePx=DEF_IMG_SIZE,
33
+ showPromoCode
30
34
  }) => {
31
35
 
32
- const imgSize = (isMobile) ? IMG_SIZE * 0.65 : IMG_SIZE
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={imgSize} width={imgSize} className=''/>
39
- ) : (
40
- <div style={{height: imgSize, width: imgSize}}/ >
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 isMobile={isMobile} className='' item={item} size='xs' buttonClx='h-8 md:h-6' ghost/>
47
- {item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatPrice(item.price)}</span>)}
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
- {formatPrice(item.price * item.quantity)}
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 { formatPrice } from '../../util'
9
+ import { formatCurrencyValue } from '../../util'
10
+ import { sendFBEvent, sendGAEvent } from '../../util/analytics'
10
11
 
11
12
  import CartLineItem from './cart-line-item'
12
- import { sendFBEvent, sendGAEvent } from '../../util/analytics'
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
- isMobile?: boolean,
18
- showCarousel?: boolean
19
- /** if not provided, checkout button will not be shown */
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
- /** Children is the heading area. */
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
- isMobile=false,
26
- showCarousel=false,
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
- {showCarousel && <ProductsCarousel items={cmmc.cartItems}/>}
64
- <div className='mt-2 w-full'>
65
- {cmmc.cartEmpty ? (
66
- <p className='text-center mt-4'>No items in cart</p>
67
- ) : (<>
68
- {cmmc.cartItems.map((item, i) => (
69
- <CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-2 md:mb-3'/>
70
- ))}
71
- <p className='mt-6 text-right border-t pt-1'>TOTAL: <span className='font-semibold'>{cmmc.cartTotal === 0 ? '0' : formatPrice(cmmc.cartTotal)}</span></p>
72
- </>)}
73
- </div>
74
- {handleCheckout && (<>
75
- {!cmmc.cartEmpty && (
76
- <Button
77
- variant='primary'
78
- rounded='lg'
79
- className='mt-12 mx-auto w-full sm:max-w-[220px]'
80
- onClick={_handleCheckout}
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 { formatPrice } from '../util'
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 ? (', ' + formatPrice(item.price)) : '')}</Label>
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 { formatPrice } from '../util'
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 + ', ' + formatPrice(prod.price)}
32
+ {prod.titleAsOption + ', ' + formatCurrencyValue(prod.price)}
33
33
  </div>
34
34
  </div>
35
35
  ))}
@@ -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'/>
@@ -1,6 +1,6 @@
1
1
  import { Button } from '@hanzo/ui/primitives'
2
2
 
3
- import PaymentMethods from './cards'
3
+ import PaymentMethods from './card-icon-row'
4
4
  import type { PropsWithChildren } from 'react'
5
5
 
6
6
  const CCButton: React.FC<PropsWithChildren> = ({
@@ -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