LoginGen 文档
介绍
LoginGen 是一个登录页模板网站,之所以开发这个项目,是因为市面上虽然有很多模板网站,但大都缺乏对登录页的支持,或者只是简单一两个模板而已,并且大都不具有完整的前端逻辑。虽然可能在交互权重上,登录页并不像 Landing Page 那样的重要,但一个漂亮的登录页面也是会让用户加深对网站的第一印象。
技术栈主要基于 Next.js 与 Shadcn/ui 开发,除了提供模板设计以外还提供完整的前端逻辑,并使用 Next.js 提供的 Server Actions 能力进行统一的服务端认证处理 (社交登录除外,为了方便使用放在了客户端事件中)。
所有代码都是开放的,就像 Shadcn 那样直接粘贴复制就可以使用,所以你可以根据你项目的需求随意修改代码,并且这也是推荐的。当然为了方便考虑,表单组件也提取了一些常用的属性,可以让使用者开箱即用。
后续会不断的增加、完善、优化模板,请大家拭目以待。
安装
模板基于 Next.js 与 Shadcn 开发,所以首先你需要具有该项目环境:
- "next": "^14.2+"
- "tailwindcss": "^3.4+"
- "react-hook-form: 7.52.2" - 暂时不要使用更高的版本,更高的版本在渲染时有 bug (issues: https://github.com/react-hook-form/react-hook-form/issues/12518)
shadcn 组件
如果使用其他包命令工具或想要手动安装,请参考 shadcn 的文档。
pnpm dlx shadcn@latest add card tabs form button input input-otp separator sonner
添加以下 css 动画到你的 tailwind.config.js
文件中
// for input-otp
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
keyframes: {
'caret-blink': {
'0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' }
}
},
animation: {
'caret-blink': 'caret-blink 1.25s ease-out infinite'
}
}
}
}
对于 sonner 你需要添加以下组件
import { Toaster } from '@/components/ui/sonner'
export default function RootLayout({ children }) {
return (
<html lang='en'>
<head />
<body>
<main>{children}</main>
<Toaster />
</body>
</html>
)
}
三方依赖库
pnpm add react-hook-form@7.52.2 zod motion @zxcvbn-ts/core @zxcvbn-ts/language-common @zxcvbn-ts/language-en embla-carousel-react embla-carousel-autoplay embla-carousel-fade lucide-react
pnpm add -D embla-carousel
react-hook-form
+zod
-- 表单客户端校验motion
-- 动画库@zxcvbn-*
-- 密码强度检测库embla-carousel-*
-- 轮播图库(其中embla-carousel
需要安装到devDependencies
以正确获取 TypeScript 类型)lucide-react
-- Icon 库
复制粘贴代码
每个模板的预览页面都有完整的代码展示,你只需将需要的代码复制粘贴到你项目的对应位置即可。模板包含的图片都可以直接下载原图,包含的视频都以 CDN 的方式提供。
目前暂不支持类似 npx
的一键安装功能,后期可能会考虑。
主题
每个模板中都有一个 styles/theme.module.css
主题文件,该文件包含所有的主题变量以及该模板所需的样式、动画等,你可以修改该文件中的样式来修改主题或符合你网站的 UI 审美。
暗色模式
暗色模式请按照 shadcn 的文档 进行配置,在每个模板的
theme.module.css
主题文件中暗色主题会使用 :global(.dark)
css 选择器前缀,以响应网站全局的暗色模式。
I18n(TODO)
目前暂未支持多语言配置,但你依然可以手动添加,虽然可能会稍微麻烦一点,后期会考虑加上该配置功能。
表单属性
一共有五个表单:登录、注册、忘记密码、重置密码、OTP(one-time password) 验证,每个表单都具有一些参数以便于开箱即用,大部分参数都是通用的。
虽然在模板代码中,默认每个表单页面都是一个单独的路由,然后通过服务端重定向来进行表单页面路由之间的导航。但是所有的组件都设计为传递属性的方式使用,也就意味着你完全可以使用客户端路由。
登录
接口定义如下:
const _auths = ['social', 'email', 'password'] as const
type Auth = (typeof _auths)[number]
export interface SignInFormProps {
auths?: Readonly<Auth[]>
socials?: SocialKey[]
email?: string
emailStrategy?: 'magic-link' | 'one-time-pwd'
socialHandler?: (event: MouseEvent<HTMLButtonElement>, key: SocialKey) => void | Promise<void>
forgetHandler?: string | ((event: React.MouseEvent<HTMLAnchorElement>) => void | Promise<void>) // for more convenient use
serverAction?: (
formType: 'email' | 'password',
currentState: SignInActionState,
formData: FormData
) => Promise<SignInActionState>
serverStateSuccessHandler?: (formType: 'email' | 'password', email: string) => void | Promise<void>
}
参数 | 默认值 | 解释 |
---|---|---|
auths | ['social', 'password', 'email'] | 认证方式:社交登录、邮箱登录、密码登录 |
socials | ['google', 'github'] | 社交登录的类型(其中布局会根据选择的数量以及表单的宽度自动布局) |
初始化邮箱,也可用于从 otp 验证表单中,编辑邮箱跳转后的预填充显示 | ||
emailStrategy | 'magic-link' | 发送邮件的类型,如果是 'magic-link' 则不跳转页面,触发一个 sonner 弹窗,否则应该跳转到 otp 验证表单 |
socialHandler | 点击社交登录按钮时的回调函数,你可以使用如 Auth.js 库来实现 | |
forgetHandler | '#' | 当点击 '忘记密码' 按钮时的回调,使用该参数主要是为了方便使用 |
serverAction | 除了社交登录以外的所有表单,统一通过 Server Actions 提交到服务端 | |
serverStateSuccessHandler | 一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现 |
说明:
socials
-- 每个模板都有一个config/social.tsx
配置文件,你可以编辑此文件以扩展你所需要的社交登录方式。你可以任意修改key
以适配你所使用的认证框架,也可以任意修改 icon svg 为你喜欢的。email
-- 在verify-otp
表单 UI 中,有一个可以修改邮箱的按钮,当用户点击按钮之后,应该回退到上一个页面以让用户修改为正确的邮箱。该email
属性就是为这个回退后预设邮箱显示而设计的。
注册
接口定义如下:
export interface SignUpFormProps {
email?: string
serverAction: (currentState: SignUpActionState, formData: FormData) => Promise<SignUpActionState>
serverStateSuccessHandler?: (email: string) => void | Promise<void>
}
参数 | 默认值 | 解释 |
---|---|---|
初始化邮箱,也可用于从 otp 验证表单中,编辑邮箱跳转后的预填充显示 | ||
serverAction | 通过 Server Actions 将表单数据提交到服务端 | |
serverStateSuccessHandler | 一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现 |
忘记密码
接口定义如下:
export interface ForgetPasswordProps {
email?: string
emailStrategy?: 'magic-link' | 'one-time-pwd'
serverAction: (
currentState: ForgetPasswordActionState,
formData: FormData
) => Promise<ForgetPasswordActionState>
serverStateSuccessHandler?: (email: string) => void | Promise<void>
}
参数 | 默认值 | 解释 |
---|---|---|
初始化邮箱,也可用于从 otp 验证表单中,编辑邮箱跳转后的预填充显示 | ||
emailStrategy | 'magic-link' | 发送邮件的类型,如果是 'magic-link' 则不跳转页面,触发一个 sonner 弹窗,否则应该跳转到 otp 验证表单 |
serverAction | 通过 Server Actions 将表单数据提交到服务端 | |
serverStateSuccessHandler | 一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现 |
重置密码
接口定义如下:
export interface ResetPasswordFormProps {
email?: string // Only used to test password strength
resetToken?: string
serverAction: (
resetToken: string,
currentState: ResetPasswordActionState,
formData: FormData
) => Promise<ResetPasswordActionState>
serverStateSuccessHandler?: () => void | Promise<void>
}
参数 | 默认值 | 解释 |
---|---|---|
该邮箱只用于测试密码强度(建议设置,以提高用户输入密码的强度) | ||
resetToken | 服务端生成的用于追踪表单流程的 token | |
serverAction | 通过 Server Actions 将表单数据提交到服务端 | |
serverStateSuccessHandler | 一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现 |
resetToken 在重置密码流程中,一般服务端会生成一个
token
用于安全的追踪与确认当前表单的提交者以及状态。服务端一般会用两种方式传递
token
,一种是放在 URL 的查询参数中,一种是放在cookie
中,当表单提交时浏览器会自动携带当前的cookie
。如果是前者,你可以使用
searchParams
或useSearchParams()
来获取查询参数中的resetToken
传递给表单,表单会在提交的时候自动将该resetToken
传递给服务端的 Server Action。如果是后者,你可以忽略该参数。
OTP 验证
接口定义如下:
export interface VerifyOTPFormProps {
email: string
verifyToken?: string
editEmailHandler?: (event: MouseEvent<HTMLButtonElement>) => void
serverAction: (
email: string,
verifyToken: string,
currentState: VerifyOTPActionState,
formData: FormData
) => Promise<VerifyOTPActionState>
codeResendAction: (email: string, verifyToken: string) => Promise<VerifyOTPActionState>
serverStateSuccessHandler?: () => void | Promise<void>
}
参数 | 默认值 | 解释 |
---|---|---|
初始化邮箱,并且该邮箱会显示在 UI 上,可方便用户进行编辑 | ||
verifyToken | 服务端生成的用于追踪表单流程的 token | |
editEmailHandler | 当点击编辑邮箱按钮时的事件函数 | |
codeResendAction | 重新发送 code 的事件函数(秒数可以通过 verify-otp-form.tsx 中的 code_resend_max_seconds 常量进行设置) | |
serverAction | 通过 Server Actions 将表单数据提交到服务端 | |
serverStateSuccessHandler | 一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现 |
verifyToken 在验证 one-time password 流程中,一般服务端会生成一个
token
用于安全的追踪与确认当前表单的提交者以及状态。服务端一般会用两种方式传递
token
,一种是放在 URL 的查询参数中,一种是放在cookie
中,当表单提交时浏览器会自动携带当前的cookie
。如果是前者,你可以使用
searchParams
或useSearchParams()
来获取查询参数中的verifyToken
传递给表单,表单会在提交的时候自动将该verifyToken
传递给服务端的 Server Action。如果是后者,你可以忽略该参数。
服务端
除了社交登录以外,其他所有的登录选项在客户端验证完成后,统一提交到 Server
Actions 中进行服务端的逻辑处理。其使用 React 的 useFormState
与 useFormStatus
进行表单提交操作。
模板代码中的 types/action-state.ts
中,简单定义了提交到 Server Action 中的数据格式,你可以根据需要自行扩展。
以下是在 server action 中操作的示例代码:
sign-in-action.ts
'use server'
import type { FormType, SignInActionState } from '@/types/server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function signInAction(
formType: FormType,
currentState: SignInActionState,
formData: FormData
): Promise<SignInActionState> {
switch (formType) {
case 'email':
return await emailAction(formData)
case 'password':
return await passwordAction(formData)
}
}
async function emailAction(formData: FormData): Promise<SignInActionState> {
const data = Object.fromEntries(formData)
const parsed = signInSchemas.email.safeParse(data)
// Form validation is also required on the server side
if (!parsed.success) {
const issues: Record<string, string> = {}
parsed.error.issues.map((issue) => (issues[issue.path[0]] = issue.message))
return {
success: false,
formIssues: issues
}
}
// Perform some server-side operations, such as sending emails, operating databases, etc.
// Redirection can be performed on the server side
revalidatePath('/auth/sign-in')
redirect(`/auth/verify-otp`)
// Or return it directly to the client
// return { success: true }
}
async function passwordAction(formData: FormData): Promise<SignInActionState> {
const data = Object.fromEntries(formData)
// mock delay
await new Promise((resolve) => setTimeout(resolve, 2000))
console.log('Sign in password server action form data : ' + JSON.stringify(data, null, 2))
return { success: true }
}