@luxfi/ui 5.5.4 → 5.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commerce/ui/context.tsx +13 -16
- package/components/access-code-input.tsx +3 -4
- package/components/analytics.tsx +8 -17
- package/components/auth/callback.tsx +40 -0
- package/components/auth/login-panel.tsx +22 -50
- package/components/auth/login.tsx +47 -0
- package/components/auth/provider.tsx +34 -0
- package/components/auth/resume.ts +16 -0
- package/components/auth/widget.tsx +80 -0
- package/components/back-button.tsx +4 -7
- package/components/chat-widget.tsx +9 -11
- package/components/commerce/bag-button.tsx +8 -7
- package/components/commerce/buy-button.tsx +2 -3
- package/components/commerce/checkout-button.tsx +12 -10
- package/components/commerce/checkout-panel/cart-accordian.tsx +3 -4
- package/components/commerce/checkout-panel/desktop-cp.tsx +7 -8
- package/components/commerce/checkout-panel/index.tsx +1 -1
- package/components/commerce/checkout-panel/mobile-cp.tsx +5 -5
- package/components/commerce/checkout-panel/policy-links.tsx +7 -12
- package/components/commerce/checkout-panel/steps-indicator.tsx +1 -1
- package/components/commerce/desktop-bag-popup.tsx +1 -1
- package/components/commerce/drawer/micro.tsx +30 -30
- package/components/commerce/drawer/shell.tsx +3 -5
- package/components/contact-dialog/contact-form.tsx +6 -13
- package/components/contact-dialog/index.tsx +2 -2
- package/components/copyright.tsx +3 -2
- package/components/drawer-margin.tsx +2 -2
- package/components/footer.tsx +7 -8
- package/components/header/desktop-nav-menu.tsx +6 -6
- package/components/header/desktop.tsx +9 -9
- package/components/header/index.tsx +2 -2
- package/components/header/mobile-bag-drawer.tsx +1 -1
- package/components/header/mobile-menu-toggle-button.tsx +5 -6
- package/components/header/mobile-nav-menu-ai.tsx +14 -13
- package/components/header/mobile-nav-menu-item.tsx +12 -11
- package/components/header/mobile-nav-menu.tsx +19 -18
- package/components/header/mobile.tsx +10 -11
- package/components/icons/index.ts +0 -1
- package/components/icons/social-icon.tsx +1 -1
- package/components/index.ts +2 -2
- package/components/logo.tsx +40 -67
- package/components/main.tsx +3 -4
- package/components/mini-chart/mini-chart.tsx +2 -1
- package/components/security/index.tsx +55 -0
- package/next/font/font-desc.ts +18 -0
- package/next/font/fonts.ts +40 -0
- package/next/font/get-app-router-font-classes.ts +14 -12
- package/next/font/load-and-return-lux-next-fonts-on-import.ts +16 -24
- package/next/font/local/Zen-Variable.woff2 +0 -0
- package/next/font/next-font-desc.ts +18 -17
- package/next/head-metadata/index.tsx +7 -6
- package/next/head-metadata/next-metadata.ts +49 -0
- package/next/middleware/determine-device-mw.ts +0 -13
- package/package.json +15 -15
- package/root-layout/index.tsx +42 -29
- package/style/fonts/Zen-Variable.woff2 +0 -0
- package/style/lux-colors.css +0 -1
- package/style/lux-fonts.css +5 -11
- package/style/lux-global-non-next.css +9 -9
- package/style/lux-global.css +99 -15
- package/tsconfig.json +8 -0
- package/types/site-def.ts +7 -0
- package/components/auth/auth-listener.tsx +0 -33
- package/components/auth/auth-token/clear-auth-token.tsx +0 -12
- package/components/auth/auth-token/set-auth-token.tsx +0 -16
- package/components/auth/common-auth-domains.ts +0 -17
- package/components/auth/mobile-login-button.tsx +0 -112
- package/components/auth/signup-panel.tsx +0 -113
- package/components/icons/lux-logo.tsx +0 -10
- package/next/font/local/Druk-Wide-Bold.ttf +0 -0
- package/next/font/local/Druk-Wide-Medium.ttf +0 -0
- package/next/head-metadata/from-next/metadata-types.ts +0 -158
- package/next/head-metadata/from-next/opengraph-types.ts +0 -267
- package/next/head-metadata/from-next/twitter-types.ts +0 -92
- package/style/fonts/Druk-Wide-Bold.ttf +0 -0
- package/style/fonts/Druk-Wide-Medium.ttf +0 -0
- package/tailwind/fontFamily.tailwind.lux.ts +0 -18
- package/tailwind/index.ts +0 -2
- package/tailwind/lux-tw-fonts.ts +0 -40
- package/tailwind/tailwind.config.lux-preset.ts +0 -10
package/commerce/ui/context.tsx
CHANGED
|
@@ -12,13 +12,17 @@ import { usePathname } from 'next/navigation'
|
|
|
12
12
|
import { useDebounceCallback } from 'usehooks-ts'
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
import { preset as twConfig } from '@hanzo/ui/tailwind'
|
|
16
15
|
import { useCommerce } from '@hanzo/commerce'
|
|
17
16
|
|
|
18
17
|
import type { CommerceDrawer, SelectAndBuy } from './store'
|
|
19
18
|
import { CommerceUIStore } from './store'
|
|
20
19
|
import conf from './conf'
|
|
21
20
|
|
|
21
|
+
/** The `md` rung: below this the commerce drawer behaves as a mobile sheet.
|
|
22
|
+
* It used to be read out of a Tailwind preset's `screens.md`, which is one
|
|
23
|
+
* more place for a number that belongs in one. */
|
|
24
|
+
const DESKTOP_MIN = 768
|
|
25
|
+
|
|
22
26
|
const LOG = false ////////////////////
|
|
23
27
|
const log = (s: string) => {
|
|
24
28
|
if (LOG) {
|
|
@@ -48,23 +52,16 @@ const CommerceUIProvider: React.FC<PropsWithChildren> = ({
|
|
|
48
52
|
const storeRef = useRef<CommerceUIStore>(new CommerceUIStore(cmmc, conf))
|
|
49
53
|
const prevPathRef = useRef<string>('initial')
|
|
50
54
|
|
|
51
|
-
const onResize = () => {
|
|
52
|
-
const width = window.innerWidth
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if ('md' in twConfig.theme?.screens && typeof twConfig.theme?.screens.md === 'string') {
|
|
57
|
-
desktopMin = parseInt(twConfig.theme?.screens.md)
|
|
58
|
-
}
|
|
59
|
-
if (width < desktopMin) {
|
|
60
|
-
if (!storeRef.current.isMobile) {
|
|
61
|
-
storeRef.current.setMobile(true)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
else if (storeRef.current.isMobile) {
|
|
65
|
-
storeRef.current.setMobile(false)
|
|
55
|
+
const onResize = () => {
|
|
56
|
+
const width = window.innerWidth
|
|
57
|
+
if (width < DESKTOP_MIN) {
|
|
58
|
+
if (!storeRef.current.isMobile) {
|
|
59
|
+
storeRef.current.setMobile(true)
|
|
66
60
|
}
|
|
67
61
|
}
|
|
62
|
+
else if (storeRef.current.isMobile) {
|
|
63
|
+
storeRef.current.setMobile(false)
|
|
64
|
+
}
|
|
68
65
|
storeRef.current.setViewportHeight(window.innerHeight)
|
|
69
66
|
}
|
|
70
67
|
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '@hanzo/ui/primitives'
|
|
5
|
-
import { cn } from '@hanzo/ui
|
|
6
|
-
|
|
5
|
+
import { Box, cn } from '@hanzo/ui'
|
|
7
6
|
const AccessCodeInput: React.FC<{
|
|
8
7
|
onSuccess?: () => void
|
|
9
8
|
onFail?: () => void
|
|
@@ -35,7 +34,7 @@ const AccessCodeInput: React.FC<{
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
return (
|
|
38
|
-
<
|
|
37
|
+
<Box className={cn('flex flex-col gap-2 mx-auto w-full text-center items-center', className)}>
|
|
39
38
|
<InputOTP
|
|
40
39
|
className='mx-auto'
|
|
41
40
|
maxLength={6}
|
|
@@ -64,7 +63,7 @@ const AccessCodeInput: React.FC<{
|
|
|
64
63
|
null
|
|
65
64
|
}
|
|
66
65
|
</p>
|
|
67
|
-
</
|
|
66
|
+
</Box>
|
|
68
67
|
)
|
|
69
68
|
}
|
|
70
69
|
|
package/components/analytics.tsx
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react'
|
|
4
3
|
import { GoogleAnalytics } from '@next/third-parties/google'
|
|
5
|
-
import { FacebookPixel } from '../next/analytics'
|
|
6
|
-
|
|
7
|
-
const Analytics = () => {
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
document.body.style.display = 'flex'
|
|
11
|
-
}, [])
|
|
5
|
+
import { FacebookPixel } from '../next/analytics'
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
7
|
+
const Analytics = () => (
|
|
8
|
+
<>
|
|
9
|
+
<FacebookPixel />
|
|
10
|
+
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID ?? ''} />
|
|
11
|
+
</>
|
|
12
|
+
)
|
|
20
13
|
|
|
21
|
-
export {
|
|
22
|
-
Analytics as default
|
|
23
|
-
}
|
|
14
|
+
export { Analytics as default }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useIam } from '@hanzo/iam/react'
|
|
6
|
+
|
|
7
|
+
import { resume } from './resume'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The site's OAuth redirect target. IAM sends the authorization code here and
|
|
11
|
+
* this redeems it for tokens with the PKCE verifier the browser kept, then
|
|
12
|
+
* returns the visitor to the page they left.
|
|
13
|
+
*/
|
|
14
|
+
const Callback = () => {
|
|
15
|
+
|
|
16
|
+
const { handleCallback } = useIam()
|
|
17
|
+
const [error, setError] = useState<string | null>(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
handleCallback()
|
|
21
|
+
.then(() => { window.location.replace(resume()) })
|
|
22
|
+
.catch((e: Error) => { setError(e.message) })
|
|
23
|
+
/* Runs once: the authorization code is single-use. */
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<main className='flex min-h-screen items-center justify-center p-8 text-center'>
|
|
28
|
+
{error ? (
|
|
29
|
+
<div className='flex flex-col gap-4'>
|
|
30
|
+
<p role='alert'>{error}</p>
|
|
31
|
+
<a href='/' className='underline'>Back to the site</a>
|
|
32
|
+
</div>
|
|
33
|
+
) : (
|
|
34
|
+
<p>Signing you in…</p>
|
|
35
|
+
)}
|
|
36
|
+
</main>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default Callback
|
|
@@ -1,58 +1,28 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import React from 'react'
|
|
3
4
|
import Link from 'next/link'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
5
|
-
import { setCookie } from 'cookies-next'
|
|
6
5
|
|
|
7
|
-
import { cn } from '@hanzo/ui
|
|
6
|
+
import { Box, cn } from '@hanzo/ui'
|
|
8
7
|
import { Button, Carousel, CarouselContent, CarouselItem } from '@hanzo/ui/primitives'
|
|
9
|
-
import { LoginPanel as Login } from '@hanzo/auth/components'
|
|
10
8
|
|
|
11
|
-
import
|
|
9
|
+
import Login from './login'
|
|
10
|
+
import { LuxLogo } from '@luxfi/logo'
|
|
12
11
|
import Logo from '../logo'
|
|
13
|
-
import { EmblaAutoplay } from '..'
|
|
14
12
|
import { legal } from '../../site-def/footer'
|
|
15
13
|
|
|
16
14
|
const LoginPanel: React.FC<{
|
|
17
15
|
close: () => void
|
|
18
|
-
getStartedUrl?: string
|
|
19
|
-
redirectUrl?: string
|
|
20
16
|
className?: string
|
|
21
17
|
reviews: { text: string, author: string, href: string }[]
|
|
22
|
-
setIsLogin?: React.Dispatch<React.SetStateAction<boolean>>
|
|
23
18
|
}> = ({
|
|
24
19
|
close,
|
|
25
|
-
getStartedUrl = '/',
|
|
26
|
-
redirectUrl,
|
|
27
20
|
className = '',
|
|
28
|
-
reviews
|
|
29
|
-
setIsLogin
|
|
21
|
+
reviews
|
|
30
22
|
}) => {
|
|
31
|
-
const router = useRouter()
|
|
32
|
-
|
|
33
|
-
const termsOfServiceUrl = legal.find(({ title }) => title === 'Terms and Conditions')?.href || ''
|
|
34
|
-
const privacyPolicyUrl = legal.find(({ title }) => title === 'Privacy Policy')?.href || ''
|
|
35
|
-
const domains = ['lux.id', 'lux.credit', 'lux.market', 'lux.network', 'lux.shop', 'wallet.lux.network', 'safe.lux.network', 'lux.finance', 'lux.exchange', 'lux.quest']
|
|
36
|
-
// TODO :aa shouldn't this happen in @hanzo/auth: components/LoginPanel ??
|
|
37
|
-
// Otherwise, the functionality is split across modules! (and client/fw!)
|
|
38
|
-
// (This was never my intent w the onLoginChanged callback.)
|
|
39
|
-
const onLogin = (token: string) => {
|
|
40
|
-
for (let i = 0; i < domains.length; i ++) {
|
|
41
|
-
setCookie('auth-token', token, {
|
|
42
|
-
domain: domains[i],
|
|
43
|
-
path: '/',
|
|
44
|
-
sameSite: 'none',
|
|
45
|
-
secure: true,
|
|
46
|
-
httpOnly: false,
|
|
47
|
-
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
redirectUrl && router.push(redirectUrl)
|
|
52
|
-
}
|
|
53
23
|
|
|
54
24
|
return (
|
|
55
|
-
<
|
|
25
|
+
<Box className={cn('grid grid-cols-1 md:grid-cols-2', className)}>
|
|
56
26
|
<div className='hidden md:flex w-full h-full bg-[radial-gradient(circle_at_24.1%_68.8%_,_rgba(15,14,14,0)_,_rgba(9,9,9,99.4))] flex-row items-end justify-end overflow-y-auto min-h-screen'>
|
|
57
27
|
<div className='h-full w-full max-w-[750px] px-8 pt-0'>
|
|
58
28
|
<div className='h-full w-full max-w-[550px] mx-auto flex flex-col justify-between min-h-screen py-10'>
|
|
@@ -61,12 +31,14 @@ const LoginPanel: React.FC<{
|
|
|
61
31
|
onClick={close}
|
|
62
32
|
className='w-fit !min-w-0 p-2'
|
|
63
33
|
>
|
|
64
|
-
<Logo size='md'
|
|
34
|
+
<Logo size='md' variant='wordmark' />
|
|
65
35
|
</Button>
|
|
66
36
|
<Carousel
|
|
67
|
-
|
|
37
|
+
// Autoplay is an option now, not a plugin — and it holds while the
|
|
38
|
+
// pointer is over the carousel or focus is inside it, which is
|
|
39
|
+
// what `stopOnInteraction` was asking for.
|
|
40
|
+
options={{ align: 'center', loop: true, autoplay: 5000 }}
|
|
68
41
|
className='w-full'
|
|
69
|
-
plugins={[EmblaAutoplay({ delay: 5000, stopOnInteraction: true })]}
|
|
70
42
|
>
|
|
71
43
|
<CarouselContent>
|
|
72
44
|
{reviews.map(({ text, author, href }, index) => (
|
|
@@ -90,21 +62,21 @@ const LoginPanel: React.FC<{
|
|
|
90
62
|
onClick={close}
|
|
91
63
|
className='block md:hidden absolute rounded-full p-2 left-0 h-auto hover:bg-background'
|
|
92
64
|
>
|
|
93
|
-
<LuxLogo
|
|
65
|
+
<LuxLogo variant='white' size={20} />
|
|
94
66
|
</Button>
|
|
95
|
-
<Login
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
<Login className='w-full max-w-sm' />
|
|
68
|
+
<p className='text-sm opacity-50'>
|
|
69
|
+
{legal.map(({ title, href }, index) => (
|
|
70
|
+
<React.Fragment key={href}>
|
|
71
|
+
{index > 0 && ' · '}
|
|
72
|
+
<Link href={href} className='underline'>{title}</Link>
|
|
73
|
+
</React.Fragment>
|
|
74
|
+
))}
|
|
75
|
+
</p>
|
|
104
76
|
</div>
|
|
105
77
|
</div>
|
|
106
78
|
</div>
|
|
107
|
-
</
|
|
79
|
+
</Box>
|
|
108
80
|
)
|
|
109
81
|
}
|
|
110
82
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@hanzo/ui'
|
|
6
|
+
import { Login as IamLogin, useIam } from '@hanzo/iam/react'
|
|
7
|
+
|
|
8
|
+
import { remember } from './resume'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sign-in rendered in place, on this site's own domain: IAM mints an
|
|
12
|
+
* authorization code bound to a PKCE challenge and the form navigates to
|
|
13
|
+
* /auth/callback to redeem it. No credential ever reaches this bundle's
|
|
14
|
+
* origin, and there is one sign-in surface rather than one per app.
|
|
15
|
+
*/
|
|
16
|
+
const Login: React.FC<{
|
|
17
|
+
className?: string
|
|
18
|
+
onSuccess?: (token: string) => void
|
|
19
|
+
}> = ({
|
|
20
|
+
className,
|
|
21
|
+
onSuccess
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
const { config } = useIam()
|
|
25
|
+
/* OIDC anti-CSRF nonce, one per mounted form. */
|
|
26
|
+
const [state] = useState(() => crypto.randomUUID())
|
|
27
|
+
|
|
28
|
+
/* Sign-in leaves for /auth/callback, so record the page holding this form to
|
|
29
|
+
come back to. */
|
|
30
|
+
useEffect(remember, [])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<IamLogin
|
|
34
|
+
serverUrl={config.serverUrl}
|
|
35
|
+
clientId={config.clientId}
|
|
36
|
+
redirectUri={config.redirectUri}
|
|
37
|
+
organization={config.organization}
|
|
38
|
+
state={state}
|
|
39
|
+
/* Naming a class replaces the view's own, so keep both: the site skins
|
|
40
|
+
.hanzo-iam-* once, in lux-global.css. */
|
|
41
|
+
className={cn('hanzo-iam-card', className)}
|
|
42
|
+
onSuccess={onSuccess}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default Login
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { type PropsWithChildren } from 'react'
|
|
4
|
+
|
|
5
|
+
import { IamProvider } from '@hanzo/iam/react'
|
|
6
|
+
import { BRAND_SERVER_URLS } from '@hanzo/iam/paths'
|
|
7
|
+
import type { IAMConfig } from '@hanzo/iam/browser'
|
|
8
|
+
|
|
9
|
+
/* These are Lux sites, so they sign in at Lux's own issuer. Naming another
|
|
10
|
+
brand's would send a Lux user to a login page that has never heard of the
|
|
11
|
+
redirect_uri they came from. */
|
|
12
|
+
const ORG = 'lux'
|
|
13
|
+
|
|
14
|
+
/* Where the authorization code comes back. Every site mounts <Callback/> here. */
|
|
15
|
+
const CALLBACK = '/auth/callback'
|
|
16
|
+
|
|
17
|
+
/* The OAuth client a site presents itself as: the `<org>-<app>` pair registered
|
|
18
|
+
on lux.id, so lux.shop is `lux-shop`. */
|
|
19
|
+
const configure = (app: string): IAMConfig => ({
|
|
20
|
+
serverUrl: BRAND_SERVER_URLS[ORG],
|
|
21
|
+
organization: ORG,
|
|
22
|
+
clientId: `${ORG}-${app}`,
|
|
23
|
+
/* Read per render rather than at module load: one bundle serves the real
|
|
24
|
+
host and any preview of it, and each must redeem its code back to itself. */
|
|
25
|
+
redirectUri: (typeof window === 'undefined' ? '' : window.location.origin) + CALLBACK,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/* Provider is memoized on the config's values, not its identity, so building a
|
|
29
|
+
fresh object here does not rebuild the SDK. */
|
|
30
|
+
const Auth: React.FC<{ app: string } & PropsWithChildren> = ({ app, children }) => (
|
|
31
|
+
<IamProvider config={configure(app)}>{children}</IamProvider>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export default Auth
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* Where the visitor was when they chose to sign in. The OIDC round-trip lands
|
|
2
|
+
on /auth/callback, so without this every sign-in would end at the home page.
|
|
3
|
+
Session-scoped: it describes one trip through the browser, not a preference. */
|
|
4
|
+
const KEY = 'lux-auth-resume'
|
|
5
|
+
|
|
6
|
+
const remember = () => {
|
|
7
|
+
sessionStorage.setItem(KEY, window.location.pathname + window.location.search)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const resume = (): string => {
|
|
11
|
+
const to = sessionStorage.getItem(KEY)
|
|
12
|
+
sessionStorage.removeItem(KEY)
|
|
13
|
+
return to ?? '/'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { remember, resume }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useContext } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button, Popover, PopoverContent, PopoverTrigger, Separator } from '@hanzo/ui/primitives'
|
|
6
|
+
import { cn } from '@hanzo/ui'
|
|
7
|
+
import { IamContext, useIam, useIamIdentity } from '@hanzo/iam/react'
|
|
8
|
+
|
|
9
|
+
import { remember } from './resume'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Hide the sign-in button, leaving only the profile of an already signed-in
|
|
13
|
+
user. Checkout wants this: it has its own sign-in step. */
|
|
14
|
+
noLogin?: boolean
|
|
15
|
+
className?: string
|
|
16
|
+
/** Sign in in place instead of leaving for lux.id — the mobile drawer opens
|
|
17
|
+
its own panel. */
|
|
18
|
+
handleLogin?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Widget: React.FC<Props> = ({
|
|
22
|
+
noLogin = false,
|
|
23
|
+
handleLogin,
|
|
24
|
+
className
|
|
25
|
+
}) => {
|
|
26
|
+
|
|
27
|
+
const { isAuthenticated, isLoading, login, logout } = useIam()
|
|
28
|
+
const identity = useIamIdentity()
|
|
29
|
+
|
|
30
|
+
/* Say nothing until the stored token has been read, rather than flashing
|
|
31
|
+
"Login" at someone who is already signed in. */
|
|
32
|
+
if (isLoading) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!isAuthenticated) {
|
|
37
|
+
return noLogin ? null : (
|
|
38
|
+
<Button
|
|
39
|
+
variant='primary'
|
|
40
|
+
className={cn('h-8 w-fit !min-w-0', className)}
|
|
41
|
+
onClick={handleLogin ?? (() => { remember(); void login() })}
|
|
42
|
+
>
|
|
43
|
+
Login
|
|
44
|
+
</Button>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Popover>
|
|
50
|
+
<PopoverTrigger asChild>
|
|
51
|
+
<Button
|
|
52
|
+
variant='outline'
|
|
53
|
+
size='icon'
|
|
54
|
+
className={cn('rounded-full text-muted border-2 border-muted bg-level-1 hover:bg-level-2 hover:border-foreground hover:text-foreground uppercase w-8 h-8', className)}
|
|
55
|
+
>{identity?.initials}</Button>
|
|
56
|
+
</PopoverTrigger>
|
|
57
|
+
<PopoverContent className='bg-level-0'>
|
|
58
|
+
<div className='grid gap-4'>
|
|
59
|
+
<div className='space-y-2 truncate'>
|
|
60
|
+
<h4 className='font-medium leading-none truncate'>{identity?.name}</h4>
|
|
61
|
+
{identity?.email && identity.email !== identity.name && (
|
|
62
|
+
<p className='text-sm opacity-50 truncate'>{identity.email}</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<Separator />
|
|
66
|
+
<Button variant='outline' onClick={() => { logout() }}>Logout</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</PopoverContent>
|
|
69
|
+
</Popover>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The widget rides in shared chrome, and not every site mounts <Auth> —
|
|
74
|
+
app/credit carries its own root layout. A site that offers no sign-in shows
|
|
75
|
+
no sign-in, which is why the context is read before any hook that needs it. */
|
|
76
|
+
const AuthWidget: React.FC<Props> = (props) => (
|
|
77
|
+
useContext(IamContext) ? <Widget {...props} /> : null
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export default AuthWidget
|
|
@@ -4,17 +4,14 @@ import { useRouter } from 'next/navigation'
|
|
|
4
4
|
|
|
5
5
|
import { ChevronLeft } from 'lucide-react'
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
import {
|
|
9
8
|
Button,
|
|
10
9
|
buttonVariants,
|
|
11
10
|
} from '@hanzo/ui/primitives'
|
|
12
|
-
|
|
13
|
-
import type { VariantProps } from '@hanzo/ui/util'
|
|
14
|
-
|
|
11
|
+
import { css, type ButtonSize, type ButtonVariant } from '@hanzo/ui'
|
|
15
12
|
const BackButton: React.FC<{
|
|
16
|
-
variant?:
|
|
17
|
-
size?:
|
|
13
|
+
variant?: { variant?: ButtonVariant | null; size?: ButtonSize | null }['variant']
|
|
14
|
+
size?: { variant?: ButtonVariant | null; size?: ButtonSize | null }['size']
|
|
18
15
|
onBack?: () => void
|
|
19
16
|
clx?: string
|
|
20
17
|
iconClx?: string
|
|
@@ -41,7 +38,7 @@ const BackButton: React.FC<{
|
|
|
41
38
|
onClick={back}
|
|
42
39
|
className={clx}
|
|
43
40
|
>
|
|
44
|
-
<ChevronLeft
|
|
41
|
+
<ChevronLeft style={css(iconClx)}/>
|
|
45
42
|
</Button>
|
|
46
43
|
)
|
|
47
44
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
4
|
import { Button, Card } from '@hanzo/ui/primitives'
|
|
5
|
-
import { cn } from '@hanzo/ui
|
|
6
|
-
|
|
7
|
-
import { LuxLogo } from './icons'
|
|
5
|
+
import { Box, cn } from '@hanzo/ui'
|
|
6
|
+
import { LuxLogo } from '@luxfi/logo'
|
|
8
7
|
import type { ChatbotSuggestedQuestion } from '../types'
|
|
9
8
|
|
|
10
9
|
const ChatWidget: React.FC<{
|
|
@@ -49,7 +48,7 @@ const ChatWidget: React.FC<{
|
|
|
49
48
|
const iframeSrc = `${chatbotUrl}?${searchParams.toString()}`
|
|
50
49
|
|
|
51
50
|
return (<>
|
|
52
|
-
<
|
|
51
|
+
<Box className={
|
|
53
52
|
'fixed bottom-0 sm:bottom-16 right-0 w-full h-full ' +
|
|
54
53
|
'sm:max-w-[400px] sm:max-h-[550px] sm:px-4 z-floating ' +
|
|
55
54
|
(showChatbot ? 'flex' : 'hidden')
|
|
@@ -58,16 +57,14 @@ const ChatWidget: React.FC<{
|
|
|
58
57
|
<div className='flex px-4 py-2 h-12 bg-level-0 items-center justify-between'>
|
|
59
58
|
<h1 className='font-semibold font-heading'>{title} <span className='opacity-60'>{subtitle}</span></h1>
|
|
60
59
|
<Button onClick={onClick} variant='link' size='icon' className='w-fit sm:hidden'>
|
|
61
|
-
<LuxLogo
|
|
60
|
+
<LuxLogo variant='white' size={24}/>
|
|
62
61
|
</Button>
|
|
63
62
|
</div>
|
|
64
63
|
<iframe src={iframeSrc} className='h-full' />
|
|
65
64
|
</Card>
|
|
66
|
-
</
|
|
65
|
+
</Box>
|
|
67
66
|
|
|
68
|
-
<
|
|
69
|
-
width={28}
|
|
70
|
-
height={28}
|
|
67
|
+
<Box tag="span"
|
|
71
68
|
onClick={onClick}
|
|
72
69
|
className={cn(
|
|
73
70
|
// z-index should be below anything in commerce-iu (buy drawer and checkout widget)
|
|
@@ -77,8 +74,9 @@ const ChatWidget: React.FC<{
|
|
|
77
74
|
showChatbot ? 'rotate-180' : '',
|
|
78
75
|
buttonClx
|
|
79
76
|
)}
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
>
|
|
78
|
+
<LuxLogo variant='white' size={28} />
|
|
79
|
+
</Box>
|
|
82
80
|
</>)
|
|
83
81
|
}
|
|
84
82
|
|
|
@@ -4,7 +4,7 @@ import { observable, type IObservableValue, reaction, runInAction } from 'mobx'
|
|
|
4
4
|
import { observer } from 'mobx-react-lite'
|
|
5
5
|
|
|
6
6
|
import { buttonVariants } from '@hanzo/ui/primitives'
|
|
7
|
-
import { cn, type
|
|
7
|
+
import { Box, cn, type ButtonSize, type ButtonVariant } from '@hanzo/ui'
|
|
8
8
|
import { useCommerce } from '@hanzo/commerce'
|
|
9
9
|
|
|
10
10
|
import { Bag } from '../icons'
|
|
@@ -13,7 +13,7 @@ const BagButton: React.FC<{
|
|
|
13
13
|
showIfEmpty?: boolean
|
|
14
14
|
animateOnHover?: boolean
|
|
15
15
|
animateOnQuantityChange?: boolean
|
|
16
|
-
size?:
|
|
16
|
+
size?: { variant?: ButtonVariant | null; size?: ButtonSize | null }['size']
|
|
17
17
|
className?: string
|
|
18
18
|
iconClx?: string
|
|
19
19
|
onClick?: () => void
|
|
@@ -57,12 +57,13 @@ const BagButton: React.FC<{
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
|
-
<
|
|
60
|
+
<Box
|
|
61
61
|
aria-label="Bag"
|
|
62
62
|
role='button'
|
|
63
63
|
onClick={onClick}
|
|
64
64
|
className={cn(
|
|
65
|
-
buttonVariants({ variant: 'ghost', size
|
|
65
|
+
buttonVariants({ variant: 'ghost', size }),
|
|
66
|
+
'rounded-md',
|
|
66
67
|
// Overides the bg change on hover --not a "hover effect"
|
|
67
68
|
'relative group p-0 aspect-square hover:bg-background',
|
|
68
69
|
((wiggleRef.current.get() === 'more') ?
|
|
@@ -73,14 +74,14 @@ const BagButton: React.FC<{
|
|
|
73
74
|
)}
|
|
74
75
|
>
|
|
75
76
|
{!c.cartEmpty && (
|
|
76
|
-
<
|
|
77
|
+
<Box className={
|
|
77
78
|
'z-above-content flex flex-col justify-center items-center ' +
|
|
78
79
|
'absolute left-0 right-0 top-0 bottom-0 ' +
|
|
79
80
|
'leading-none font-sans font-bold text-primary-fg text-xs '
|
|
80
81
|
}>
|
|
81
82
|
<div className='h-[3px] w-full' />
|
|
82
83
|
<div>{c.cartQuantity}</div>
|
|
83
|
-
</
|
|
84
|
+
</Box>
|
|
84
85
|
)}
|
|
85
86
|
<Bag width='24' height='28' className={cn(
|
|
86
87
|
'relative -top-[3px] fill-primary',
|
|
@@ -91,7 +92,7 @@ const BagButton: React.FC<{
|
|
|
91
92
|
''
|
|
92
93
|
)
|
|
93
94
|
)} aria-hidden="true" />
|
|
94
|
-
</
|
|
95
|
+
</Box>
|
|
95
96
|
)
|
|
96
97
|
})
|
|
97
98
|
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
import React, {type PropsWithChildren} from 'react'
|
|
3
3
|
|
|
4
4
|
import { Button, buttonVariants } from '@hanzo/ui/primitives'
|
|
5
|
-
import { type VariantProps } from '@hanzo/ui/util'
|
|
6
5
|
|
|
7
|
-
import { cn } from '@hanzo/ui
|
|
6
|
+
import { cn, type ButtonSize, type ButtonVariant } from '@hanzo/ui'
|
|
8
7
|
import { useSelectAndBuy } from '../../commerce/ui/context'
|
|
9
8
|
|
|
10
9
|
const BuyButton: React.FC<
|
|
11
10
|
PropsWithChildren &
|
|
12
|
-
|
|
11
|
+
{ variant?: ButtonVariant | null; size?: ButtonSize | null } &
|
|
13
12
|
{
|
|
14
13
|
skuPath: string
|
|
15
14
|
className?: string
|