@hanzo/commerce 1.0.8
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/blocks/components/commerce-cat-and-item-block.tsx +13 -0
- package/blocks/def/commerce-cat-and-item-block.ts +9 -0
- package/components/Icons.tsx +35 -0
- package/components/add-to-cart-widget.tsx +83 -0
- package/components/cart-icon.tsx +10 -0
- package/components/cart-line-item.tsx +47 -0
- package/components/cart.tsx +64 -0
- package/components/facet-toggles-widget/facet-image.tsx +58 -0
- package/components/facet-toggles-widget/index.tsx +82 -0
- package/components/facets-widget.tsx +46 -0
- package/components/index.ts +14 -0
- package/components/product-card.tsx +79 -0
- package/components/product-selection-mobile-picker.tsx +78 -0
- package/components/product-selection-radio-group.tsx +34 -0
- package/components/select-category-and-item-widget.tsx +84 -0
- package/components/select-item-in-category-view.tsx +200 -0
- package/package.json +50 -0
- package/service/commerce-service.ts +49 -0
- package/service/context.tsx +39 -0
- package/service/firebase-config.ts +17 -0
- package/service/impl/index.ts +3 -0
- package/service/impl/standalone/get-singleton.ts +28 -0
- package/service/impl/standalone/obs-line-item.ts +62 -0
- package/service/impl/standalone/service.ts +198 -0
- package/service/index.ts +4 -0
- package/service/utils.ts +37 -0
- package/tsconfig.json +11 -0
- package/types/README.md +2 -0
- package/types/index.ts +103 -0
- package/util/index.ts +25 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type CommerceCatAndItemBlock from '../def/commerce-cat-and-item-block'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
|
|
6
|
+
interface CommerceCatAndItemBlock extends Block {
|
|
7
|
+
blockType: 'commerce-cat-and-item'
|
|
8
|
+
parentPath: string // SKU path of parent of Category ... eg, in Market, LXB-AU or LXB-AG. Next level is Cat.
|
|
9
|
+
defaultCatPath?: string // SKU path of default Category... eg, in Market, LXB-AU-B (Minted Bar)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Block } from '@hanzo/ui/blocks'
|
|
2
|
+
|
|
3
|
+
interface CommerceCatAndItemBlock extends Block {
|
|
4
|
+
blockType: 'commerce-cat-and-item'
|
|
5
|
+
parentPath: string // SKU path of parent of Category ... eg, in Market, LXB-AU or LXB-AG. Next level is Cat.
|
|
6
|
+
defaultCatPath?: string // SKU path of default Category... eg, in Market, LXB-AU-B (Minted Bar)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { type CommerceCatAndItemBlock as default }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ShoppingCart,
|
|
3
|
+
Menu,
|
|
4
|
+
ChevronLeft,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
Loader2,
|
|
7
|
+
Check,
|
|
8
|
+
Plus,
|
|
9
|
+
Minus,
|
|
10
|
+
ChevronDown,
|
|
11
|
+
ChevronsLeft,
|
|
12
|
+
ChevronsRight,
|
|
13
|
+
Image,
|
|
14
|
+
Trash2,
|
|
15
|
+
Search,
|
|
16
|
+
Barcode
|
|
17
|
+
} from "lucide-react"
|
|
18
|
+
|
|
19
|
+
export const Icons = {
|
|
20
|
+
shoppingCart: ShoppingCart,
|
|
21
|
+
menu: Menu,
|
|
22
|
+
chevronLeft: ChevronLeft,
|
|
23
|
+
chevronRight: ChevronRight,
|
|
24
|
+
spinner: Loader2,
|
|
25
|
+
check: Check,
|
|
26
|
+
plus: Plus,
|
|
27
|
+
minus: Minus,
|
|
28
|
+
chevronDown: ChevronDown,
|
|
29
|
+
chevronsLeft: ChevronsLeft,
|
|
30
|
+
chevronsRight: ChevronsRight,
|
|
31
|
+
placeholder: Image,
|
|
32
|
+
trash: Trash2,
|
|
33
|
+
search: Search,
|
|
34
|
+
barcode: Barcode
|
|
35
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { observer } from 'mobx-react-lite'
|
|
4
|
+
|
|
5
|
+
import { Button, type ButtonSizes } from '@hanzo/ui/primitives'
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
import { Icons } from './Icons'
|
|
9
|
+
import type { LineItem } from '../types'
|
|
10
|
+
|
|
11
|
+
const AddToCartWidget: React.FC<{
|
|
12
|
+
item: LineItem
|
|
13
|
+
ghost?: boolean
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
className?: string
|
|
16
|
+
isMobile?: boolean
|
|
17
|
+
wide?: boolean
|
|
18
|
+
size?: ButtonSizes
|
|
19
|
+
}> = observer(({
|
|
20
|
+
item,
|
|
21
|
+
ghost=false,
|
|
22
|
+
disabled=false,
|
|
23
|
+
className='',
|
|
24
|
+
size='xs'
|
|
25
|
+
}) => {
|
|
26
|
+
|
|
27
|
+
const iconClx = ghost ? 'h-3 w-3 text-muted-3 hover:text-foreground' : 'h-5 w-5 mr-1'
|
|
28
|
+
const digitClx = ghost ? 'px-1' : 'px-2 font-bold '
|
|
29
|
+
|
|
30
|
+
if (disabled) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn('flex flex-row items-stretch ' + (ghost ? 'bg-transparent rounded-xl' : 'bg-secondary rounded-xl'), className)}>
|
|
33
|
+
<div className={'text-sm flex items-center cursor-default ' + digitClx} >{item.quantity}</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return ( item.isInCart ? (
|
|
39
|
+
<div className={cn('flex flex-row items-stretch justify-center ' + (ghost ? 'bg-transparent rounded-xl' : 'bg-secondary rounded-xl'), className)}>
|
|
40
|
+
<Button
|
|
41
|
+
aria-label={'Remove a ' + item.title + ' from the cart'}
|
|
42
|
+
size={size}
|
|
43
|
+
variant={ghost ? 'ghost' : 'secondary'}
|
|
44
|
+
rounded={ghost ? 'full' : 'xl'}
|
|
45
|
+
className='px-1 lg:min-w-0 lg:px-2 xs:grow xs:justify-end'
|
|
46
|
+
key='left'
|
|
47
|
+
onClick={item.decrement.bind(item)}
|
|
48
|
+
>
|
|
49
|
+
{(item.quantity > 1) ? (
|
|
50
|
+
<Icons.minus className={iconClx} aria-hidden='true'/>
|
|
51
|
+
) : (
|
|
52
|
+
<Icons.trash className={iconClx} aria-hidden='true'/>
|
|
53
|
+
)}
|
|
54
|
+
</Button>
|
|
55
|
+
<div className={'text-sm flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}</div>
|
|
56
|
+
<Button
|
|
57
|
+
aria-label={'Add another ' + item.title + ' to the cart'}
|
|
58
|
+
size={size}
|
|
59
|
+
variant={ghost ? 'ghost' : 'secondary'}
|
|
60
|
+
rounded={ghost ? 'full' : 'xl'}
|
|
61
|
+
className='px-1 lg:min-w-0 lg:px-2 xs:grow xs:justify-start'
|
|
62
|
+
onClick={item.increment.bind(item)}
|
|
63
|
+
key='right'
|
|
64
|
+
>
|
|
65
|
+
<Icons.plus className={iconClx} aria-hidden='true'/>
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
<Button
|
|
70
|
+
aria-label={'Add a ' + item.title + ' to cart'}
|
|
71
|
+
size={size}
|
|
72
|
+
variant='secondary'
|
|
73
|
+
rounded='xl'
|
|
74
|
+
className={className}
|
|
75
|
+
onClick={item.increment.bind(item)}
|
|
76
|
+
>
|
|
77
|
+
<Icons.plus className='h-5 w-5 mr-1' aria-hidden='true'/>
|
|
78
|
+
<span className='mr-1'>Add</span>
|
|
79
|
+
</Button>
|
|
80
|
+
))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export default AddToCartWidget
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { type LucideProps } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
const CartIcon: React.FC<LucideProps> = (props: LucideProps) => (
|
|
5
|
+
<svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' {...props}>
|
|
6
|
+
<path d='m3.731 8-1.231 20h27l-1.231-20zm1.991 2.103h20.557l.972 15.794h-22.502l.972-15.794z'/>
|
|
7
|
+
<path d='m20.718 8.134c0-2.375-1.932-4.307-4.307-4.307s-4.307 1.932-4.307 4.307h-2.084c0-3.524 2.867-6.391 6.391-6.391s6.391 2.867 6.391 6.391z'/>
|
|
8
|
+
</svg>)
|
|
9
|
+
|
|
10
|
+
export default CartIcon
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { observer } from 'mobx-react-lite'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
|
6
|
+
|
|
7
|
+
import type { LineItem } from '../types'
|
|
8
|
+
import AddToCartWidget from './add-to-cart-widget'
|
|
9
|
+
import { formatPrice } from '../util'
|
|
10
|
+
|
|
11
|
+
const IMG_SIZE=40
|
|
12
|
+
|
|
13
|
+
const CartLineItem: React.FC<{
|
|
14
|
+
item: LineItem
|
|
15
|
+
className?: string
|
|
16
|
+
isMobile?: boolean
|
|
17
|
+
}> = observer(({
|
|
18
|
+
item,
|
|
19
|
+
className='',
|
|
20
|
+
isMobile=false
|
|
21
|
+
}) => {
|
|
22
|
+
|
|
23
|
+
const imgSize = (isMobile) ? IMG_SIZE * 0.65 : IMG_SIZE
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className='flex flex-row justify-between items-center gap-2 sm:gap-4'>
|
|
27
|
+
{!!item.img ? (
|
|
28
|
+
<Image src={item.img} alt={item.title + ' image'} height={imgSize} width={imgSize} className='self-start'/>
|
|
29
|
+
) : (
|
|
30
|
+
<div style={{height: imgSize, width: imgSize}}/ >
|
|
31
|
+
)}
|
|
32
|
+
<div className={cn('text-sm font-sans grow', className)} >
|
|
33
|
+
<div className=''>{item.title}</div>
|
|
34
|
+
<div className='flex flex-row items-center justify-between'>
|
|
35
|
+
<div className='flex flex-row items-center'>
|
|
36
|
+
<AddToCartWidget isMobile={isMobile} className='mr-2' item={item} ghost/>{item.quantity > 1 && '@' + formatPrice(item.price)}
|
|
37
|
+
</div>
|
|
38
|
+
<div className='flex flex-row items-center justify-end'>
|
|
39
|
+
{formatPrice(item.price * item.quantity)}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export default CartLineItem
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { type PropsWithChildren, useState } from 'react'
|
|
3
|
+
import { observer } from 'mobx-react-lite'
|
|
4
|
+
|
|
5
|
+
import { Button } from '@hanzo/ui/primitives'
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
import { persistCart, useCommerce } from '../service'
|
|
9
|
+
import { formatPrice } from '../util'
|
|
10
|
+
|
|
11
|
+
import CartLineItem from './cart-line-item'
|
|
12
|
+
import { useRouter } from 'next/navigation'
|
|
13
|
+
import { useAuth } from '@hanzo/auth/service'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const Cart: React.FC<PropsWithChildren & {
|
|
17
|
+
className?: string
|
|
18
|
+
isMobile?: boolean,
|
|
19
|
+
hideCheckout?: boolean
|
|
20
|
+
}> = observer(({
|
|
21
|
+
children,
|
|
22
|
+
className='',
|
|
23
|
+
isMobile=false,
|
|
24
|
+
hideCheckout=false
|
|
25
|
+
}) => {
|
|
26
|
+
const [loadingCheckout, setLoadingCheckout] = useState(false)
|
|
27
|
+
const c = useCommerce()
|
|
28
|
+
const router = useRouter()
|
|
29
|
+
const auth = useAuth()
|
|
30
|
+
|
|
31
|
+
const checkout = async () => {
|
|
32
|
+
setLoadingCheckout(true)
|
|
33
|
+
if (auth.user) {
|
|
34
|
+
await persistCart(c.cartItems, auth.user.email)
|
|
35
|
+
}
|
|
36
|
+
router.push('/checkout')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn('border p-6 rounded-lg', className)}>
|
|
41
|
+
{children}
|
|
42
|
+
<div className='mt-2'>
|
|
43
|
+
{!!children && <div className='h-[1px] w-pr-80 mx-auto bg-muted-3'/>}
|
|
44
|
+
{c.cartItems.length === 0 ? (
|
|
45
|
+
<p className='text-center mt-4'>No items in cart</p>
|
|
46
|
+
) : (<>
|
|
47
|
+
{c.cartItems.map((item, i) => (<CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-4 sm:mb-2'/>))}
|
|
48
|
+
<p className='mt-6 text-right border-t pt-1'>TOTAL: {c.cartTotal === 0 ? '0' : formatPrice(c.cartTotal)}</p>
|
|
49
|
+
</>)}
|
|
50
|
+
</div>
|
|
51
|
+
{c.cartItems.length > 0 && !hideCheckout && (
|
|
52
|
+
<>
|
|
53
|
+
{!auth.loggedIn ? (
|
|
54
|
+
<Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login</Button>
|
|
55
|
+
) : (
|
|
56
|
+
<Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={checkout} disabled={loadingCheckout}>Checkout</Button>
|
|
57
|
+
)}
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export default Cart
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Image from 'next/image'
|
|
3
|
+
|
|
4
|
+
import type { FacetValueDesc } from '../../types'
|
|
5
|
+
|
|
6
|
+
const ICON_SIZE = 16
|
|
7
|
+
const SVG_MULT = 1.5
|
|
8
|
+
|
|
9
|
+
const FacetImage: React.FC<{
|
|
10
|
+
facetValueDesc: FacetValueDesc
|
|
11
|
+
}> = ({
|
|
12
|
+
facetValueDesc
|
|
13
|
+
}) => {
|
|
14
|
+
if (!facetValueDesc.img) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
const isURL = typeof facetValueDesc.img === 'string'
|
|
18
|
+
if (isURL) {
|
|
19
|
+
return (
|
|
20
|
+
<Image
|
|
21
|
+
src={facetValueDesc.img as string}
|
|
22
|
+
alt={`Toggle ${facetValueDesc.label}`}
|
|
23
|
+
className={'block mr-1 aspect-square rounded border border-muted-2'}
|
|
24
|
+
width={ICON_SIZE}
|
|
25
|
+
height={ICON_SIZE}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
const svgStyle: any = { position: 'relative' }
|
|
30
|
+
if (facetValueDesc.imgAR && facetValueDesc.imgAR < 1) {
|
|
31
|
+
const w = ICON_SIZE * SVG_MULT
|
|
32
|
+
svgStyle.top = (( 1 / facetValueDesc.imgAR - 1) * w) / 2
|
|
33
|
+
}
|
|
34
|
+
else if (facetValueDesc.imgAR && facetValueDesc.imgAR > 1) {
|
|
35
|
+
const h = ICON_SIZE * SVG_MULT
|
|
36
|
+
svgStyle.left = (((1 - facetValueDesc.imgAR) * h) / 2)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<span
|
|
41
|
+
className='block overflow-hidden '
|
|
42
|
+
style={{
|
|
43
|
+
color: 'inherit',
|
|
44
|
+
width: ICON_SIZE * SVG_MULT,
|
|
45
|
+
height: ICON_SIZE * SVG_MULT,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{React.cloneElement(facetValueDesc.img as React.ReactElement<any>, {
|
|
49
|
+
width: (facetValueDesc.imgAR ?? 1) * ICON_SIZE * SVG_MULT,
|
|
50
|
+
height: SVG_MULT * ICON_SIZE,
|
|
51
|
+
style: svgStyle
|
|
52
|
+
})}
|
|
53
|
+
</span>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default FacetImage
|
|
58
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { ToggleGroup, ToggleGroupItem,} from "@hanzo/ui/primitives"
|
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
|
6
|
+
|
|
7
|
+
import type { FacetValueDesc, StringMutator, StringArrayMutator } from '../../types'
|
|
8
|
+
import FacetImage from './facet-image'
|
|
9
|
+
|
|
10
|
+
const FacetTogglesWidget: React.FC<{
|
|
11
|
+
facetValues: FacetValueDesc[]
|
|
12
|
+
mutator: StringMutator | StringArrayMutator
|
|
13
|
+
multiple?: boolean
|
|
14
|
+
className?: string
|
|
15
|
+
isMobile?: boolean
|
|
16
|
+
}> = ({
|
|
17
|
+
facetValues,
|
|
18
|
+
mutator,
|
|
19
|
+
multiple='',
|
|
20
|
+
className='',
|
|
21
|
+
isMobile=false
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
const [last, setLast] = useState<string | undefined>(undefined)
|
|
25
|
+
|
|
26
|
+
const handleChangeMultiple = (selected: string[]) => {
|
|
27
|
+
(mutator as StringArrayMutator).set(selected)
|
|
28
|
+
setLast(selected.length === 1 ? selected[0] : undefined)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleChangeSingle = (selected: string) => {
|
|
32
|
+
(mutator as StringMutator).set(selected)
|
|
33
|
+
if (selected) { setLast(selected) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const roundedToSpread: any = {}
|
|
37
|
+
if (multiple) {
|
|
38
|
+
roundedToSpread.rounded = 'xl'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const val = multiple ?
|
|
42
|
+
(mutator as StringArrayMutator).get()
|
|
43
|
+
:
|
|
44
|
+
(mutator as StringMutator).get()
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ToggleGroup
|
|
48
|
+
type={multiple ? 'multiple' : 'single'}
|
|
49
|
+
value={val}
|
|
50
|
+
variant='default'
|
|
51
|
+
size={isMobile ? 'sm' : 'default'}
|
|
52
|
+
onValueChange={multiple ? handleChangeMultiple : handleChangeSingle}
|
|
53
|
+
className={className}
|
|
54
|
+
{...roundedToSpread}
|
|
55
|
+
>
|
|
56
|
+
{facetValues.map((fv, index) => {
|
|
57
|
+
const roundedToSpread: any = {}
|
|
58
|
+
if (!multiple) {
|
|
59
|
+
roundedToSpread.rounded = 'none'
|
|
60
|
+
if (index === 0) { roundedToSpread.rounded = 'llg' }
|
|
61
|
+
else if (index === facetValues.length - 1) { roundedToSpread.rounded = 'rlg' }
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
<ToggleGroupItem
|
|
65
|
+
key={fv.value}
|
|
66
|
+
value={fv.value}
|
|
67
|
+
disabled={(last && last === fv.value || fv.value === mutator.get())}
|
|
68
|
+
aria-label={`Select ${fv.label}`}
|
|
69
|
+
{...roundedToSpread}
|
|
70
|
+
>
|
|
71
|
+
<span className={cn('flex flex-row justify-center gap-1 h-4 items-center')} >
|
|
72
|
+
<FacetImage facetValueDesc={fv} />
|
|
73
|
+
<span className='hidden md:block whitespace-nowrap'>{fv.label}</span>
|
|
74
|
+
</span>
|
|
75
|
+
</ToggleGroupItem>
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</ToggleGroup>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default FacetTogglesWidget
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { type PropsWithChildren } from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
|
5
|
+
|
|
6
|
+
import type { FacetsDesc, StringMutator, StringArrayMutator } from '../types'
|
|
7
|
+
|
|
8
|
+
import FacetTogglesWidget from './facet-toggles-widget'
|
|
9
|
+
|
|
10
|
+
const FacetsWidget: React.FC<PropsWithChildren & {
|
|
11
|
+
facets: FacetsDesc
|
|
12
|
+
facetClassNames?: string[]
|
|
13
|
+
mutators: StringMutator[] | StringArrayMutator[]
|
|
14
|
+
multiple?: boolean
|
|
15
|
+
isMobile?: boolean
|
|
16
|
+
id?: string
|
|
17
|
+
className?: string
|
|
18
|
+
}> = ({
|
|
19
|
+
children,
|
|
20
|
+
facets,
|
|
21
|
+
mutators,
|
|
22
|
+
multiple=false,
|
|
23
|
+
facetClassNames,
|
|
24
|
+
isMobile=false,
|
|
25
|
+
className='',
|
|
26
|
+
id='FacetsWidget'
|
|
27
|
+
}) => {
|
|
28
|
+
const horiz = className.includes('flex-row')
|
|
29
|
+
return (
|
|
30
|
+
<div id={id} className={className} >
|
|
31
|
+
{Object.keys(facets).map((key, i) => (
|
|
32
|
+
<FacetTogglesWidget
|
|
33
|
+
key={i}
|
|
34
|
+
multiple={multiple}
|
|
35
|
+
mutator={mutators[i]}
|
|
36
|
+
isMobile={isMobile}
|
|
37
|
+
facetValues={facets[parseInt(key)]}
|
|
38
|
+
className={cn((horiz ? '' : 'mb-2'), (i !== 0 && !horiz) ? 'mt-2' : '', (facetClassNames?.[i]) ?? '')}
|
|
39
|
+
/>
|
|
40
|
+
))}
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default FacetsWidget
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { default as AddToCartWidget } from './add-to-cart-widget'
|
|
2
|
+
export { default as Cart } from './cart'
|
|
3
|
+
export { default as CartLineItem } from './cart-line-item'
|
|
4
|
+
export { default as FacetsWidget } from './facets-widget'
|
|
5
|
+
export { default as FacetTogglesWidget,
|
|
6
|
+
type FacetMutatorSingle,
|
|
7
|
+
type FacetMutatorMultiple
|
|
8
|
+
} from './facet-toggles-widget'
|
|
9
|
+
export { default as ProductCard } from './product-card'
|
|
10
|
+
export { default as ProductSelectionMobilePicker } from './product-selection-mobile-picker'
|
|
11
|
+
export { default as ProductSelectionRadioGroup } from './product-selection-radio-group'
|
|
12
|
+
export { default as SelectCategoryAndItemWidget } from './select-category-and-item-widget'
|
|
13
|
+
export { default as SelectItemInCategoryView } from './select-item-in-category-view'
|
|
14
|
+
export * as Icons from './Icons'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Image from 'next/image'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AspectRatio,
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardFooter,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from '@hanzo/ui/primitives'
|
|
13
|
+
import type { ImageDef } from '@hanzo/ui/types'
|
|
14
|
+
import { cn } from '@hanzo/ui/util'
|
|
15
|
+
|
|
16
|
+
import { formatPrice } from '../util'
|
|
17
|
+
import type { LineItem } from '../types'
|
|
18
|
+
import { Icons } from './Icons'
|
|
19
|
+
|
|
20
|
+
import AddToCartWidget from './add-to-cart-widget'
|
|
21
|
+
|
|
22
|
+
interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
item: LineItem
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ProductCard: React.FC<ProductCardProps> = ({
|
|
27
|
+
item,
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}) => {
|
|
31
|
+
|
|
32
|
+
const ProductImage: React.FC<{className?: string}> = ({className=''}) => (
|
|
33
|
+
<Image
|
|
34
|
+
src={item.img!}
|
|
35
|
+
alt={item.title}
|
|
36
|
+
className={className}
|
|
37
|
+
loading='lazy'
|
|
38
|
+
// width={700}
|
|
39
|
+
// height={700}
|
|
40
|
+
fill
|
|
41
|
+
style={{
|
|
42
|
+
objectFit: 'contain',
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Card
|
|
49
|
+
className={cn('max-h-[360px] lg:min-w-[200px] max-w-[260px] overflow-hidden', className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<CardHeader className='w-full border-b p-6 min-h-[180px] max-h-[240px] relative'>
|
|
53
|
+
{item.img ? (
|
|
54
|
+
<ProductImage className='p-6' />
|
|
55
|
+
) : (
|
|
56
|
+
<div
|
|
57
|
+
aria-label='Placeholder'
|
|
58
|
+
role='img'
|
|
59
|
+
aria-roledescription='placeholder'
|
|
60
|
+
className='flex h-full items-center justify-center bg-secondary'
|
|
61
|
+
>
|
|
62
|
+
<Icons.barcode className='h-9 w-9 text-muted' aria-hidden='true' />
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</CardHeader>
|
|
66
|
+
<CardContent className='grid gap-2.5 p-4'>
|
|
67
|
+
<CardTitle className='text-sm sm:text-base flex flex-col justify-start items-center line-clap-3'>
|
|
68
|
+
{item.title.split(', ').map((e, i) => (<p key={i}>{e}</p>))}
|
|
69
|
+
<p className='mt-1 font-semibold'>{formatPrice(item.price)}</p>
|
|
70
|
+
</CardTitle>
|
|
71
|
+
</CardContent>
|
|
72
|
+
<CardFooter className='p-4 flex flex-row justify-center'>
|
|
73
|
+
<AddToCartWidget item={item}/>
|
|
74
|
+
</CardFooter>
|
|
75
|
+
</Card>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default ProductCard
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import Picker from 'react-mobile-picker'
|
|
3
|
+
|
|
4
|
+
import type { Product } from '../types'
|
|
5
|
+
import { formatPrice } from '../util'
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
// from source code
|
|
9
|
+
interface PickerItemRenderProps {
|
|
10
|
+
selected: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function renderOptions(options: string[], selectedColor: string) {
|
|
14
|
+
return options.map((option) => (
|
|
15
|
+
<Picker.Item key={option} value={option}>
|
|
16
|
+
{({ selected }: PickerItemRenderProps) => (
|
|
17
|
+
<div className={selected ? `font-semibold ${selectedColor}` : 'text-neutral-400'}>{option}</div>
|
|
18
|
+
)}
|
|
19
|
+
</Picker.Item>
|
|
20
|
+
))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// const DEFAULT_HEIGHT = 216
|
|
24
|
+
// const DEFAULT_ITEM_HEIGHT = 36
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const ProductSelectionMobilePicker: React.FC<{
|
|
28
|
+
products: Product[]
|
|
29
|
+
selectedSku: string | undefined
|
|
30
|
+
onValueChange: (v: string) => void
|
|
31
|
+
height?: number
|
|
32
|
+
itemHeight?: number
|
|
33
|
+
outerClx?: string
|
|
34
|
+
itemClx?: string
|
|
35
|
+
}> = ({
|
|
36
|
+
products,
|
|
37
|
+
selectedSku,
|
|
38
|
+
onValueChange,
|
|
39
|
+
height,
|
|
40
|
+
itemHeight,
|
|
41
|
+
outerClx='',
|
|
42
|
+
itemClx=''
|
|
43
|
+
}) => {
|
|
44
|
+
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
const onChange = (val, ignore) => {
|
|
47
|
+
console.log("VAL", val)
|
|
48
|
+
console.log("key")
|
|
49
|
+
onValueChange(val.item)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn('border border-muted-4 rounded-lg px-2', outerClx)}>
|
|
54
|
+
<Picker
|
|
55
|
+
className=''
|
|
56
|
+
value={selectedSku ? {item: selectedSku} : {item: undefined}}
|
|
57
|
+
onChange={onChange}
|
|
58
|
+
wheelMode="natural"
|
|
59
|
+
height={height}
|
|
60
|
+
itemHeight={itemHeight}
|
|
61
|
+
>
|
|
62
|
+
<Picker.Column name="item">
|
|
63
|
+
{products.map((prod) => (
|
|
64
|
+
<Picker.Item key={prod.sku} value={prod.sku}>
|
|
65
|
+
{({ selected }: PickerItemRenderProps) => (
|
|
66
|
+
<div className={cn(selected ? 'font-semibold text-accent' : 'text-muted', itemClx)}>
|
|
67
|
+
{prod.titleAsOption + ', ' + formatPrice(prod.price)}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</Picker.Item>
|
|
71
|
+
))}
|
|
72
|
+
</Picker.Column>
|
|
73
|
+
</Picker>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default ProductSelectionMobilePicker
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Label, RadioGroup, RadioGroupItem } from '@hanzo/ui/primitives'
|
|
4
|
+
import type { Product } from '../types'
|
|
5
|
+
import { formatPrice } from '../util'
|
|
6
|
+
|
|
7
|
+
const ProductSelectionRadioGroup: React.FC<{
|
|
8
|
+
products: Product[]
|
|
9
|
+
selectedSku: string | undefined
|
|
10
|
+
onValueChange: (v: string) => void
|
|
11
|
+
groupClx?: string
|
|
12
|
+
itemClx?: string
|
|
13
|
+
}> = ({
|
|
14
|
+
products,
|
|
15
|
+
selectedSku,
|
|
16
|
+
onValueChange,
|
|
17
|
+
groupClx='',
|
|
18
|
+
itemClx=''
|
|
19
|
+
}) => (
|
|
20
|
+
<RadioGroup
|
|
21
|
+
className={groupClx}
|
|
22
|
+
onValueChange={onValueChange}
|
|
23
|
+
value={selectedSku}
|
|
24
|
+
>
|
|
25
|
+
{products.map((prod) => (
|
|
26
|
+
<div className={itemClx} key={prod.sku}>
|
|
27
|
+
<RadioGroupItem value={prod.sku} id={prod.sku} />
|
|
28
|
+
<Label htmlFor={prod.sku}>{prod.titleAsOption + ', ' + formatPrice(prod.price)}</Label>
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</RadioGroup>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export default ProductSelectionRadioGroup
|