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