@luxfi/core 4.3.11
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/access-code-input.tsx +71 -0
- package/components/auth/login-panel.tsx +80 -0
- package/components/chat-widget.tsx +77 -0
- package/components/commerce/bag-button.tsx +67 -0
- package/components/commerce/checkout-panel/close-button.tsx +23 -0
- package/components/commerce/checkout-panel/dt-bag-carousel.tsx +36 -0
- package/components/commerce/checkout-panel/dt-checkout-panel.tsx +68 -0
- package/components/commerce/checkout-panel/index.tsx +124 -0
- package/components/commerce/checkout-panel/links-row.tsx +21 -0
- package/components/commerce/checkout-panel/mb-checkout-panel.tsx +51 -0
- package/components/commerce/checkout-panel/steps-indicator.tsx +39 -0
- package/components/commerce/checkout-panel/thank-you.tsx +18 -0
- package/components/commerce/desktop-bag-popup.tsx +78 -0
- package/components/commerce/mobile-bag-drawer.tsx +51 -0
- package/components/commerce/mobile-menu-toggle-button.tsx +35 -0
- package/components/commerce/mobile-nav-menu.tsx +64 -0
- package/components/contact-dialog/contact-form.tsx +112 -0
- package/components/contact-dialog/disclaimer.tsx +13 -0
- package/components/contact-dialog/index.tsx +64 -0
- package/components/copyright.tsx +21 -0
- package/components/footer.tsx +78 -0
- package/components/header/desktop.tsx +54 -0
- package/components/header/index.tsx +26 -0
- package/components/header/mobile.tsx +161 -0
- package/components/header/theme-toggle.tsx +26 -0
- package/components/icons/bag-icon.tsx +10 -0
- package/components/icons/github.tsx +14 -0
- package/components/icons/index.tsx +35 -0
- package/components/icons/lux-logo.tsx +10 -0
- package/components/icons/secure-delivery.tsx +13 -0
- package/components/icons/social-icon.tsx +35 -0
- package/components/icons/social-svg.css +3 -0
- package/components/icons/youtube-logo.tsx +59 -0
- package/components/index.ts +26 -0
- package/components/logo.tsx +81 -0
- package/components/mini-chart/index.tsx +8 -0
- package/components/mini-chart/mini-chart-props.ts +44 -0
- package/components/mini-chart/mini-chart.tsx +85 -0
- package/components/mini-chart/wrapper.tsx +23 -0
- package/components/not-found/index.tsx +27 -0
- package/components/not-found/not-found-content.mdx +5 -0
- package/components/root-layout.tsx +71 -0
- package/components/scripts.tsx +23 -0
- package/conf/index.ts +50 -0
- package/environment.d.ts +6 -0
- package/next/analytics/fpixel.ts +16 -0
- package/next/analytics/google-analytics.ts +14 -0
- package/next/analytics/index.ts +3 -0
- package/next/analytics/pixel-analytics.tsx +55 -0
- package/next/determine-device-mw.ts +16 -0
- package/next/font/get-app-router-font-classes.ts +12 -0
- package/next/font/load-and-return-lux-next-fonts-on-import.ts +67 -0
- package/next/font/local/Druk-Wide-Bold.ttf +0 -0
- package/next/font/local/Druk-Wide-Medium.ttf +0 -0
- package/next/font/next-font-desc.ts +28 -0
- package/next/font/pages-router-font-vars.tsx +18 -0
- package/next/head-metadata/from-next/metadata-types.ts +158 -0
- package/next/head-metadata/from-next/opengraph-types.ts +267 -0
- package/next/head-metadata/from-next/twitter-types.ts +92 -0
- package/next/head-metadata/index.tsx +208 -0
- package/next/index.ts +1 -0
- package/package.json +72 -0
- package/server-actions/firebase-app.ts +14 -0
- package/server-actions/index.ts +5 -0
- package/server-actions/store-contact.ts +51 -0
- package/site-def/footer/community.tsx +67 -0
- package/site-def/footer/company.ts +37 -0
- package/site-def/footer/ecosystem.ts +37 -0
- package/site-def/footer/index.tsx +26 -0
- package/site-def/footer/legal.ts +28 -0
- package/site-def/footer/network.ts +45 -0
- package/site-def/footer/svg/warpcast-logo.svg +12 -0
- package/site-def/index.ts +3 -0
- package/site-def/main-nav.ts +35 -0
- package/site-def/site-def.ts +37 -0
- package/style/lux-colors.css +85 -0
- package/style/lux-global.css +19 -0
- package/tailwind/fontFamily.tailwind.lux.ts +18 -0
- package/tailwind/index.ts +2 -0
- package/tailwind/lux-tw-fonts.ts +40 -0
- package/tailwind/tailwind.config.lux-preset.ts +10 -0
- package/tsconfig.json +10 -0
- package/types/contact-info.ts +11 -0
- package/types/index.ts +1 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
'use client'
|
2
|
+
|
3
|
+
import { useState } from 'react'
|
4
|
+
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '@hanzo/ui/primitives'
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
6
|
+
|
7
|
+
const AccessCodeInput: React.FC<{
|
8
|
+
onSuccess?: () => void
|
9
|
+
onFail?: () => void
|
10
|
+
validCodes?: string[]
|
11
|
+
className?: string
|
12
|
+
}> = ({
|
13
|
+
onSuccess,
|
14
|
+
onFail,
|
15
|
+
validCodes,
|
16
|
+
className
|
17
|
+
}) => {
|
18
|
+
const [status, setStatus] = useState<'valid' | 'invalid' | 'checking' | undefined>()
|
19
|
+
|
20
|
+
const checkAccessCode = (code: string) => {
|
21
|
+
setStatus(undefined)
|
22
|
+
if (code.length === 6) {
|
23
|
+
setStatus('checking')
|
24
|
+
setTimeout(() => {
|
25
|
+
if (validCodes?.includes(code) && onSuccess) {
|
26
|
+
setStatus('valid')
|
27
|
+
onSuccess()
|
28
|
+
}
|
29
|
+
else {
|
30
|
+
setStatus('invalid')
|
31
|
+
onFail && onFail()
|
32
|
+
}
|
33
|
+
}, 1000)
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
return (
|
38
|
+
<div className={cn('flex flex-col gap-2 mx-auto w-full text-center items-center', className)}>
|
39
|
+
<InputOTP
|
40
|
+
className='mx-auto'
|
41
|
+
maxLength={6}
|
42
|
+
onInput={(event) => checkAccessCode((event.target as HTMLInputElement).value)}
|
43
|
+
render={({ slots }) => (
|
44
|
+
<>
|
45
|
+
<InputOTPGroup>
|
46
|
+
{slots.slice(0, 3).map((slot, index) => (
|
47
|
+
<InputOTPSlot key={index} {...slot} />
|
48
|
+
))}{" "}
|
49
|
+
</InputOTPGroup>
|
50
|
+
<InputOTPSeparator />
|
51
|
+
<InputOTPGroup>
|
52
|
+
{slots.slice(3).map((slot, index) => (
|
53
|
+
<InputOTPSlot key={index + 3} {...slot} />
|
54
|
+
))}
|
55
|
+
</InputOTPGroup>
|
56
|
+
</>
|
57
|
+
)}
|
58
|
+
/>
|
59
|
+
<p className='h-[3rem]'>
|
60
|
+
{
|
61
|
+
status === 'checking' ? 'Checking access code...' :
|
62
|
+
status === 'invalid' ? <span className='text-destructive'>Invalid access code!</span> :
|
63
|
+
status === 'valid' ? <span>Access code is valid! Redirecting...</span> :
|
64
|
+
null
|
65
|
+
}
|
66
|
+
</p>
|
67
|
+
</div>
|
68
|
+
)
|
69
|
+
}
|
70
|
+
|
71
|
+
export default AccessCodeInput
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import Link from 'next/link'
|
2
|
+
import Autoplay from 'embla-carousel-autoplay'
|
3
|
+
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
5
|
+
import { Button, Carousel, CarouselContent, CarouselItem } from '@hanzo/ui/primitives'
|
6
|
+
import { LoginPanel as Login } from '@hanzo/auth/components'
|
7
|
+
|
8
|
+
import { Logo } from '..'
|
9
|
+
import LuxLogo from '../icons/lux-logo'
|
10
|
+
|
11
|
+
const LoginPanel: React.FC<{
|
12
|
+
close: () => void
|
13
|
+
getStartedUrl?: string
|
14
|
+
redirectUrl?: string
|
15
|
+
className?: string
|
16
|
+
reviews: { text: string, author: string, href: string }[]
|
17
|
+
}> = ({
|
18
|
+
close,
|
19
|
+
getStartedUrl='/',
|
20
|
+
redirectUrl,
|
21
|
+
className='',
|
22
|
+
reviews
|
23
|
+
}) => {
|
24
|
+
return (
|
25
|
+
<div className={cn('grid grid-cols-1 md:grid-cols-2', className)}>
|
26
|
+
<div className='hidden md:flex w-full h-full bg-level-1 flex-row items-end justify-end overflow-y-auto min-h-screen'>
|
27
|
+
<div className='h-full w-full max-w-[750px] px-8 pt-0'>
|
28
|
+
<div className='h-full w-full max-w-[550px] mx-auto flex flex-col justify-between min-h-screen py-10'>
|
29
|
+
<Button
|
30
|
+
variant='ghost'
|
31
|
+
onClick={close}
|
32
|
+
className='w-fit !min-w-0 p-2'
|
33
|
+
>
|
34
|
+
<Logo size='md' spanClassName='!cursor-pointer' layout='text-only'/>
|
35
|
+
</Button>
|
36
|
+
<Carousel
|
37
|
+
options={{ align: 'center', loop: true }}
|
38
|
+
className='w-full'
|
39
|
+
plugins={[Autoplay({ delay: 5000, stopOnInteraction: true })]}
|
40
|
+
>
|
41
|
+
<CarouselContent>
|
42
|
+
{reviews.map(({text, author, href}, index) => (
|
43
|
+
<CarouselItem key={index}>
|
44
|
+
<Link href={href} className='flex flex-col gap-3 cursor-pointer'>
|
45
|
+
<p>“{text}“</p>
|
46
|
+
<p className='text-sm'>{author}</p>
|
47
|
+
</Link>
|
48
|
+
</CarouselItem>
|
49
|
+
))}
|
50
|
+
</CarouselContent>
|
51
|
+
</Carousel>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
</div>
|
55
|
+
<div className='w-full h-full bg-background flex flex-row items-center'>
|
56
|
+
<div className='w-full max-w-[750px] relative flex flex-col items-center px-8 pt-0 text-center'>
|
57
|
+
<div className='relative h-full w-full max-w-[400px] mx-auto flex flex-col gap-4 items-center py-10'>
|
58
|
+
<Button
|
59
|
+
variant='ghost'
|
60
|
+
onClick={close}
|
61
|
+
className='block md:hidden absolute rounded-full p-2 left-0 h-auto hover:bg-background'
|
62
|
+
>
|
63
|
+
<LuxLogo className='w-5 h-5'/>
|
64
|
+
</Button>
|
65
|
+
{/* TODO: add Terms of Service and Privacy Policy links */}
|
66
|
+
<Login
|
67
|
+
getStartedUrl={getStartedUrl}
|
68
|
+
redirectUrl={redirectUrl}
|
69
|
+
className='w-full max-w-sm'
|
70
|
+
termsOfServiceUrl=''
|
71
|
+
privacyPolicyUrl=''
|
72
|
+
/>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
)
|
78
|
+
}
|
79
|
+
|
80
|
+
export default LoginPanel
|
@@ -0,0 +1,77 @@
|
|
1
|
+
'use client'
|
2
|
+
import React from 'react'
|
3
|
+
|
4
|
+
import { Button, Card } from '@hanzo/ui/primitives'
|
5
|
+
|
6
|
+
import LuxLogo from './icons/lux-logo'
|
7
|
+
import { cn } from '@hanzo/ui/util'
|
8
|
+
|
9
|
+
const ChatWidget: React.FC<{
|
10
|
+
title: string,
|
11
|
+
chatbotUrl: string,
|
12
|
+
subtitle?: string,
|
13
|
+
question?: string,
|
14
|
+
/*
|
15
|
+
Currently supports these icons from remix icons (https://remixicon.com/):
|
16
|
+
GlobalLineIcon,
|
17
|
+
ShieldFlashLineIcon,
|
18
|
+
BankCardLineIcon,
|
19
|
+
GroupLineIcon,
|
20
|
+
QuestionnaireLineIcon
|
21
|
+
*/
|
22
|
+
suggestedQuestions?: { heading: string, message: string, icon?: string }[]
|
23
|
+
}> = ({
|
24
|
+
title,
|
25
|
+
chatbotUrl,
|
26
|
+
subtitle,
|
27
|
+
question,
|
28
|
+
suggestedQuestions
|
29
|
+
}) => {
|
30
|
+
|
31
|
+
const [showChatbot, setShowChatbot] = React.useState<boolean>(false)
|
32
|
+
|
33
|
+
const onClick = () => { setShowChatbot(!showChatbot) }
|
34
|
+
|
35
|
+
const searchParams = new URLSearchParams()
|
36
|
+
if (question) {
|
37
|
+
searchParams.append('question', question)
|
38
|
+
}
|
39
|
+
if (suggestedQuestions) {
|
40
|
+
searchParams.append('sQuestions', suggestedQuestions.map(({ message }) => message).join(','))
|
41
|
+
searchParams.append('sHeadings', suggestedQuestions.map(({ heading }) => heading).join(','))
|
42
|
+
searchParams.append('sIcons', suggestedQuestions.map(({ icon }) => icon).join(','))
|
43
|
+
}
|
44
|
+
|
45
|
+
const iframeSrc = `${chatbotUrl}?${searchParams.toString()}`
|
46
|
+
|
47
|
+
return (<>
|
48
|
+
<div className={
|
49
|
+
'fixed bottom-0 sm:bottom-16 right-0 w-full h-full ' +
|
50
|
+
'sm:max-w-[400px] sm:max-h-[550px] sm:px-4 z-above-floating ' +
|
51
|
+
(showChatbot ? 'flex' : 'hidden')
|
52
|
+
}>
|
53
|
+
<Card className='flex flex-col h-full w-full'>
|
54
|
+
<div className='flex px-4 py-2 h-12 bg-level-0 items-center justify-between'>
|
55
|
+
<h3 className='font-semibold font-heading'>{title} <span className='opacity-60'>{subtitle}</span></h3>
|
56
|
+
<Button onClick={onClick} variant='link' size='icon' className='w-fit sm:hidden'>
|
57
|
+
<LuxLogo width={24} height={24}/>
|
58
|
+
</Button>
|
59
|
+
</div>
|
60
|
+
<iframe src={iframeSrc} className='h-full' />
|
61
|
+
</Card>
|
62
|
+
</div>
|
63
|
+
|
64
|
+
<LuxLogo
|
65
|
+
width={28}
|
66
|
+
height={28}
|
67
|
+
onClick={onClick}
|
68
|
+
className={cn(
|
69
|
+
'fixed bottom-5 right-5 z-floating transition-all cursor-pointer hover:drop-shadow-[0_2px_6px_rgba(255,255,255,1)]',
|
70
|
+
showChatbot ? 'rotate-180' : ''
|
71
|
+
)}
|
72
|
+
strokeWidth={1}
|
73
|
+
/>
|
74
|
+
</>)
|
75
|
+
}
|
76
|
+
|
77
|
+
export default ChatWidget
|
@@ -0,0 +1,67 @@
|
|
1
|
+
'use client'
|
2
|
+
import React from 'react'
|
3
|
+
import { observer } from 'mobx-react-lite'
|
4
|
+
|
5
|
+
import { buttonVariants, type ButtonSizes } from '@hanzo/ui/primitives'
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
7
|
+
import { useCommerce } from '@hanzo/commerce'
|
8
|
+
|
9
|
+
import * as Icons from '../icons'
|
10
|
+
|
11
|
+
const BagButton: React.FC<{
|
12
|
+
showIfEmpty?: boolean
|
13
|
+
noHoverEffects?: boolean
|
14
|
+
size?: ButtonSizes
|
15
|
+
className?: string
|
16
|
+
iconClx?: string
|
17
|
+
onClick?: () => void
|
18
|
+
}> = observer(({
|
19
|
+
showIfEmpty=false,
|
20
|
+
noHoverEffects=false,
|
21
|
+
size='default',
|
22
|
+
className='',
|
23
|
+
iconClx='',
|
24
|
+
onClick
|
25
|
+
}) => {
|
26
|
+
|
27
|
+
const c = useCommerce()
|
28
|
+
|
29
|
+
// undefined means context is not installed, ie commerce functions are not in use
|
30
|
+
if (!c || (!showIfEmpty && c.cartEmpty)) {
|
31
|
+
return <div /> // trigger code needs non-null
|
32
|
+
}
|
33
|
+
|
34
|
+
return (
|
35
|
+
<div
|
36
|
+
aria-label="Bag"
|
37
|
+
role='button'
|
38
|
+
onClick={onClick}
|
39
|
+
className={cn(
|
40
|
+
buttonVariants({ variant: 'ghost', size, rounded: 'md' }),
|
41
|
+
// Overides the bg change on hover --not a "hover effect"
|
42
|
+
'relative group p-0 aspect-square hover:bg-background',
|
43
|
+
className
|
44
|
+
)}
|
45
|
+
>
|
46
|
+
{!c.cartEmpty && (
|
47
|
+
<div className={
|
48
|
+
'z-above-content flex flex-col justify-center items-center ' +
|
49
|
+
'absolute left-0 right-0 top-0 bottom-0 ' +
|
50
|
+
'leading-none font-sans font-bold text-primary-fg text-accent text-xs '
|
51
|
+
}>
|
52
|
+
<div className='h-[3px] w-full' />
|
53
|
+
<div>{c.cartQuantity}</div>
|
54
|
+
</div>
|
55
|
+
)}
|
56
|
+
<Icons.bag className={cn(
|
57
|
+
'relative -top-[3px] fill-primary w-6 h-7 ',
|
58
|
+
iconClx,
|
59
|
+
(noHoverEffects ? '' : (
|
60
|
+
'group-hover:fill-primary-hover group-hover:scale-105 transition-scale transition-duration-300'
|
61
|
+
))
|
62
|
+
)} aria-hidden="true" />
|
63
|
+
</div>
|
64
|
+
)
|
65
|
+
})
|
66
|
+
|
67
|
+
export default BagButton
|
@@ -0,0 +1,23 @@
|
|
1
|
+
'use client'
|
2
|
+
import React from 'react'
|
3
|
+
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
5
|
+
|
6
|
+
import Logo from '../../logo'
|
7
|
+
|
8
|
+
const CloseButton: React.FC<{
|
9
|
+
close: () => void
|
10
|
+
className?: string
|
11
|
+
}> = ({
|
12
|
+
close,
|
13
|
+
className=''
|
14
|
+
}) => (
|
15
|
+
<div
|
16
|
+
onClick={close}
|
17
|
+
className={cn('md:self-start', className)}
|
18
|
+
>
|
19
|
+
<Logo layout='text-only' href='/'/>
|
20
|
+
</div>
|
21
|
+
)
|
22
|
+
|
23
|
+
export default CloseButton
|
@@ -0,0 +1,36 @@
|
|
1
|
+
'use client'
|
2
|
+
import React from 'react'
|
3
|
+
import { observer } from 'mobx-react-lite'
|
4
|
+
|
5
|
+
import {
|
6
|
+
useCommerce,
|
7
|
+
CarouselItemSelector,
|
8
|
+
type CarouselItemSelectorPropsExt
|
9
|
+
} from '@hanzo/commerce'
|
10
|
+
|
11
|
+
const DesktopBagCarousel: React.FC<{
|
12
|
+
constrainTo: {w: number, h: number}
|
13
|
+
className?: string
|
14
|
+
}> = observer(({
|
15
|
+
constrainTo,
|
16
|
+
className=''
|
17
|
+
|
18
|
+
}) => {
|
19
|
+
const cmmc = useCommerce()
|
20
|
+
return (
|
21
|
+
<CarouselItemSelector
|
22
|
+
items={cmmc.cartItems}
|
23
|
+
selectedItemRef={cmmc}
|
24
|
+
scrollable={false} // ignored
|
25
|
+
selectSku={cmmc.setCurrentItem.bind(cmmc)}
|
26
|
+
clx={className}
|
27
|
+
ext={{
|
28
|
+
options: {loop: true},
|
29
|
+
constrainTo,
|
30
|
+
imageOnly: true
|
31
|
+
} satisfies CarouselItemSelectorPropsExt}
|
32
|
+
/>
|
33
|
+
)
|
34
|
+
})
|
35
|
+
|
36
|
+
export default DesktopBagCarousel
|
@@ -0,0 +1,68 @@
|
|
1
|
+
'use client'
|
2
|
+
import React, { type PropsWithChildren } from 'react'
|
3
|
+
|
4
|
+
import { ScrollArea } from '@hanzo/ui/primitives'
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
6
|
+
import { AuthWidget } from '@hanzo/auth/components'
|
7
|
+
import { CartPanel } from '@hanzo/commerce'
|
8
|
+
|
9
|
+
import * as Icons from '../../icons'
|
10
|
+
import DesktopBagCarousel from './dt-bag-carousel'
|
11
|
+
import CloseButton from './close-button'
|
12
|
+
import LinksRow from './links-row'
|
13
|
+
import StepsIndicator from './steps-indicator'
|
14
|
+
|
15
|
+
const DesktopCheckoutPanel: React.FC<PropsWithChildren & {
|
16
|
+
index: number
|
17
|
+
stepNames: string[]
|
18
|
+
close:() => void
|
19
|
+
className?: string
|
20
|
+
}> = ({
|
21
|
+
index,
|
22
|
+
stepNames,
|
23
|
+
close,
|
24
|
+
className='',
|
25
|
+
children
|
26
|
+
}) => (
|
27
|
+
|
28
|
+
<div /* id='CHECKOUT_PANEL' */ className={cn('grid grid-cols-2', className)}>
|
29
|
+
<ScrollArea className='w-full h-full bg-level-1 flex flex-row items-start overflow-y-auto min-h-screen'>
|
30
|
+
<div className='h-full w-full flex justify-end'>
|
31
|
+
<div className='h-full w-full max-w-[750px] px-8 pt-0'>
|
32
|
+
<div className='h-full w-full max-w-[550px] mx-auto flex flex-col gap-3 justify-end min-h-screen'>
|
33
|
+
<div className='flex flex-col gap-3 h-30 justify-center'>
|
34
|
+
<CloseButton close={close} />
|
35
|
+
<StepsIndicator currentStep={index} stepNames={stepNames}/>
|
36
|
+
</div>
|
37
|
+
{children}
|
38
|
+
<LinksRow className='mt-auto mb-3' />
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
</div>
|
42
|
+
</ScrollArea>
|
43
|
+
<div className='w-full h-full bg-background flex flex-row items-start justify-start'>
|
44
|
+
<div className='w-full max-w-[750px] relative flex flex-col items-center justify-start px-8 pt-0'>
|
45
|
+
<AuthWidget noLogin className='hidden md:flex absolute top-4 right-4 '/>
|
46
|
+
<div className='flex items-center justify-center h-30'>
|
47
|
+
<Icons.bag className='fill-foreground mr-2 relative -top-1 w-6 h-7'/>
|
48
|
+
<p className='font-heading text-default'>Order Summary</p>
|
49
|
+
</div>
|
50
|
+
<div className='w-full max-w-[550px] mx-auto'>
|
51
|
+
<DesktopBagCarousel className='h-[260px] w-[360px] lg:w-[420px] mx-auto -mt-8' constrainTo={{w: 250, h: 250}}/>
|
52
|
+
<CartPanel
|
53
|
+
className='w-full border-none p-0 pr-3'
|
54
|
+
itemClx='mb-3'
|
55
|
+
totalClx='sticky bottom-0 p-1 bg-background'
|
56
|
+
scrollAfter={5}
|
57
|
+
scrollHeightClx='h-[50vh]'
|
58
|
+
selectItems
|
59
|
+
showPromoCode
|
60
|
+
showShipping
|
61
|
+
/>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
)
|
67
|
+
|
68
|
+
export default DesktopCheckoutPanel
|
@@ -0,0 +1,124 @@
|
|
1
|
+
'use client'
|
2
|
+
import React, { useEffect, useRef, useState } from 'react'
|
3
|
+
|
4
|
+
import { capitalize, cn } from '@hanzo/ui/util'
|
5
|
+
|
6
|
+
import { useCommerce, ShippingStepForm, PaymentStepForm } from '@hanzo/commerce'
|
7
|
+
import type { CheckoutStep } from '@hanzo/commerce/types'
|
8
|
+
|
9
|
+
import ThankYou from './thank-you'
|
10
|
+
|
11
|
+
const STEPS = [
|
12
|
+
{
|
13
|
+
name: 'payment',
|
14
|
+
Comp: PaymentStepForm
|
15
|
+
},
|
16
|
+
{
|
17
|
+
name: 'delivery',
|
18
|
+
Comp: ShippingStepForm
|
19
|
+
},
|
20
|
+
{
|
21
|
+
name: 'done',
|
22
|
+
label: 'Done!',
|
23
|
+
Comp: ThankYou
|
24
|
+
}
|
25
|
+
] satisfies CheckoutStep[]
|
26
|
+
|
27
|
+
const STEP_NAMES = STEPS.map((s) => (s.label ? s.label : capitalize(s.name)))
|
28
|
+
|
29
|
+
import DesktopCP from './dt-checkout-panel'
|
30
|
+
import MobileCP from './mb-checkout-panel'
|
31
|
+
|
32
|
+
const CheckoutPanel: React.FC<{
|
33
|
+
close: () => void
|
34
|
+
className?: string
|
35
|
+
}> = ({
|
36
|
+
close,
|
37
|
+
className=''
|
38
|
+
}) => {
|
39
|
+
|
40
|
+
const cmmc = useCommerce()
|
41
|
+
|
42
|
+
// For sites that don't initialize cmmc
|
43
|
+
if (!cmmc) {
|
44
|
+
return <></>
|
45
|
+
}
|
46
|
+
const [stepIndex, setStepIndex] = useState<number>(0)
|
47
|
+
const [orderId, setOrderId] = useState<string | undefined>(undefined)
|
48
|
+
|
49
|
+
// Step.name or 'first' or 'next' or 'last'
|
50
|
+
const setStep = (name: string): void => {
|
51
|
+
|
52
|
+
if (name === 'first') {
|
53
|
+
setStepIndex(0)
|
54
|
+
}
|
55
|
+
else if (name === 'last') {
|
56
|
+
setStepIndex(STEPS.length - 1)
|
57
|
+
}
|
58
|
+
else if (name === 'next') {
|
59
|
+
if (stepIndex <= STEPS.length - 2) {
|
60
|
+
setStepIndex(stepIndex + 1)
|
61
|
+
}
|
62
|
+
else {
|
63
|
+
throw new Error('CheckoutPanel.setStep(): Attempting to advance past last step!')
|
64
|
+
}
|
65
|
+
}
|
66
|
+
else {
|
67
|
+
const indexFound = STEPS.findIndex((el) => (el.name === name))
|
68
|
+
if (indexFound !== -1) {
|
69
|
+
setStepIndex(indexFound)
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
throw new Error('CheckoutPanel.setStep(): Step named ' + name + ' not found!')
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
const _close = () => {
|
78
|
+
setStep('first')
|
79
|
+
close()
|
80
|
+
}
|
81
|
+
|
82
|
+
// Determine if mobile or desktop based on visibility of desktopElement
|
83
|
+
// https://stackoverflow.com/a/21696585/11378853
|
84
|
+
const desktopElement = useRef<HTMLDivElement | null>(null)
|
85
|
+
const [layout, setLayout] = useState<'mobile' | 'desktop' | undefined>()
|
86
|
+
useEffect(() => {
|
87
|
+
const checkLayout = () => {
|
88
|
+
setLayout(!!desktopElement.current?.offsetParent ? 'desktop' : 'mobile')
|
89
|
+
}
|
90
|
+
|
91
|
+
// initial layout check
|
92
|
+
checkLayout()
|
93
|
+
|
94
|
+
window.addEventListener('resize', checkLayout)
|
95
|
+
return () => {
|
96
|
+
window.removeEventListener('resize', checkLayout)
|
97
|
+
}
|
98
|
+
}, [])
|
99
|
+
|
100
|
+
const StepToRender = STEPS[stepIndex].Comp
|
101
|
+
|
102
|
+
return (<>
|
103
|
+
<DesktopCP
|
104
|
+
className={cn('h-full', className, 'hidden md:flex')}
|
105
|
+
close={_close}
|
106
|
+
index={stepIndex}
|
107
|
+
stepNames={STEP_NAMES}
|
108
|
+
>
|
109
|
+
{/* Element required to determine if DesktopCP is visible */}
|
110
|
+
<div ref={desktopElement}/>
|
111
|
+
{layout === 'desktop' && <StepToRender onDone={() => {setStep('next')}} orderId={orderId} setOrderId={setOrderId}/>}
|
112
|
+
</DesktopCP>
|
113
|
+
<MobileCP
|
114
|
+
className={cn('h-full overflow-y-auto', className, 'md:hidden' )}
|
115
|
+
close={_close}
|
116
|
+
index={stepIndex}
|
117
|
+
stepNames={STEP_NAMES}
|
118
|
+
>
|
119
|
+
{layout === 'mobile' && <StepToRender onDone={() => {setStep('next')}} orderId={orderId} setOrderId={setOrderId}/>}
|
120
|
+
</MobileCP>
|
121
|
+
</>)
|
122
|
+
}
|
123
|
+
|
124
|
+
export default CheckoutPanel
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import Link from 'next/link'
|
2
|
+
|
3
|
+
import { Separator } from '@hanzo/ui/primitives'
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
5
|
+
|
6
|
+
const LinksRow: React.FC<{
|
7
|
+
className?: string
|
8
|
+
}> = ({
|
9
|
+
className=''
|
10
|
+
}) => (
|
11
|
+
<div className={cn('flex flex-col', className)}>
|
12
|
+
<Separator className='my-1'/>
|
13
|
+
<div className='flex gap-4 text-sm'>
|
14
|
+
{/* TODO: add Refund policy and Privacy policy links */}
|
15
|
+
<Link href=''>Refund policy</Link>
|
16
|
+
<Link href=''>Privacy policy</Link>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
)
|
20
|
+
|
21
|
+
export default LinksRow
|
@@ -0,0 +1,51 @@
|
|
1
|
+
'use client'
|
2
|
+
import React, { type PropsWithChildren } from 'react'
|
3
|
+
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
5
|
+
import { AuthWidget } from '@hanzo/auth/components'
|
6
|
+
import { CartAccordian } from '@hanzo/commerce'
|
7
|
+
|
8
|
+
import CloseButton from './close-button'
|
9
|
+
import BagButton from '../bag-button'
|
10
|
+
import LinksRow from './links-row'
|
11
|
+
import StepsIndicator from './steps-indicator'
|
12
|
+
|
13
|
+
const MobileCheckoutPanel: React.FC<PropsWithChildren & {
|
14
|
+
index: number
|
15
|
+
stepNames: string[]
|
16
|
+
close:() => void
|
17
|
+
className?: string
|
18
|
+
}> = ({
|
19
|
+
index,
|
20
|
+
stepNames,
|
21
|
+
close,
|
22
|
+
className='',
|
23
|
+
children
|
24
|
+
}) => (
|
25
|
+
|
26
|
+
<div /* id='MOBILE_GRID' */ className={cn('bg-background flex flex-col justify-start px-4', className)}>
|
27
|
+
<div className='sticky top-0 w-full flex flex-row justify-between items-center bg-background'>
|
28
|
+
<CloseButton close={close} />
|
29
|
+
<StepsIndicator currentStep={index} stepNames={stepNames}/>
|
30
|
+
{/* Need wrapper div since 'noLogin' returns null if no logged in user */}
|
31
|
+
<div className='w-10 h-10 flex items-center justify-center'><AuthWidget noLogin className=''/></div>
|
32
|
+
</div>
|
33
|
+
<CartAccordian
|
34
|
+
icon={
|
35
|
+
<BagButton
|
36
|
+
noHoverEffects
|
37
|
+
showIfEmpty
|
38
|
+
size='sm'
|
39
|
+
className=
|
40
|
+
'mr-1 relative w-5 h-6 sm:w-6 sm:h-7 '
|
41
|
+
iconClx='fill-foreground '
|
42
|
+
/>
|
43
|
+
}
|
44
|
+
className='flex items-center justify-center py-2 w-full'
|
45
|
+
/>
|
46
|
+
{children}
|
47
|
+
<LinksRow className='mt-auto mb-3 pt-2' />
|
48
|
+
</div>
|
49
|
+
)
|
50
|
+
|
51
|
+
export default MobileCheckoutPanel
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import {
|
2
|
+
Breadcrumb,
|
3
|
+
BreadcrumbItem,
|
4
|
+
BreadcrumbLink,
|
5
|
+
BreadcrumbList,
|
6
|
+
BreadcrumbSeparator
|
7
|
+
} from '@hanzo/ui/primitives'
|
8
|
+
import { cn } from '@hanzo/ui/util'
|
9
|
+
|
10
|
+
const StepsIndicator: React.FC<{
|
11
|
+
currentStep: number
|
12
|
+
stepNames: string[]
|
13
|
+
className?: string
|
14
|
+
}> = ({
|
15
|
+
currentStep,
|
16
|
+
stepNames,
|
17
|
+
className=''
|
18
|
+
}) => (
|
19
|
+
<Breadcrumb className={className}>
|
20
|
+
<BreadcrumbList>
|
21
|
+
{stepNames.map((name, i) => (
|
22
|
+
<>
|
23
|
+
<BreadcrumbItem key={`item-${i}`}>
|
24
|
+
<BreadcrumbLink className={cn(
|
25
|
+
currentStep >= i ? '!text-foreground hover:text-foreground' : 'hover:text-muted-2',
|
26
|
+
'text-xxs sm:text-sm'
|
27
|
+
)}
|
28
|
+
>
|
29
|
+
{name}
|
30
|
+
</BreadcrumbLink>
|
31
|
+
</BreadcrumbItem>
|
32
|
+
{i !== stepNames.length - 1 && <BreadcrumbSeparator key={`sep-${i}`}/>}
|
33
|
+
</>
|
34
|
+
))}
|
35
|
+
</BreadcrumbList>
|
36
|
+
</Breadcrumb>
|
37
|
+
)
|
38
|
+
|
39
|
+
export default StepsIndicator
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import Link from 'next/link'
|
3
|
+
|
4
|
+
import { ApplyTypography } from '@hanzo/ui/primitives'
|
5
|
+
|
6
|
+
import type { CheckoutStepComponentProps } from '@hanzo/commerce/types'
|
7
|
+
|
8
|
+
const ThankYou: React.FC<CheckoutStepComponentProps> = ({}) => (
|
9
|
+
<ApplyTypography className='flex flex-col gap-4 text-center mt-10'>
|
10
|
+
<h3>Thank you for your order!</h3>
|
11
|
+
<h6>Once your payment has been confirmed, you'll recieve an email with additional information.</h6>
|
12
|
+
<p>
|
13
|
+
While you wait, we cordially invite you to join the <Link href='https://warpcast.com/~/channel/lux'>Lux Channel</Link> on <Link href='https://warpcast.com/~/invite-page/227706?id=fbc9ca91'>Warpcast</Link>.
|
14
|
+
</p>
|
15
|
+
</ApplyTypography>
|
16
|
+
)
|
17
|
+
|
18
|
+
export default ThankYou
|