@luxfi/ui 5.5.3 → 5.5.5
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/LICENSE +29 -0
- package/components/auth/callback.tsx +40 -0
- package/components/auth/login-panel.tsx +15 -44
- 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/chat-widget.tsx +6 -7
- package/components/commerce/checkout-panel/desktop-cp.tsx +2 -2
- package/components/commerce/checkout-panel/mobile-cp.tsx +2 -2
- package/components/footer-nav.tsx +31 -0
- package/components/footer.tsx +7 -12
- package/components/header/desktop-nav-menu.tsx +4 -6
- package/components/header/desktop.tsx +4 -4
- package/components/header/index.tsx +1 -1
- package/components/header/mobile-nav-menu-ai.tsx +1 -1
- package/components/header/mobile-nav-menu.tsx +2 -2
- package/components/header/mobile.tsx +3 -3
- package/components/icons/index.ts +0 -1
- package/components/index.ts +2 -2
- package/components/logo.tsx +39 -67
- package/components/security/index.tsx +55 -0
- package/next/analytics/fpixel.ts +1 -1
- package/next/middleware/determine-device-mw.ts +0 -13
- package/package.json +31 -28
- package/root-layout/index.tsx +3 -10
- package/site-def/footer/community.tsx +1 -4
- package/site-def/footer/network.ts +2 -2
- package/site-def/main-nav.tsx +19 -19
- package/style/lux-global.css +49 -0
- package/types/site-def.ts +7 -0
- package/util/index.ts +11 -0
- package/components/auth/auth-listener.tsx +0 -29
- 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 -107
- package/components/auth/signup-panel.tsx +0 -113
- package/components/icons/lux-logo.tsx +0 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Lux Partners Limited
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -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,55 +1,26 @@
|
|
|
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
6
|
import { cn } from '@hanzo/ui/util'
|
|
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
12
|
import { EmblaAutoplay } from '..'
|
|
14
13
|
import { legal } from '../../site-def/footer'
|
|
15
14
|
|
|
16
15
|
const LoginPanel: React.FC<{
|
|
17
16
|
close: () => void
|
|
18
|
-
getStartedUrl?: string
|
|
19
|
-
redirectUrl?: string
|
|
20
17
|
className?: string
|
|
21
18
|
reviews: { text: string, author: string, href: string }[]
|
|
22
|
-
setIsLogin?: React.Dispatch<React.SetStateAction<boolean>>
|
|
23
19
|
}> = ({
|
|
24
20
|
close,
|
|
25
|
-
getStartedUrl = '/',
|
|
26
|
-
redirectUrl,
|
|
27
21
|
className = '',
|
|
28
|
-
reviews
|
|
29
|
-
setIsLogin
|
|
22
|
+
reviews
|
|
30
23
|
}) => {
|
|
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
24
|
|
|
54
25
|
return (
|
|
55
26
|
<div className={cn('grid grid-cols-1 md:grid-cols-2', className)}>
|
|
@@ -61,7 +32,7 @@ const LoginPanel: React.FC<{
|
|
|
61
32
|
onClick={close}
|
|
62
33
|
className='w-fit !min-w-0 p-2'
|
|
63
34
|
>
|
|
64
|
-
<Logo size='md'
|
|
35
|
+
<Logo size='md' variant='wordmark' />
|
|
65
36
|
</Button>
|
|
66
37
|
<Carousel
|
|
67
38
|
options={{ align: 'center', loop: true }}
|
|
@@ -90,17 +61,17 @@ const LoginPanel: React.FC<{
|
|
|
90
61
|
onClick={close}
|
|
91
62
|
className='block md:hidden absolute rounded-full p-2 left-0 h-auto hover:bg-background'
|
|
92
63
|
>
|
|
93
|
-
<LuxLogo
|
|
64
|
+
<LuxLogo variant='white' size={20} />
|
|
94
65
|
</Button>
|
|
95
|
-
<Login
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
66
|
+
<Login className='w-full max-w-sm' />
|
|
67
|
+
<p className='text-sm opacity-50'>
|
|
68
|
+
{legal.map(({ title, href }, index) => (
|
|
69
|
+
<React.Fragment key={href}>
|
|
70
|
+
{index > 0 && ' · '}
|
|
71
|
+
<Link href={href} className='underline'>{title}</Link>
|
|
72
|
+
</React.Fragment>
|
|
73
|
+
))}
|
|
74
|
+
</p>
|
|
104
75
|
</div>
|
|
105
76
|
</div>
|
|
106
77
|
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
|
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/util'
|
|
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,7 +4,7 @@ import React from 'react'
|
|
|
4
4
|
import { Button, Card } from '@hanzo/ui/primitives'
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
6
|
|
|
7
|
-
import { LuxLogo } from '
|
|
7
|
+
import { LuxLogo } from '@luxfi/logo'
|
|
8
8
|
import type { ChatbotSuggestedQuestion } from '../types'
|
|
9
9
|
|
|
10
10
|
const ChatWidget: React.FC<{
|
|
@@ -58,16 +58,14 @@ const ChatWidget: React.FC<{
|
|
|
58
58
|
<div className='flex px-4 py-2 h-12 bg-level-0 items-center justify-between'>
|
|
59
59
|
<h1 className='font-semibold font-heading'>{title} <span className='opacity-60'>{subtitle}</span></h1>
|
|
60
60
|
<Button onClick={onClick} variant='link' size='icon' className='w-fit sm:hidden'>
|
|
61
|
-
<LuxLogo
|
|
61
|
+
<LuxLogo variant='white' size={24}/>
|
|
62
62
|
</Button>
|
|
63
63
|
</div>
|
|
64
64
|
<iframe src={iframeSrc} className='h-full' />
|
|
65
65
|
</Card>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
-
<
|
|
69
|
-
width={28}
|
|
70
|
-
height={28}
|
|
68
|
+
<span
|
|
71
69
|
onClick={onClick}
|
|
72
70
|
className={cn(
|
|
73
71
|
// z-index should be below anything in commerce-iu (buy drawer and checkout widget)
|
|
@@ -77,8 +75,9 @@ const ChatWidget: React.FC<{
|
|
|
77
75
|
showChatbot ? 'rotate-180' : '',
|
|
78
76
|
buttonClx
|
|
79
77
|
)}
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
>
|
|
79
|
+
<LuxLogo variant='white' size={28} />
|
|
80
|
+
</span>
|
|
82
81
|
</>)
|
|
83
82
|
}
|
|
84
83
|
|
|
@@ -3,7 +3,7 @@ import React, { type PropsWithChildren } from 'react'
|
|
|
3
3
|
import { observer } from 'mobx-react-lite'
|
|
4
4
|
|
|
5
5
|
import { ScrollArea, StepIndicator } from '@hanzo/ui/primitives'
|
|
6
|
-
import
|
|
6
|
+
import AuthWidget from '../../auth/widget'
|
|
7
7
|
import { CartPanel, useCommerce } from '@hanzo/commerce'
|
|
8
8
|
import { cn } from '@hanzo/ui/util'
|
|
9
9
|
|
|
@@ -26,7 +26,7 @@ const DesktopCheckoutPanel: React.FC<PropsWithChildren & CheckoutPanelProps> = o
|
|
|
26
26
|
<div key={1} className='w-full h-full bg-background flex flex-row items-start justify-end'>
|
|
27
27
|
<div className='w-full h-full max-w-[750px] relative flex flex-col items-stretch justify-start px-8 pb-8'>
|
|
28
28
|
<div key={1} className='h-[80px] grow-0 flex flex-row items-center z-10' >
|
|
29
|
-
<Logo size='md' href='/' onClick={onLeave} variant='
|
|
29
|
+
<Logo size='md' href='/' onClick={onLeave} variant='wordmark' outerClx='logo-outer-tooltip-class' />
|
|
30
30
|
<Tooltip select='.logo-outer-tooltip-class' text='home' position='right' offset={6}/>
|
|
31
31
|
</div>
|
|
32
32
|
<BackButton
|
|
@@ -3,7 +3,7 @@ import React, { type PropsWithChildren } from 'react'
|
|
|
3
3
|
|
|
4
4
|
import { StepIndicator } from '@hanzo/ui/primitives'
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
|
-
import
|
|
6
|
+
import AuthWidget from '../../auth/widget'
|
|
7
7
|
|
|
8
8
|
import { BackButton, Logo } from '../..'
|
|
9
9
|
import BagButton from '../bag-button'
|
|
@@ -31,7 +31,7 @@ const MobileCheckoutPanel: React.FC<PropsWithChildren & CheckoutPanelProps> = ({
|
|
|
31
31
|
}
|
|
32
32
|
onBack={onLeave}
|
|
33
33
|
/>
|
|
34
|
-
<Logo size='xs' variant='
|
|
34
|
+
<Logo size='xs' variant='wordmark' href='/' onClick={onLeave} outerClx='-ml-2'/>
|
|
35
35
|
</div>
|
|
36
36
|
<StepIndicator
|
|
37
37
|
dotSizeRem={1}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import type { LinkDef } from '@hanzo/ui/types'
|
|
5
|
+
import { NavItems } from '@hanzo/ui/primitives'
|
|
6
|
+
|
|
7
|
+
// NavItems styles each item via a callback, and a callback cannot cross the
|
|
8
|
+
// server/client boundary. Creating it here keeps Footer itself on the server.
|
|
9
|
+
const FooterNav: React.FC<{
|
|
10
|
+
items: LinkDef[]
|
|
11
|
+
currentAs?: string
|
|
12
|
+
className?: string
|
|
13
|
+
}> = ({
|
|
14
|
+
items,
|
|
15
|
+
currentAs,
|
|
16
|
+
className = ''
|
|
17
|
+
}) => (
|
|
18
|
+
<NavItems
|
|
19
|
+
items={items}
|
|
20
|
+
currentAs={currentAs}
|
|
21
|
+
as='nav'
|
|
22
|
+
className={className}
|
|
23
|
+
itemClx={(def: LinkDef) => ((def.variant === 'linkFG') ?
|
|
24
|
+
'font-nav text-[15px]/[1.3] font-medium tracking-normal text-muted-1 sm:hover:text-foreground transition-color duration-500'
|
|
25
|
+
:
|
|
26
|
+
'text-[15px]/[1.1] font-normal tracking-[0.2px] text-muted-1 sm:hover:text-foreground transition-color duration-500'
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export default FooterNav
|
package/components/footer.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
7
7
|
import Copyright from './copyright'
|
|
8
8
|
import type { SiteDef } from '../site-def'
|
|
9
9
|
import { legal } from '../site-def/footer/legal'
|
|
10
|
+
import FooterNav from './footer-nav'
|
|
10
11
|
import Logo from './logo'
|
|
11
12
|
|
|
12
13
|
const Footer: React.FC<{
|
|
@@ -33,7 +34,7 @@ const Footer: React.FC<{
|
|
|
33
34
|
'md:flex md:flex-row md:justify-between px-[24px]'
|
|
34
35
|
}>
|
|
35
36
|
<div className='hidden lg:flex flex-col' key={0}>
|
|
36
|
-
<Logo size='md' variant='
|
|
37
|
+
<Logo size='md' variant='wordmark' />
|
|
37
38
|
</div>
|
|
38
39
|
{footer.map((defs: LinkDef[], index: number) => {
|
|
39
40
|
|
|
@@ -41,21 +42,15 @@ const Footer: React.FC<{
|
|
|
41
42
|
'xs:col-span-2 xs:mx-auto md:col-span-1 md:mx-0 ' : ''
|
|
42
43
|
|
|
43
44
|
return (
|
|
44
|
-
<
|
|
45
|
-
items={defs}
|
|
45
|
+
<FooterNav
|
|
46
|
+
items={defs}
|
|
46
47
|
currentAs={siteDef.currentAs}
|
|
47
|
-
|
|
48
|
-
className={cn('sm:min-w-[150px] md:min-w-0 flex flex-col justify-start items-start ' +
|
|
48
|
+
className={cn('sm:min-w-[150px] md:min-w-0 flex flex-col justify-start items-start ' +
|
|
49
49
|
'gap-[11px] sm:gap-[12px] md:gap-[15px] ',
|
|
50
50
|
xsColSpanClx
|
|
51
|
-
)}
|
|
52
|
-
key={index + 1}
|
|
53
|
-
itemClx={(def: LinkDef) => ((def.variant === 'linkFG') ?
|
|
54
|
-
'font-nav text-[15px]/[1.3] font-medium tracking-normal text-muted-1 sm:hover:text-foreground transition-color duration-500'
|
|
55
|
-
:
|
|
56
|
-
'text-[15px]/[1.1] font-normal tracking-[0.2px] text-muted-1 sm:hover:text-foreground transition-color duration-500'
|
|
57
51
|
)}
|
|
58
|
-
|
|
52
|
+
key={index + 1}
|
|
53
|
+
/>
|
|
59
54
|
)
|
|
60
55
|
})}
|
|
61
56
|
</div>
|
|
@@ -68,10 +68,8 @@ const DesktopNav: React.FC<{
|
|
|
68
68
|
{links.map((el, index) => (
|
|
69
69
|
<NavigationMenuItem key={index} className='!m-0'>
|
|
70
70
|
{el.isAIMenu ? (
|
|
71
|
-
<Link href={el.href}
|
|
72
|
-
|
|
73
|
-
{el.title}
|
|
74
|
-
</NavigationMenuLink>
|
|
71
|
+
<Link href={el.href} className={cn(navigationMenuTriggerStyle(), ' text-muted-1 bg-transparent')}>
|
|
72
|
+
{el.title}
|
|
75
73
|
</Link>
|
|
76
74
|
) : el.title === 'Cards' ? (
|
|
77
75
|
<>
|
|
@@ -82,7 +80,7 @@ const DesktopNav: React.FC<{
|
|
|
82
80
|
onMouseLeave={handleMouseLeave}
|
|
83
81
|
onBlur={handleMouseLeave}
|
|
84
82
|
>
|
|
85
|
-
<Link href={el.href}
|
|
83
|
+
<Link href={el.href}>
|
|
86
84
|
{el.title}
|
|
87
85
|
</Link>
|
|
88
86
|
</NavigationMenuTrigger>
|
|
@@ -112,7 +110,7 @@ const DesktopNav: React.FC<{
|
|
|
112
110
|
>
|
|
113
111
|
{
|
|
114
112
|
el.href && el.href !== '' ?
|
|
115
|
-
<Link href={el.href}
|
|
113
|
+
<Link href={el.href}>
|
|
116
114
|
{el.title}
|
|
117
115
|
</Link> : <>{el.title}</>
|
|
118
116
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { type PropsWithChildren } from 'react'
|
|
2
2
|
|
|
3
3
|
import { cn } from '@hanzo/ui/util'
|
|
4
|
-
import
|
|
4
|
+
import AuthWidget from '../auth/widget'
|
|
5
5
|
|
|
6
6
|
import Logo, { type LogoVariant } from '../logo'
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ const DesktopHeader: React.FC<{
|
|
|
25
25
|
noAuth=false,
|
|
26
26
|
noCommerce=false,
|
|
27
27
|
children,
|
|
28
|
-
logoVariant='
|
|
28
|
+
logoVariant='full'
|
|
29
29
|
}) => {
|
|
30
30
|
const [isMenuOpened, setIsMenuOpen] = React.useState(false);
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ const DesktopHeader: React.FC<{
|
|
|
42
42
|
'flex flex-row h-[80px] items-center justify-between ' +
|
|
43
43
|
'mx-[24px] w-full max-w-screen'
|
|
44
44
|
}>
|
|
45
|
-
<Logo size={logoVariant === '
|
|
45
|
+
<Logo size={logoVariant === 'mark' ? 'lg' : 'md'} href='/' outerClx='hidden lg:flex' key='two' variant={logoVariant} />
|
|
46
46
|
<Logo size='sm' href='/' outerClx='hidden md:flex lg:hidden' key='one' variant={logoVariant} />
|
|
47
47
|
{/* md or larger */}
|
|
48
48
|
<div className='flex w-full gap-4 items-center justify-center'>
|
|
@@ -52,7 +52,7 @@ const DesktopHeader: React.FC<{
|
|
|
52
52
|
{!noCommerce && (
|
|
53
53
|
<DesktopBagPopup popupClx='w-[340px]' trigger={<BagButton className='text-primary -mr-[3px] lg:min-w-0' />} />
|
|
54
54
|
)}
|
|
55
|
-
<AuthWidget
|
|
55
|
+
{!noAuth && <AuthWidget />}
|
|
56
56
|
{children}
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
@@ -15,7 +15,7 @@ const MobileNavMenuAI: React.FC<MobileNavMenuAIProps> = ({ setMenuOpen }) => {
|
|
|
15
15
|
<>
|
|
16
16
|
<div className="w-full text-2xl cursor-pointer">
|
|
17
17
|
<div className='flex justify-between'>
|
|
18
|
-
<Logo variant='
|
|
18
|
+
<Logo variant='wordmark' size='md' outerClx={'p-6 h-full'} />
|
|
19
19
|
<Plus width={28} height={28} className={
|
|
20
20
|
'block h-full aspect-square hover:bg-background sm:hover:bg-level-1 active:scale-75 text-foreground will-change-transform transition-transform transition-scale transition-duration-[1500] mt-6 mr-6 ' +
|
|
21
21
|
(!open ? 'rotate-none' : 'rotate-[135deg] scale-110')
|
|
@@ -5,7 +5,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
5
5
|
import type { LinkDefExtended, ChildMenu } from '../../site-def/main-nav'
|
|
6
6
|
import MobileNavMenuAI from './mobile-nav-menu-ai'
|
|
7
7
|
import MobileNavMenuItem from './mobile-nav-menu-item'
|
|
8
|
-
import
|
|
8
|
+
import AuthWidget from '../auth/widget'
|
|
9
9
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@hanzo/ui/primitives'
|
|
10
10
|
import { ChevronDown } from 'lucide-react'
|
|
11
11
|
import Link from 'next/link'
|
|
@@ -94,7 +94,7 @@ const MobileNav: React.FC<{
|
|
|
94
94
|
})}
|
|
95
95
|
</Accordion>
|
|
96
96
|
</div>
|
|
97
|
-
<
|
|
97
|
+
<AuthWidget className='text-2xl z-10' handleLogin={() => { setMenuState('login') }} />
|
|
98
98
|
</div>
|
|
99
99
|
) : null
|
|
100
100
|
);
|
|
@@ -6,7 +6,7 @@ import type { LinkDef } from '@hanzo/ui/types'
|
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
|
|
8
8
|
import { CartPanel, useCommerce } from '@hanzo/commerce'
|
|
9
|
-
import
|
|
9
|
+
import Login from '../auth/login'
|
|
10
10
|
import sendGAEvent from '../../next/analytics/google-analytics'
|
|
11
11
|
|
|
12
12
|
import { Avatar, Bag } from '../icons'
|
|
@@ -95,7 +95,7 @@ const MobileHeader: React.FC<{
|
|
|
95
95
|
)}>
|
|
96
96
|
{/* smaller than md: mobile style drawer menu; h-11 is 44px, the standard mobile header height */}
|
|
97
97
|
<div className='w-full h-full flex flex-row justify-between items-center font-bold pr-5'>
|
|
98
|
-
<Logo href='/' size='md' outerClx={'p-6 h-full'} variant='
|
|
98
|
+
<Logo href='/' size='md' outerClx={'p-6 h-full'} variant='wordmark' />
|
|
99
99
|
{/* Not that key to the cross-fade effect
|
|
100
100
|
is that this is **on top of** the logo. */}
|
|
101
101
|
{menuOpen() && (
|
|
@@ -129,7 +129,7 @@ const MobileHeader: React.FC<{
|
|
|
129
129
|
'flex flex-column bg-background z-below-header animate-mobile-menu-open'
|
|
130
130
|
}>
|
|
131
131
|
{(!!!noAuth && menuState === 'login') ? (
|
|
132
|
-
<
|
|
132
|
+
<Login onSuccess={onLoginChanged} className='sm:animate-in sm:zoom-in-90 m-auto w-full max-w-sm px-8' />
|
|
133
133
|
) : (
|
|
134
134
|
(!!!noCommerce && menuState === 'bag') ? (
|
|
135
135
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as Avatar } from './avatar'
|
|
2
2
|
export { default as Bag } from './bag-icon'
|
|
3
3
|
export { default as LeftArrow } from './left-arrow'
|
|
4
|
-
export { default as LuxLogo } from './lux-logo'
|
|
5
4
|
export { default as RightArrow } from './right-arrow'
|
|
6
5
|
export { default as SocialIcon, type SocialIconProps } from './social-icon'
|