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 }
}