@luxfi/ui 5.5.4 → 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.
@@ -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 { LuxLogo } from '../icons'
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' textClx='!cursor-pointer' variant='text-only' />
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 className='w-5 h-5' />
64
+ <LuxLogo variant='white' size={20} />
94
65
  </Button>
95
- <Login
96
- getStartedUrl={getStartedUrl}
97
- redirectUrl={redirectUrl}
98
- className='w-full max-w-sm'
99
- termsOfServiceUrl={termsOfServiceUrl}
100
- privacyPolicyUrl={privacyPolicyUrl}
101
- onLoginChanged={onLogin}
102
- setIsLogin={setIsLogin}
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 './icons'
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 width={24} height={24}/>
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
- <LuxLogo
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
- strokeWidth={1}
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 { AuthWidget } from '@hanzo/auth/components'
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='text-only' outerClx='logo-outer-tooltip-class' />
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 { AuthWidget } from '@hanzo/auth/components'
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='text-only' href='/' onClick={onLeave} outerClx='-ml-2'/>
34
+ <Logo size='xs' variant='wordmark' href='/' onClick={onLeave} outerClx='-ml-2'/>
35
35
  </div>
36
36
  <StepIndicator
37
37
  dotSizeRem={1}
@@ -34,7 +34,7 @@ const Footer: React.FC<{
34
34
  'md:flex md:flex-row md:justify-between px-[24px]'
35
35
  }>
36
36
  <div className='hidden lg:flex flex-col' key={0}>
37
- <Logo size='md' variant='text-only' />
37
+ <Logo size='md' variant='wordmark' />
38
38
  </div>
39
39
  {footer.map((defs: LinkDef[], index: number) => {
40
40
 
@@ -1,7 +1,7 @@
1
1
  import React, { type PropsWithChildren } from 'react'
2
2
 
3
3
  import { cn } from '@hanzo/ui/util'
4
- import { AuthWidget } from '@hanzo/auth/components'
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='text-only'
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 === 'logo-only' ? 'lg' : 'md'} href='/' outerClx='hidden lg:flex' key='two' variant={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 noLogin={noAuth}/>
55
+ {!noAuth && <AuthWidget />}
56
56
  {children}
57
57
  </div>
58
58
  </div>
@@ -18,7 +18,7 @@ const Header: React.FC<{
18
18
  siteDef,
19
19
  className = '',
20
20
  children,
21
- logoVariant='text-only'
21
+ logoVariant='full'
22
22
  }) => {
23
23
 
24
24
  // TODO
@@ -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='text-only' size='md' outerClx={'p-6 h-full'} />
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 MobileAuthWidget from '../auth/mobile-login-button'
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
- <MobileAuthWidget className='text-2xl z-10' handleLogin={() => { setMenuState('login') }} />
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 { LoginPanel } from '@hanzo/auth/components'
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='text-only' />
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
- <LoginPanel noHeading onLoginChanged={onLoginChanged} className='sm:animate-in sm:zoom-in-90' />
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'
@@ -10,7 +10,8 @@ export { default as Main } from './main'
10
10
  export { default as MiniChart } from './mini-chart'
11
11
  export { default as NotFound } from './not-found'
12
12
 
13
- export { default as AuthListener } from './auth/auth-listener'
13
+ export { default as Auth } from './auth/provider'
14
+ export { default as Login } from './auth/login'
14
15
  export { default as BackButton } from './back-button'
15
16
  export { default as BuyDrawer } from './commerce/drawer'
16
17
  export { default as DrawerMargin } from './drawer-margin'
@@ -18,7 +19,6 @@ export { default as BuyButton } from './commerce/buy-button'
18
19
  export { default as CheckoutButton } from './commerce/checkout-button'
19
20
  export { default as CheckoutPanel } from './commerce/checkout-panel'
20
21
  export { default as LoginPanel } from './auth/login-panel'
21
- export { default as SignupPanel } from './auth/signup-panel'
22
22
  export { default as Analytics } from './analytics'
23
23
  export { default as Tooltip } from './tooltip'
24
24
 
@@ -1,13 +1,30 @@
1
1
  import React from 'react'
2
2
  import Link from 'next/link'
3
3
 
4
- import { type TShirtSize } from '@hanzo/ui/types'
4
+ import { type TShirtSize } from '@hanzo/ui/types'
5
+ import { Logo as Mark, Wordmark } from '@luxfi/logo'
5
6
 
6
- import { LuxLogo } from './icons'
7
-
8
- const TEXT = 'LUX'
9
- type LogoVariant = 'text-only' | 'logo-only' | 'full'
7
+ /**
8
+ * mark — the triangle alone
9
+ * wordmark — the LUX letterforms alone
10
+ * full — the mark beside the wordmark
11
+ *
12
+ * Both halves come from @luxfi/logo, which is the one home for Lux brand art.
13
+ * This file used to draw its own triangle and set the letters "LUX" in the body
14
+ * font, so every Lux site wore typed text where the wordmark belongs — and no
15
+ * variant could fix it, because neither the mark nor the wordmark was here to
16
+ * ask for. Art lives in the brand package; this composes it.
17
+ */
18
+ type LogoVariant = 'mark' | 'wordmark' | 'full'
10
19
 
20
+ /** Mark height in px, and the wordmark height that stands with it. */
21
+ const SIZE: Record<string, { mark: number, word: number, gap: string }> = {
22
+ xl: { mark: 40, word: 22, gap: 'gap-4' },
23
+ lg: { mark: 40, word: 22, gap: 'gap-4' },
24
+ md: { mark: 32, word: 18, gap: 'gap-3' },
25
+ sm: { mark: 24, word: 13, gap: 'gap-2' },
26
+ xs: { mark: 16, word: 9, gap: 'gap-1.5' },
27
+ }
11
28
 
12
29
  const Logo: React.FC<{
13
30
  size?: TShirtSize
@@ -15,74 +32,29 @@ const Logo: React.FC<{
15
32
  onClick?: () => void
16
33
  href?: string
17
34
  outerClx?: string
18
- textClx?: string
19
35
  }> = ({
20
- size,
36
+ size = 'md',
21
37
  href, // no default please!
22
- outerClx='',
23
- textClx='',
24
- variant='full',
38
+ outerClx = '',
39
+ variant = 'full',
25
40
  onClick,
26
41
  }) => {
27
- let classes: any = {}
28
- const toAdd = (variant === 'logo-only') ?
29
- {
30
- span: 'hidden',
31
- icon: ''
32
- }
33
- :
34
- (variant === 'text-only') ?
35
- {
36
- span: '',
37
- icon: 'hidden'
38
- }
39
- :
40
- {
41
- span: '',
42
- icon: ''
43
- }
44
-
45
- if (size === 'lg' || size === 'xl' ) { // for safety
46
- classes.icon = 'h-10 w-10 mr-4 color-inherit '
47
- classes.span = 'text-3xl '
48
- }
49
- // match lux.network
50
- else if (size === 'md') {
51
- classes.icon = 'h-[32px] w-[32px] mr-[12px] color-inherit '
52
- classes.span = 'text-[1.8rem]/[1.8rem] tracking-tighter '
53
- }
54
- else if (size === 'sm' ) {
55
- classes.icon = 'h-6 w-6 mr-2 color-inherit '
56
- classes.span = 'text-lg '
57
- }
58
- // xs
59
- else {
60
- classes.icon = 'h-4 w-4 mr-1 color-inherit '
61
- classes.span = 'text-base '
62
- }
63
-
64
- classes.icon += toAdd.icon
65
- classes.span += toAdd.span
66
-
42
+ const s = SIZE[size as string] ?? SIZE.md
43
+ const clx = 'flex flex-row items-center ' + s.gap + ' '
44
+ + (href ? 'hover:opacity-80 cursor-pointer ' : 'cursor-default ')
45
+ + outerClx
67
46
 
68
- const outerClasses = 'flex flex-row items-center ' + outerClx
69
- const spanClasses = 'inline-block font-bold font-heading '
70
- + textClx
71
- + (href ? ' hover:opacity-80 cursor-pointer ' : ' cursor-default ')
72
- + classes.span
47
+ // White on both counts: the Lux ground is black and the brand's accent is
48
+ // white, so the mark and the letters carry the same value.
49
+ const art = (<>
50
+ {variant !== 'wordmark' && <Mark variant='white' size={s.mark} />}
51
+ {variant !== 'mark' && <Wordmark height={s.word} />}
52
+ </>)
73
53
 
74
- return (
75
- href ? (
76
- <Link href={href} className={outerClasses} onClick={onClick} >
77
- <LuxLogo className={classes.icon} />
78
- <span className={spanClasses}>{TEXT}</span>
79
- </Link>
80
- ) : (
81
- <span className={outerClasses} onClick={onClick}>
82
- <LuxLogo className={classes.icon} />
83
- <span className={spanClasses}>{TEXT}</span>
84
- </span>
85
- )
54
+ return href ? (
55
+ <Link href={href} className={clx} onClick={onClick}>{art}</Link>
56
+ ) : (
57
+ <span className={clx} onClick={onClick}>{art}</span>
86
58
  )
87
59
  }
88
60
 
@@ -0,0 +1,55 @@
1
+ import React from 'react'
2
+
3
+ import { ApplyTypography } from '@hanzo/ui/primitives'
4
+
5
+ import type { SiteDef } from '../../site-def'
6
+ import Footer from '../footer'
7
+ import Header from '../header'
8
+ import Main from '../main'
9
+
10
+ const Security: React.FC<{
11
+ contact: string
12
+ header?: boolean
13
+ siteDef: SiteDef
14
+ }> = ({
15
+ contact,
16
+ header = false,
17
+ siteDef
18
+ }) => (<>
19
+ {header && <Header siteDef={siteDef} />}
20
+ <Main className='px-8 sm:px-10 pb-16'>
21
+ <ApplyTypography className='w-full max-w-[46rem] mx-auto flex flex-col gap-6 pt-12'>
22
+ <h1>Reporting a security problem</h1>
23
+ <p>
24
+ Email <a href={`mailto:${contact}`}>{contact}</a>. Tell us what you found and how to
25
+ reproduce it. Send it to us before you tell anyone else, and do not open a public issue.
26
+ </p>
27
+ <p>
28
+ A real report from a stranger is worth more than another internal review. We read every one.
29
+ </p>
30
+ <h2>What happens next</h2>
31
+ <p>
32
+ Someone will read your report and reply. If we can reproduce the problem we will tell you
33
+ what we are doing about it, and tell you again once it is fixed. If we cannot reproduce it
34
+ we will say so and ask you for what we are missing.
35
+ </p>
36
+ <h2>While you are looking</h2>
37
+ <p>
38
+ Test against accounts and data that are your own. Do not read, change, or keep data that
39
+ belongs to someone else. If you reach it by accident, stop, and say so in your report.
40
+ </p>
41
+ <p>
42
+ Do not run anything that degrades the service for other people. No load testing, no denial
43
+ of service, no mass automated scanning, and no social engineering of our staff or our users.
44
+ </p>
45
+ <h2>For scanners</h2>
46
+ <p>
47
+ This page is the policy named by{' '}
48
+ <a href='/.well-known/security.txt'>/.well-known/security.txt</a>, per RFC 9116.
49
+ </p>
50
+ </ApplyTypography>
51
+ </Main>
52
+ <Footer siteDef={siteDef} />
53
+ </>)
54
+
55
+ export default Security
@@ -1,6 +1,5 @@
1
1
  import { NextRequest, NextResponse, userAgent } from 'next/server'
2
2
  import { getSelectorsByUserAgent } from 'react-device-detect'
3
- import { setCookie } from 'cookies-next'
4
3
 
5
4
  // writed this way so they can be chained :)
6
5
  const determineDeviceMW = async (request: NextRequest) => {
@@ -10,18 +9,6 @@ const determineDeviceMW = async (request: NextRequest) => {
10
9
  const agent = isMobileOnly ? 'phone' : (isTablet ? 'tablet' : (isDesktop ? 'desktop' : 'unknown'))
11
10
  const { nextUrl: url } = request
12
11
  //console.log(`\n=== from ${url.href} on a *${agent && agent.toUpperCase()}* device. ===\n`)
13
- const auth_token = url.searchParams.get('auth-token')
14
- if (auth_token) {
15
- setCookie('auth-token', auth_token, {
16
- domain: url.hostname,
17
- path: '/',
18
- sameSite: 'none',
19
- secure: true,
20
- httpOnly: false,
21
- expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
22
- })
23
- url.searchParams.delete('auth-token')
24
- }
25
12
  url.searchParams.set('agent', agent)
26
13
  return NextResponse.rewrite(url)
27
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxfi/ui",
3
- "version": "5.5.4",
3
+ "version": "5.5.5",
4
4
  "description": "Library that contains shared UI primitives, support for a common design system, and other boilerplate support.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -20,6 +20,8 @@
20
20
  ],
21
21
  "exports": {
22
22
  ".": "./components/index.ts",
23
+ "./auth-callback": "./components/auth/callback.tsx",
24
+ "./security": "./components/security/index.tsx",
23
25
  "./commerce": "./commerce/ui/context.tsx",
24
26
  "./root-layout": "./root-layout/index.tsx",
25
27
  "./next": "./next/index.ts",
@@ -29,11 +31,10 @@
29
31
  "./util": "./util/index.ts"
30
32
  },
31
33
  "dependencies": {
34
+ "@luxfi/logo": "^1.0.10",
32
35
  "@next/third-parties": "^16.1.0",
33
- "cookies-next": "^4.1.1",
34
36
  "date-fns": "^3.6.0",
35
37
  "embla-carousel-autoplay": "^8.1.6",
36
- "firebase": "10.12.0",
37
38
  "framer-motion": "^11.2.12",
38
39
  "markdown-to-jsx": "^7.4.7",
39
40
  "react-device-detect": "^2.2.3",
@@ -43,9 +44,10 @@
43
44
  "usehooks-ts": "^3.1.0"
44
45
  },
45
46
  "peerDependencies": {
46
- "@hanzo/auth": "2.5.8",
47
- "@hanzo/commerce": "7.3.10",
47
+ "@hanzo/iam": "0.21.9",
48
+ "@hanzo/commerce": "7.6.4",
48
49
  "@hanzo/ui": "5.3.34",
50
+ "@luxfi/menu-icons": "^1.0.2",
49
51
  "@hookform/resolvers": "^3.3.2",
50
52
  "lucide-react": "0.470.0",
51
53
  "mobx": "^6.12.3",
@@ -56,8 +58,7 @@
56
58
  "react-dom": "19.2.0",
57
59
  "react-hook-form": "7.54.2",
58
60
  "validator": "^13.11.0",
59
- "zod": "3.23.8",
60
- "@luxfi/menu-icons": "1.0.2"
61
+ "zod": "3.23.8"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@hookform/resolvers": "^3.3.2",
@@ -2,15 +2,13 @@ import React, { type PropsWithChildren } from 'react'
2
2
  import type { Viewport } from 'next'
3
3
 
4
4
  import { Toaster } from '@hanzo/ui/primitives'
5
- import { AuthServiceProvider } from '@hanzo/auth/service'
6
- import type { AuthServiceConf } from '@hanzo/auth/types'
7
5
  import { CommerceProvider } from '@hanzo/commerce'
8
6
 
9
7
  import getAppRouterBodyFontClasses from '../next/font/get-app-router-font-classes'
10
8
  import { FacebookPixelHead } from '../next/analytics/pixel-analytics'
11
9
 
12
10
  import { CommerceUIProvider } from '../commerce/ui/context'
13
- import { AuthListener, ChatWidget, Header, Analytics } from '../components'
11
+ import { Auth, ChatWidget, Header, Analytics } from '../components'
14
12
 
15
13
  import CommerceDrawer from '../components/commerce/drawer'
16
14
 
@@ -57,10 +55,6 @@ function RootLayout({
57
55
  chatbot?: boolean
58
56
  } & PropsWithChildren) {
59
57
 
60
- // For static export, we don't have server-side auth
61
- // User auth will be handled client-side via AuthListener
62
- const currentUser = null
63
-
64
58
  const Guts: React.FC = () => (<>
65
59
  {showHeader && <Header siteDef={siteDef}/>}
66
60
  {children}
@@ -87,7 +81,7 @@ function RootLayout({
87
81
  display: 'none', // see analytics.tsx
88
82
  }}>
89
83
  <Analytics/>
90
- <AuthServiceProvider user={currentUser} conf={{} as AuthServiceConf}>
84
+ <Auth app={siteDef.app}>
91
85
  {siteDef?.commerce ? (
92
86
  <CommerceProvider config={siteDef.commerce!} >
93
87
  <CommerceUIProvider >
@@ -98,8 +92,7 @@ function RootLayout({
98
92
  ) : (
99
93
  <Guts />
100
94
  )}
101
- <AuthListener/>
102
- </AuthServiceProvider>
95
+ </Auth>
103
96
  <Toaster position='top-center' duration={3000}/>
104
97
  </body>
105
98
  </html>
@@ -18,6 +18,55 @@ div.nextjs-toast-errors-parent[data-nextjs-toast="true"] {
18
18
  @apply uppercase;
19
19
  }
20
20
 
21
+ /* Hanzo IAM's sign-in views ship structure, not skin. This is the one place
22
+ * that markup meets the Lux surface -- colours come from lux-colors so the
23
+ * form follows the theme like everything else.
24
+ */
25
+ .hanzo-iam-card,
26
+ .hanzo-iam-form {
27
+ @apply flex flex-col gap-3 w-full;
28
+ }
29
+
30
+ .hanzo-iam-form input {
31
+ @apply w-full rounded-sm px-3 py-2 text-base;
32
+ background-color: var(--hz-ui-bg-1);
33
+ color: var(--hz-ui-fg-body);
34
+ border: 1px solid var(--hz-ui-bg-2);
35
+ }
36
+
37
+ .hanzo-iam-form input::placeholder {
38
+ color: var(--hz-ui-fg-3);
39
+ }
40
+
41
+ .hanzo-iam-form input:focus {
42
+ outline: none;
43
+ border-color: var(--hz-ui-fg-body);
44
+ }
45
+
46
+ .hanzo-iam-btn {
47
+ @apply w-full rounded-sm px-3 py-2 text-base font-semibold cursor-pointer;
48
+ background-color: var(--hz-ui-bg-inverted);
49
+ color: var(--hz-ui-fg-inverted);
50
+ }
51
+
52
+ .hanzo-iam-btn:hover {
53
+ background-color: var(--hz-ui-bg-inverted-hover);
54
+ }
55
+
56
+ .hanzo-iam-btn:disabled {
57
+ @apply opacity-50 cursor-default;
58
+ }
59
+
60
+ .hanzo-iam-divider {
61
+ @apply text-sm uppercase;
62
+ color: var(--hz-ui-fg-3);
63
+ }
64
+
65
+ .hanzo-iam-error {
66
+ @apply text-sm;
67
+ color: hsl(0 70% 60%);
68
+ }
69
+
21
70
  /* Specific style fixes for react-square-web-payments-sdk -
22
71
  * reduce gap between card input and pay button
23
72
  */
package/types/site-def.ts CHANGED
@@ -7,6 +7,13 @@ import type ChatbotConfig from './chatbot-config'
7
7
 
8
8
  interface SiteDef {
9
9
 
10
+ /**
11
+ * Short name of this site, which with the org names the IAM application it
12
+ * signs in as: 'quest' is the client `lux-quest` on lux.id. Required, so a
13
+ * site cannot ship a login button that belongs to no one.
14
+ */
15
+ app: string
16
+
10
17
  /** url of this site. All nav links in the system will show it in 'current' state */
11
18
  currentAs?: string
12
19
 
@@ -1,33 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect } from 'react'
4
- import { useAuth } from '@hanzo/auth/service'
5
- import { getCookie } from 'cookies-next'
6
-
7
- const AuthListener = () => {
8
- const auth = useAuth()
9
-
10
- useEffect(() => {
11
- // Sites that do not share a login origin leave this unset; without it there
12
- // is nothing to ask, so asking would only fetch the string "undefined".
13
- const loginSite = process.env.NEXT_PUBLIC_LOGIN_SITE_URL
14
- if (!loginSite) return
15
-
16
- fetch(`${loginSite}/api/auth/get-auth-token`, {
17
- method: 'GET',
18
- credentials: 'include',
19
- })
20
- .then(response => response.json())
21
- .then((data: any) => {
22
- const token = data.reqToken
23
- if (!!token) {
24
- auth.loginWithCustomToken(token)
25
- }
26
- })
27
- .catch(() => { /* no shared session available */ })
28
- }, [auth])
29
-
30
- return ( <></> )
31
- }
32
-
33
- export default AuthListener
@@ -1,12 +0,0 @@
1
- import domains from '../common-auth-domains'
2
-
3
- const ClearAuthToken = () => {
4
- return (<>
5
- {domains.map(({url}) => (
6
- /* Clear auth-token cookie across all Lux domains */
7
- <img src={`${url}/api/auth/clear-auth-token`} className='absolute hidden'/>
8
- ))}
9
- </>)
10
- }
11
-
12
- export default ClearAuthToken
@@ -1,16 +0,0 @@
1
- import domains from '../common-auth-domains'
2
-
3
- const SetAuthToken: React.FC<{
4
- authToken: string
5
- }> = ({
6
- authToken,
7
- }) => {
8
- return (<>
9
- {!!authToken && domains.map(({url}) => (
10
- /* Set auth-token cookie across all Lux domains */
11
- <img src={`${url}/api/auth/set-auth-token?token=${authToken}`} className='absolute hidden'/>
12
- ))}
13
- </>)
14
- }
15
-
16
- export default SetAuthToken
@@ -1,17 +0,0 @@
1
- const domains = [
2
- { id: 'lux.market', url: 'https://lux.market' },
3
- { id: 'lux.shop', url: 'https://lux.shop' },
4
- { id: 'lux.credit', url: 'https://lux.credit' },
5
- { id: 'lux.network', url: 'https://lux.network' },
6
- { id: 'wallet.lux.network', url: 'https://wallet.lux.network' },
7
- { id: 'safe.lux.network', url: 'https://safe.lux.network' },
8
- { id: 'lux.finance', url: 'https://lux.finance' },
9
- { id: 'lux.exchange', url: 'https://lux.exchange' },
10
- { id: 'lux.quest', url: 'https://lux.quest' },
11
- { id: 'lux.id', url: 'https://lux.id' },
12
- { id: 'lux.chat', url: 'https://lux.chat' }
13
- ]
14
-
15
- export {
16
- domains as default
17
- }
@@ -1,112 +0,0 @@
1
- 'use client'
2
- import React from "react"
3
- import { observer } from "mobx-react-lite"
4
-
5
- import {
6
- Button,
7
- LinkElement,
8
- Popover,
9
- PopoverContent,
10
- PopoverTrigger,
11
- Separator
12
- } from '@hanzo/ui/primitives'
13
-
14
- import type { LinkDef } from '@hanzo/ui/types'
15
- import { cn } from '@hanzo/ui/util'
16
-
17
- import { useAuth } from "@hanzo/auth/service"
18
-
19
- import { Ethereum } from "@hanzo/auth/icons"
20
-
21
- const MobileAuthWidget: React.FC<{
22
- noLogin?: boolean
23
- className?: string
24
- handleLogin?: () => void
25
- }> = observer(({
26
- noLogin = false,
27
- className
28
- }) => {
29
- const auth = useAuth()
30
- const handleLogin = () => {
31
- window.location.href = "https://lux.id";
32
- };
33
-
34
- if (!auth) {
35
- return null
36
- }
37
- if (!auth.loggedIn && typeof window !== 'undefined') {
38
- return (noLogin ? null : (
39
- (handleLogin) ? (
40
- <div className="flex items-center py-1 px-1 gap-1">
41
- <Button
42
- variant='primary'
43
- className='text-base font-semibold !min-w-0 self-center flex-1'
44
- onClick={handleLogin}
45
- >
46
- Sign Up
47
- </Button>
48
- <Button
49
- variant='outline'
50
- className=' text-base font-semibold !min-w-0 self-center flex-1'
51
- onClick={handleLogin}
52
- >
53
- Log In
54
- </Button>
55
- </div>
56
- ) : (
57
- // Without a login origin there is nowhere to send them, and the
58
- // link would read `undefined?redirectUrl=…` — which next/link
59
- // prefetches, so every page fetches a 404 it can never use.
60
- process.env.NEXT_PUBLIC_LOGIN_SITE_URL ? (
61
- <LinkElement
62
- def={{
63
- href: `${process.env.NEXT_PUBLIC_LOGIN_SITE_URL}?redirectUrl=${window.location.href}`,
64
- title: 'Login',
65
- variant: 'primary',
66
- newTab: false
67
- } satisfies LinkDef}
68
- className='h-8 w-fit !min-w-0'
69
- />
70
- ) : null
71
- )
72
- ))
73
- }
74
-
75
- return (
76
- <Popover>
77
- <PopoverTrigger asChild>
78
- <Button
79
- variant="outline"
80
- size='icon'
81
- 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)}
82
- >{auth.user?.email[0]}</Button>
83
- </PopoverTrigger>
84
- <PopoverContent className='bg-level-0'>
85
- <div className="grid gap-4">
86
- <div className="space-y-2 truncate">
87
- {auth.user?.displayName ? (
88
- <>
89
- <h4 className="font-medium leading-none truncate">{auth.user.displayName}</h4>
90
- <p className="text-sm opacity-50 truncate">{auth.user.email}</p>
91
- </>
92
- ) : (
93
- <h4 className="font-medium leading-none truncate">{auth.user?.email}</h4>
94
- )}
95
- {auth.user?.walletAddress ? (
96
- <p className="text-sm opacity-50 truncate">{auth.user.walletAddress}</p>
97
- ) : (
98
- <Button variant="outline" className='w-full flex items-center gap-2' onClick={auth.associateWallet.bind(auth)}>
99
- <Ethereum height={20} />Connect your wallet
100
- </Button>
101
- )}
102
- </div>
103
- <Separator />
104
- <Button variant="outline" onClick={auth.logout.bind(auth)}>Logout</Button>
105
- </div>
106
- </PopoverContent>
107
- </Popover>
108
- )
109
-
110
- })
111
-
112
- export default MobileAuthWidget
@@ -1,113 +0,0 @@
1
- 'use client'
2
-
3
- import Link from 'next/link'
4
- import { useRouter } from 'next/navigation'
5
-
6
- import { setCookie } from 'cookies-next'
7
-
8
- import { cn } from '@hanzo/ui/util'
9
- import { Button, Carousel, CarouselContent, CarouselItem } from '@hanzo/ui/primitives'
10
- import { LoginPanel as Login, SignupPanel as Signup } from '@hanzo/auth/components'
11
-
12
- import { LuxLogo } from '../icons'
13
- import Logo from '../logo'
14
- import { EmblaAutoplay } from '..'
15
- import { legal } from '../../site-def/footer'
16
-
17
- const SignupPanel: React.FC<{
18
- close: () => void
19
- getStartedUrl?: string
20
- redirectUrl?: string
21
- className?: string
22
- reviews: { text: string, author: string, href: string }[]
23
- setIsLogin?: React.Dispatch<React.SetStateAction<boolean>>
24
- }> = ({
25
- close,
26
- getStartedUrl = '/',
27
- redirectUrl,
28
- className = '',
29
- reviews,
30
- setIsLogin
31
- }) => {
32
- const router = useRouter()
33
-
34
- const termsOfServiceUrl = legal.find(({ title }) => title === 'Terms and Conditions')?.href || ''
35
- const privacyPolicyUrl = legal.find(({ title }) => title === 'Privacy Policy')?.href || ''
36
- const domains = ['lux.id', 'lux.credit', 'lux.market', 'lux.network', 'lux.shop', 'wallet.lux.network', 'safe.lux.network', 'lux.finance', 'lux.exchange', 'lux.quest']
37
-
38
- // TODO :aa shouldn't this happen in @hanzo/auth: components/LoginPanel ??
39
- // Otherwise, the functionality is split across modules! (and client/fw!)
40
- // (This was never my intent w the onLoginChanged callback.)
41
- const onLogin = (token: string) => {
42
- for (let i = 0; i < domains.length; i++) {
43
- setCookie('auth-token', token, {
44
- domain: domains[i],
45
- path: '/',
46
- sameSite: 'none',
47
- secure: true,
48
- httpOnly: false,
49
- expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
50
- })
51
- }
52
-
53
- redirectUrl && router.push(redirectUrl)
54
- }
55
-
56
- return (
57
- <div className={cn('grid grid-cols-1 md:grid-cols-2', className)}>
58
- <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'>
59
- <div className='h-full w-full max-w-[750px] px-8 pt-0'>
60
- <div className='h-full w-full max-w-[550px] mx-auto flex flex-col justify-between min-h-screen py-10'>
61
- <Button
62
- variant='ghost'
63
- onClick={close}
64
- className='w-fit !min-w-0 p-2'
65
- >
66
- <Logo size='md' textClx='!cursor-pointer' variant='text-only' />
67
- </Button>
68
- <Carousel
69
- options={{ align: 'center', loop: true }}
70
- className='w-full'
71
- plugins={[EmblaAutoplay({ delay: 5000, stopOnInteraction: true })]}
72
- >
73
- <CarouselContent>
74
- {reviews.map(({ text, author, href }, index) => (
75
- <CarouselItem key={index}>
76
- <Link href={href} className='flex flex-col gap-3 cursor-pointer'>
77
- <p>“{text}“</p>
78
- <p className='text-sm'>{author}</p>
79
- </Link>
80
- </CarouselItem>
81
- ))}
82
- </CarouselContent>
83
- </Carousel>
84
- </div>
85
- </div>
86
- </div>
87
- <div className='w-full h-full bg-background flex flex-row items-center'>
88
- <div className='w-full max-w-[750px] relative flex flex-col items-center px-8 pt-0 text-center'>
89
- <div className='relative h-full w-full max-w-[400px] mx-auto flex flex-col gap-4 items-center py-10'>
90
- <Button
91
- variant='ghost'
92
- onClick={close}
93
- className='block md:hidden absolute rounded-full p-2 left-0 h-auto hover:bg-background'
94
- >
95
- <LuxLogo className='w-5 h-5' />
96
- </Button>
97
- <Signup
98
- getStartedUrl={getStartedUrl}
99
- redirectUrl={redirectUrl}
100
- className='w-full max-w-sm'
101
- termsOfServiceUrl={termsOfServiceUrl}
102
- privacyPolicyUrl={privacyPolicyUrl}
103
- onLoginChanged={onLogin}
104
- setIsLogin={setIsLogin}
105
- />
106
- </div>
107
- </div>
108
- </div>
109
- </div>
110
- )
111
- }
112
-
113
- export default SignupPanel
@@ -1,10 +0,0 @@
1
- import React from 'react'
2
- import { type LucideProps } from 'lucide-react'
3
-
4
- const LuxLogo: React.FC<LucideProps> = (props: LucideProps) => (
5
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" {...props}>
6
- <polygon points="25,46.65 50,3.35 0,3.35" fill="white" stroke='black' strokeWidth={props.strokeWidth}/>
7
- </svg>
8
- )
9
-
10
- export default LuxLogo