Skip to content

Commit 541f275

Browse files
authored
💄 style: Add login ui for next-auth (lobehub#6434)
1 parent 3fb966c commit 541f275

File tree

6 files changed

+183
-8
lines changed

6 files changed

+183
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
'use client';
2+
3+
import { LobeChat } from '@lobehub/ui/brand';
4+
import { Button, Col, Flex, Row, Skeleton, Typography } from 'antd';
5+
import { createStyles } from 'antd-style';
6+
import { AuthError } from 'next-auth';
7+
import { signIn } from 'next-auth/react';
8+
import { useRouter, useSearchParams } from 'next/navigation';
9+
import { memo } from 'react';
10+
import { useTranslation } from 'react-i18next';
11+
12+
import BrandWatermark from '@/components/BrandWatermark';
13+
import AuthIcons from '@/components/NextAuth/AuthIcons';
14+
import { DOCUMENTS_REFER_URL, PRIVACY_URL, TERMS_URL } from '@/const/url';
15+
import { useUserStore } from '@/store/user';
16+
17+
const { Title, Paragraph } = Typography;
18+
19+
const useStyles = createStyles(({ css, token }) => ({
20+
button: css`
21+
text-transform: capitalize;
22+
`,
23+
container: css`
24+
min-width: 360px;
25+
border: 1px solid ${token.colorBorder};
26+
border-radius: ${token.borderRadiusLG}px;
27+
background: ${token.colorBgContainer};
28+
`,
29+
contentCard: css`
30+
padding-block: 2.5rem;
31+
padding-inline: 2rem;
32+
`,
33+
description: css`
34+
margin: 0;
35+
color: ${token.colorTextSecondary};
36+
`,
37+
footer: css`
38+
padding: 1rem;
39+
border-block-start: 1px solid ${token.colorBorder};
40+
border-radius: 0 0 8px 8px;
41+
42+
color: ${token.colorTextDescription};
43+
44+
background: ${token.colorBgElevated};
45+
`,
46+
text: css`
47+
text-align: center;
48+
`,
49+
title: css`
50+
margin: 0;
51+
color: ${token.colorTextHeading};
52+
`,
53+
}));
54+
55+
const BtnListLoading = memo(() => {
56+
return (
57+
<Flex gap={'small'} vertical>
58+
<Skeleton.Button active style={{ minWidth: 300 }} />
59+
<Skeleton.Button active style={{ minWidth: 300 }} />
60+
<Skeleton.Button active style={{ minWidth: 300 }} />
61+
</Flex>
62+
);
63+
});
64+
65+
/**
66+
* Follow the implementation from AuthJS official documentation,
67+
* but using client components.
68+
* ref: https://authjs.dev/guides/pages/signin
69+
*/
70+
export default memo(() => {
71+
const { styles } = useStyles();
72+
const { t } = useTranslation('clerk');
73+
const router = useRouter();
74+
75+
const oAuthSSOProviders = useUserStore((s) => s.oAuthSSOProviders);
76+
77+
const searchParams = useSearchParams();
78+
79+
// Redirect back to the page url
80+
const callbackUrl = searchParams.get('callbackUrl') ?? '';
81+
82+
const handleSignIn = async (provider: string) => {
83+
try {
84+
await signIn(provider, { redirectTo: callbackUrl });
85+
} catch (error) {
86+
// Signin can fail for a number of reasons, such as the user
87+
// not existing, or the user not having the correct role.
88+
// In some cases, you may want to redirect to a custom error
89+
if (error instanceof AuthError) {
90+
return router.push(`/next-auth/?error=${error.type}`);
91+
}
92+
93+
// Otherwise if a redirects happens Next.js can handle it
94+
// so you can just re-thrown the error and let Next.js handle it.
95+
// Docs: https://nextjs.org/docs/app/api-reference/functions/redirect#server-component
96+
throw error;
97+
}
98+
};
99+
100+
const footerBtns = [
101+
{ href: DOCUMENTS_REFER_URL, id: 0, label: t('footerPageLink__help') },
102+
{ href: PRIVACY_URL, id: 1, label: t('footerPageLink__privacy') },
103+
{ href: TERMS_URL, id: 2, label: t('footerPageLink__terms') },
104+
];
105+
106+
return (
107+
<div className={styles.container}>
108+
<div className={styles.contentCard}>
109+
{/* Card Body */}
110+
<Flex gap="large" vertical>
111+
{/* Header */}
112+
<div className={styles.text}>
113+
<Title className={styles.title} level={4}>
114+
<div>
115+
<LobeChat size={48} />
116+
</div>
117+
{t('signIn.start.title', { applicationName: 'LobeChat' })}
118+
</Title>
119+
<Paragraph className={styles.description}>{t('signIn.start.subtitle')}</Paragraph>
120+
</div>
121+
{/* Content */}
122+
<Flex gap="small" vertical>
123+
{oAuthSSOProviders ? (
124+
oAuthSSOProviders.map((provider) => (
125+
<Button
126+
className={styles.button}
127+
icon={AuthIcons(provider, 16)}
128+
key={provider}
129+
onClick={() => handleSignIn(provider)}
130+
>
131+
{provider}
132+
</Button>
133+
))
134+
) : (
135+
<BtnListLoading />
136+
)}
137+
</Flex>
138+
</Flex>
139+
</div>
140+
<div className={styles.footer}>
141+
{/* Footer */}
142+
<Row>
143+
<Col span={12}>
144+
<Flex justify="left" style={{ height: '100%' }}>
145+
<BrandWatermark />
146+
</Flex>
147+
</Col>
148+
<Col offset={4} span={8}>
149+
<Flex justify="right">
150+
{footerBtns.map((btn) => (
151+
<Button key={btn.id} onClick={() => router.push(btn.href)} size="small" type="text">
152+
{btn.label}
153+
</Button>
154+
))}
155+
</Flex>
156+
</Col>
157+
</Row>
158+
</div>
159+
</div>
160+
);
161+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Suspense } from 'react';
2+
3+
import Loading from '@/components/Loading/BrandTextLoading';
4+
5+
import AuthSignInBox from './AuthSignInBox';
6+
7+
export default () => (
8+
<Suspense fallback={<Loading />}>
9+
<AuthSignInBox />
10+
</Suspense>
11+
);

src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { userService } from '@/services/user';
1010
import { useUserStore } from '@/store/user';
1111
import { userProfileSelectors } from '@/store/user/selectors';
1212

13-
import AuthIcons from './AuthIcons';
13+
import AuthIcons from '@/components/NextAuth/AuthIcons';
1414

1515
const { Item } = List;
1616

+8-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ import {
1212
} from '@lobehub/ui/icons';
1313
import React from 'react';
1414

15-
const iconProps = {
16-
size: 32,
17-
};
18-
1915
const iconComponents: { [key: string]: React.ElementType } = {
2016
'auth0': Auth0,
2117
'authelia': Authelia.Color,
@@ -29,9 +25,15 @@ const iconComponents: { [key: string]: React.ElementType } = {
2925
'zitadel': Zitadel.Color,
3026
};
3127

32-
const AuthIcons = (id: string) => {
28+
/**
29+
* Get the auth icons component for the given id
30+
* @param id
31+
* @param size default is 36
32+
* @returns
33+
*/
34+
const AuthIcons = (id: string, size = 36) => {
3335
const IconComponent = iconComponents[id] || iconComponents.default;
34-
return <IconComponent {...iconProps} />;
36+
return <IconComponent size={size}/>;
3537
};
3638

3739
export default AuthIcons;

src/libs/next-auth/auth.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default {
4242
debug: authEnv.NEXT_AUTH_DEBUG,
4343
pages: {
4444
error: '/next-auth/error',
45+
signIn: '/next-auth/signin',
4546
},
4647
providers: initSSOProviders(),
4748
secret: authEnv.NEXT_AUTH_SECRET,

src/middleware.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const config = {
3636

3737
'/login(.*)',
3838
'/signup(.*)',
39-
'/next-auth/error',
39+
'/next-auth/(.*)',
4040
// ↓ cloud ↓
4141
],
4242
};

0 commit comments

Comments
 (0)