LoginGen 文档

介绍

LoginGen 是一个登录页模板网站,之所以开发这个项目,是因为市面上虽然有很多模板网站,但大都缺乏对登录页的支持,或者只是简单一两个模板而已,并且大都不具有完整的前端逻辑。虽然可能在交互权重上,登录页并不像 Landing Page 那样的重要,但一个漂亮的登录页面也是会让用户加深对网站的第一印象。

技术栈主要基于 Next.jsShadcn/ui 开发,除了提供模板设计以外还提供完整的前端逻辑,并使用 Next.js 提供的 Server Actions 能力进行统一的服务端认证处理 (社交登录除外,为了方便使用放在了客户端事件中)

所有代码都是开放的,就像 Shadcn 那样直接粘贴复制就可以使用,所以你可以根据你项目的需求随意修改代码,并且这也是推荐的。当然为了方便考虑,表单组件也提取了一些常用的属性,可以让使用者开箱即用。

后续会不断的增加、完善、优化模板,请大家拭目以待。

安装

模板基于 Next.js 与 Shadcn 开发,所以首先你需要具有该项目环境:

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']社交登录的类型(其中布局会根据选择的数量以及表单的宽度自动布局)
email初始化邮箱,也可用于从 otp 验证表单中,编辑邮箱跳转后的预填充显示
emailStrategy'magic-link'发送邮件的类型,如果是 'magic-link' 则不跳转页面,触发一个 sonner 弹窗,否则应该跳转到 otp 验证表单
socialHandler点击社交登录按钮时的回调函数,你可以使用如 Auth.js 库来实现
forgetHandler'#'当点击 '忘记密码' 按钮时的回调,使用该参数主要是为了方便使用
serverAction除了社交登录以外的所有表单,统一通过 Server Actions 提交到服务端
serverStateSuccessHandler一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现

说明:

  1. socials -- 每个模板都有一个 config/social.tsx 配置文件,你可以编辑此文件以扩展你所需要的社交登录方式。你可以任意修改 key 以适配你所使用的认证框架,也可以任意修改 icon svg 为你喜欢的。
  2. email -- 在 verify-otp 表单 UI 中,有一个可以修改邮箱的按钮,当用户点击按钮之后,应该回退到上一个页面以让用户修改为正确的邮箱。该 email 属性就是为这个回退后预设邮箱显示而设计的。

注册

接口定义如下:

export interface SignUpFormProps {
  email?: string
  serverAction: (currentState: SignUpActionState, formData: FormData) => Promise<SignUpActionState>
  serverStateSuccessHandler?: (email: string) => void | Promise<void>
}
参数默认值解释
email初始化邮箱,也可用于从 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>
}
参数默认值解释
email初始化邮箱,也可用于从 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>
}
参数默认值解释
email该邮箱只用于测试密码强度(建议设置,以提高用户输入密码的强度)
resetToken服务端生成的用于追踪表单流程的 token
serverAction通过 Server Actions 将表单数据提交到服务端
serverStateSuccessHandler一般情况下,当服务端认证完成后直接在服务端重定向即可,但如果你想要在客户端实现一些重定向逻辑,则可在该回调参数中实现

resetToken 在重置密码流程中,一般服务端会生成一个 token 用于安全的追踪与确认当前表单的提交者以及状态。

服务端一般会用两种方式传递 token,一种是放在 URL 的查询参数中,一种是放在 cookie 中,当表单提交时浏览器会自动携带当前的 cookie

如果是前者,你可以使用 searchParamsuseSearchParams() 来获取查询参数中的 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>
}
参数默认值解释
email初始化邮箱,并且该邮箱会显示在 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

如果是前者,你可以使用 searchParamsuseSearchParams() 来获取查询参数中的 verifyToken传递给表单,表单会在提交的时候自动将该 verifyToken 传递给服务端的 Server Action。

如果是后者,你可以忽略该参数。

服务端

除了社交登录以外,其他所有的登录选项在客户端验证完成后,统一提交到 Server Actions 中进行服务端的逻辑处理。其使用 React 的 useFormStateuseFormStatus 进行表单提交操作。

模板代码中的 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 }
}