@cogito.ai/cli 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -22
- package/dist/index.js +9 -15
- package/dist/templates/web-nextjs/.github/copilot-instructions.md +5 -6
- package/dist/templates/web-nextjs/README.md +25 -24
- package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +18 -8
- package/dist/templates/web-nextjs/apps/docs/.source/dynamic.ts +11 -5
- package/dist/templates/web-nextjs/apps/docs/.source/server.ts +37 -17
- package/dist/templates/web-nextjs/apps/docs/app/docs/[[...slug]]/page.tsx +1 -6
- package/dist/templates/web-nextjs/apps/docs/app/docs/layout.tsx +1 -4
- package/dist/templates/web-nextjs/apps/docs/app/llms-full.txt/route.ts +3 -1
- package/dist/templates/web-nextjs/apps/docs/next-env.d.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/.github/copilot-instructions.md +53 -6
- package/dist/templates/web-nextjs/apps/web/.github/skills/impeccable/SKILL.md +55 -0
- package/dist/templates/web-nextjs/apps/web/DESIGN.md +65 -0
- package/dist/templates/web-nextjs/apps/web/messages/en.json +81 -5
- package/dist/templates/web-nextjs/apps/web/messages/zh.json +81 -5
- package/dist/templates/web-nextjs/apps/web/next.config.ts +3 -3
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +174 -39
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +13 -3
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +4 -1
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +18 -17
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/profile/page.tsx +2 -8
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/about/page.tsx +3 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +17 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/help/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +10 -8
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/page.tsx +22 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/privacy/page.tsx +3 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/terms/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +2 -3
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/app-sidebar.tsx +13 -16
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/chart-area-interactive.tsx +122 -146
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/data-table.tsx +84 -149
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-documents.tsx +7 -16
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-main.tsx +4 -4
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-secondary.tsx +4 -4
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-user.tsx +12 -21
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/page.tsx +10 -13
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/section-cards.tsx +5 -9
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/site-header.tsx +6 -7
- package/dist/templates/web-nextjs/apps/web/src/components/landing/features.tsx +63 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/footer.tsx +48 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/header.tsx +97 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/hero.tsx +45 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/how-it-works.tsx +35 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/pricing-teaser.tsx +23 -0
- package/dist/templates/web-nextjs/apps/web/src/components/profile/profile-form.tsx +6 -4
- package/dist/templates/web-nextjs/apps/web/src/components/providers/theme-provider.tsx +16 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert-dialog.tsx +32 -49
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +16 -23
- package/dist/templates/web-nextjs/apps/web/src/components/ui/avatar.tsx +25 -38
- package/dist/templates/web-nextjs/apps/web/src/components/ui/badge.tsx +16 -18
- package/dist/templates/web-nextjs/apps/web/src/components/ui/breadcrumb.tsx +19 -26
- package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +23 -24
- package/dist/templates/web-nextjs/apps/web/src/components/ui/card.tsx +19 -36
- package/dist/templates/web-nextjs/apps/web/src/components/ui/chart.tsx +60 -94
- package/dist/templates/web-nextjs/apps/web/src/components/ui/checkbox.tsx +8 -11
- package/dist/templates/web-nextjs/apps/web/src/components/ui/collapsible.tsx +5 -17
- package/dist/templates/web-nextjs/apps/web/src/components/ui/command.tsx +25 -48
- package/dist/templates/web-nextjs/apps/web/src/components/ui/dialog.tsx +21 -35
- package/dist/templates/web-nextjs/apps/web/src/components/ui/drawer.tsx +24 -35
- package/dist/templates/web-nextjs/apps/web/src/components/ui/dropdown-menu.tsx +26 -55
- package/dist/templates/web-nextjs/apps/web/src/components/ui/field.tsx +62 -76
- package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +19 -34
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input-otp.tsx +13 -20
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +6 -6
- package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +6 -6
- package/dist/templates/web-nextjs/apps/web/src/components/ui/pagination.tsx +21 -42
- package/dist/templates/web-nextjs/apps/web/src/components/ui/popover.tsx +16 -31
- package/dist/templates/web-nextjs/apps/web/src/components/ui/progress.tsx +5 -8
- package/dist/templates/web-nextjs/apps/web/src/components/ui/radio-group.tsx +8 -8
- package/dist/templates/web-nextjs/apps/web/src/components/ui/scroll-area.tsx +10 -12
- package/dist/templates/web-nextjs/apps/web/src/components/ui/select.tsx +26 -41
- package/dist/templates/web-nextjs/apps/web/src/components/ui/separator.tsx +7 -7
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sheet.tsx +29 -38
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sidebar.tsx +157 -189
- package/dist/templates/web-nextjs/apps/web/src/components/ui/skeleton.tsx +3 -3
- package/dist/templates/web-nextjs/apps/web/src/components/ui/slider.tsx +10 -15
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +13 -7
- package/dist/templates/web-nextjs/apps/web/src/components/ui/switch.tsx +9 -9
- package/dist/templates/web-nextjs/apps/web/src/components/ui/table.tsx +24 -48
- package/dist/templates/web-nextjs/apps/web/src/components/ui/tabs.tsx +21 -31
- package/dist/templates/web-nextjs/apps/web/src/components/ui/textarea.tsx +5 -5
- package/dist/templates/web-nextjs/apps/web/src/components/ui/theme-toggle.tsx +23 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle-group.tsx +15 -16
- package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle.tsx +14 -15
- package/dist/templates/web-nextjs/apps/web/src/components/ui/tooltip.tsx +8 -12
- package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +2 -0
- package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +1 -3
- package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +57 -1
- package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +2 -0
- package/dist/templates/web-nextjs/apps/web/src/hooks/use-mobile.ts +4 -4
- package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +48 -4
- package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +1 -4
- package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +2 -2
- package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +13 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/tokens.css +58 -0
- package/package.json +1 -1
|
@@ -3,6 +3,73 @@
|
|
|
3
3
|
"title": "Welcome to AgentDock Web Template",
|
|
4
4
|
"description": "A Next.js scaffold with four-layer architecture."
|
|
5
5
|
},
|
|
6
|
+
"landing": {
|
|
7
|
+
"nav": {
|
|
8
|
+
"features": "Features",
|
|
9
|
+
"pricing": "Pricing",
|
|
10
|
+
"docs": "Docs",
|
|
11
|
+
"login": "Log in",
|
|
12
|
+
"getStarted": "Get Started"
|
|
13
|
+
},
|
|
14
|
+
"hero": {
|
|
15
|
+
"badge": "Open Source",
|
|
16
|
+
"title": "Build SaaS Faster with AgentDock",
|
|
17
|
+
"subtitle": "A production-ready Next.js scaffold with four-layer architecture, Supabase auth, i18n, and AI coding agent integration. Ship your next product in days, not months.",
|
|
18
|
+
"ctaPrimary": "Get Started Free",
|
|
19
|
+
"ctaSecondary": "View Docs"
|
|
20
|
+
},
|
|
21
|
+
"features": {
|
|
22
|
+
"title": "Everything you need to ship",
|
|
23
|
+
"subtitle": "Built-in features that let you focus on your product, not the boilerplate.",
|
|
24
|
+
"items": {
|
|
25
|
+
"auth": {
|
|
26
|
+
"title": "Authentication",
|
|
27
|
+
"description": "Complete auth flow with email/password, OAuth, and password reset. Secured by Supabase."
|
|
28
|
+
},
|
|
29
|
+
"monorepo": {
|
|
30
|
+
"title": "Monorepo Ready",
|
|
31
|
+
"description": "Turborepo + pnpm workspace with shared packages for types, ESLint, and tooling."
|
|
32
|
+
},
|
|
33
|
+
"aiCoding": {
|
|
34
|
+
"title": "AI Coding Ready",
|
|
35
|
+
"description": "Structured for AI agents with clear contracts, design system, and copilot instructions."
|
|
36
|
+
},
|
|
37
|
+
"docs": {
|
|
38
|
+
"title": "Docs Co-evolution",
|
|
39
|
+
"description": "Fumadocs-powered documentation that grows alongside your application."
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"howItWorks": {
|
|
44
|
+
"title": "How it works",
|
|
45
|
+
"subtitle": "Get up and running in three simple steps.",
|
|
46
|
+
"steps": {
|
|
47
|
+
"step1": {
|
|
48
|
+
"title": "Scaffold",
|
|
49
|
+
"description": "Run the CLI to generate your project with all batteries included."
|
|
50
|
+
},
|
|
51
|
+
"step2": {
|
|
52
|
+
"title": "Customize",
|
|
53
|
+
"description": "Adjust tokens, colors, and content to match your brand."
|
|
54
|
+
},
|
|
55
|
+
"step3": {
|
|
56
|
+
"title": "Ship",
|
|
57
|
+
"description": "Deploy to Vercel with one command and start building."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"pricing": {
|
|
62
|
+
"title": "Pricing",
|
|
63
|
+
"description": "Pricing plans coming soon. Contact us for early access.",
|
|
64
|
+
"cta": "Contact Us"
|
|
65
|
+
},
|
|
66
|
+
"footer": {
|
|
67
|
+
"help": "Help",
|
|
68
|
+
"privacy": "Privacy",
|
|
69
|
+
"about": "About",
|
|
70
|
+
"copyright": "© {year} AgentDock. All rights reserved."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
6
73
|
"auth": {
|
|
7
74
|
"loginTitle": "Welcome back",
|
|
8
75
|
"loginSubtitle": "Sign in to your account",
|
|
@@ -36,14 +103,23 @@
|
|
|
36
103
|
"privacyLink": "Privacy Policy",
|
|
37
104
|
"forgotPasswordLink": "Forgot password?",
|
|
38
105
|
"forgotPasswordTitle": "Reset your password",
|
|
39
|
-
"forgotPasswordSubtitle": "Enter your email and we'll send you a
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
106
|
+
"forgotPasswordSubtitle": "Enter your email and we'll send you a verification code.",
|
|
107
|
+
"sendVerificationCode": "Send verification code",
|
|
108
|
+
"verificationCodeLabel": "Email verification code",
|
|
109
|
+
"verificationCodePlaceholder": "Enter 6-digit code",
|
|
110
|
+
"resendIn": "Resend in {seconds}s",
|
|
111
|
+
"resendCode": "Resend code",
|
|
43
112
|
"resetPasswordTitle": "Set new password",
|
|
44
113
|
"resetPasswordSubtitle": "Enter your new password below.",
|
|
45
114
|
"newPasswordLabel": "New password",
|
|
46
|
-
"
|
|
115
|
+
"newPasswordPlaceholder": "••••••••",
|
|
116
|
+
"confirmPasswordLabel": "Confirm new password",
|
|
117
|
+
"confirmPasswordPlaceholder": "••••••••",
|
|
118
|
+
"resetPassword": "Reset password",
|
|
119
|
+
"changeEmail": "Change email",
|
|
120
|
+
"forgotPasswordSent": "A reset link has been sent to {email}. Please check your inbox.",
|
|
121
|
+
"sendResetLink": "Send reset link",
|
|
122
|
+
"backToLogin": "Back to login",
|
|
47
123
|
"resetLinkExpired": "The reset link has expired. Please request a new one.",
|
|
48
124
|
"resetPasswordSuccess": "Password updated. Please log in."
|
|
49
125
|
},
|
|
@@ -3,6 +3,73 @@
|
|
|
3
3
|
"title": "欢迎使用 AgentDock Web 模板",
|
|
4
4
|
"description": "基于四层架构的 Next.js 脚手架。"
|
|
5
5
|
},
|
|
6
|
+
"landing": {
|
|
7
|
+
"nav": {
|
|
8
|
+
"features": "功能",
|
|
9
|
+
"pricing": "定价",
|
|
10
|
+
"docs": "文档",
|
|
11
|
+
"login": "登录",
|
|
12
|
+
"getStarted": "开始使用"
|
|
13
|
+
},
|
|
14
|
+
"hero": {
|
|
15
|
+
"badge": "开源",
|
|
16
|
+
"title": "用 AgentDock 更快地构建 SaaS",
|
|
17
|
+
"subtitle": "一个生产就绪的 Next.js 脚手架,包含四层架构、Supabase 认证、国际化和 AI 编码代理集成。",
|
|
18
|
+
"ctaPrimary": "免费开始",
|
|
19
|
+
"ctaSecondary": "查看文档"
|
|
20
|
+
},
|
|
21
|
+
"features": {
|
|
22
|
+
"title": "开箱即用的功能",
|
|
23
|
+
"subtitle": "内置功能让你专注于产品,而非样板代码。",
|
|
24
|
+
"items": {
|
|
25
|
+
"auth": {
|
|
26
|
+
"title": "认证系统",
|
|
27
|
+
"description": "完整的认证流程,支持邮箱/密码、OAuth 和密码重置。由 Supabase 保障安全。"
|
|
28
|
+
},
|
|
29
|
+
"monorepo": {
|
|
30
|
+
"title": "Monorepo 就绪",
|
|
31
|
+
"description": "Turborepo + pnpm 工作区,包含共享的类型、ESLint 和工具包。"
|
|
32
|
+
},
|
|
33
|
+
"aiCoding": {
|
|
34
|
+
"title": "AI 编码就绪",
|
|
35
|
+
"description": "为 AI 代理优化的结构,包含清晰的契约、设计系统和 Copilot 指令。"
|
|
36
|
+
},
|
|
37
|
+
"docs": {
|
|
38
|
+
"title": "文档协同进化",
|
|
39
|
+
"description": "基于 Fumadocs 的文档系统,与应用共同成长。"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"howItWorks": {
|
|
44
|
+
"title": "工作原理",
|
|
45
|
+
"subtitle": "三个简单步骤即可启动运行。",
|
|
46
|
+
"steps": {
|
|
47
|
+
"step1": {
|
|
48
|
+
"title": "脚手架",
|
|
49
|
+
"description": "运行 CLI 生成包含所有功能的初始项目。"
|
|
50
|
+
},
|
|
51
|
+
"step2": {
|
|
52
|
+
"title": "自定义",
|
|
53
|
+
"description": "调整 token、颜色和内容以匹配你的品牌。"
|
|
54
|
+
},
|
|
55
|
+
"step3": {
|
|
56
|
+
"title": "发布",
|
|
57
|
+
"description": "一条命令部署到 Vercel,开始构建。"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"pricing": {
|
|
62
|
+
"title": "定价",
|
|
63
|
+
"description": "定价方案即将推出,欢迎联系我们获取早期访问。",
|
|
64
|
+
"cta": "联系我们"
|
|
65
|
+
},
|
|
66
|
+
"footer": {
|
|
67
|
+
"help": "帮助",
|
|
68
|
+
"privacy": "隐私",
|
|
69
|
+
"about": "关于",
|
|
70
|
+
"copyright": "© {year} AgentDock。保留所有权利。"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
6
73
|
"auth": {
|
|
7
74
|
"loginTitle": "欢迎回来",
|
|
8
75
|
"loginSubtitle": "登录您的账号",
|
|
@@ -36,14 +103,23 @@
|
|
|
36
103
|
"privacyLink": "隐私政策",
|
|
37
104
|
"forgotPasswordLink": "忘记密码?",
|
|
38
105
|
"forgotPasswordTitle": "重置密码",
|
|
39
|
-
"forgotPasswordSubtitle": "
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
106
|
+
"forgotPasswordSubtitle": "输入您的邮箱,我们将发送验证码。",
|
|
107
|
+
"sendVerificationCode": "发送验证码",
|
|
108
|
+
"verificationCodeLabel": "邮箱验证码",
|
|
109
|
+
"verificationCodePlaceholder": "输入 6 位验证码",
|
|
110
|
+
"resendIn": "{seconds} 秒后重新发送",
|
|
111
|
+
"resendCode": "重新发送验证码",
|
|
43
112
|
"resetPasswordTitle": "设置新密码",
|
|
44
113
|
"resetPasswordSubtitle": "在下方输入您的新密码。",
|
|
45
114
|
"newPasswordLabel": "新密码",
|
|
46
|
-
"
|
|
115
|
+
"newPasswordPlaceholder": "••••••••",
|
|
116
|
+
"confirmPasswordLabel": "确认新密码",
|
|
117
|
+
"confirmPasswordPlaceholder": "••••••••",
|
|
118
|
+
"resetPassword": "立即重置",
|
|
119
|
+
"changeEmail": "更换邮箱",
|
|
120
|
+
"forgotPasswordSent": "重置链接已发送至 {email},请查收邮件。",
|
|
121
|
+
"sendResetLink": "发送重置链接",
|
|
122
|
+
"backToLogin": "返回登录",
|
|
47
123
|
"resetLinkExpired": "重置链接已过期,请重新申请。",
|
|
48
124
|
"resetPasswordSuccess": "密码已更新,请登录。"
|
|
49
125
|
},
|
|
@@ -5,9 +5,9 @@ import path from 'node:path'
|
|
|
5
5
|
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
|
|
6
6
|
|
|
7
7
|
const nextConfig: NextConfig = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
turbopack: {
|
|
9
|
+
root: path.resolve(process.cwd(), '../..'),
|
|
10
|
+
},
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export default withNextIntl(nextConfig)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useActionState, useEffect, useState } from 'react'
|
|
3
|
+
import { useActionState, useEffect, useState, startTransition } from 'react'
|
|
4
4
|
import { useParams } from 'next/navigation'
|
|
5
5
|
import Link from 'next/link'
|
|
6
6
|
import { useTranslations } from 'next-intl'
|
|
@@ -12,42 +12,99 @@ import { Button } from '@/components/ui/button'
|
|
|
12
12
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
13
|
import { Input } from '@/components/ui/input'
|
|
14
14
|
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { sendPasswordResetOTP, resetPasswordWithOTP } from '@/features/auth'
|
|
16
|
+
import { resetPasswordWithOTPSchema, type ResetPasswordWithOTPInput } from '@/lib/validations/auth'
|
|
17
17
|
import type { ActionResult } from '@/core/types/auth'
|
|
18
18
|
|
|
19
19
|
export default function ForgotPasswordPage() {
|
|
20
20
|
const t = useTranslations('auth')
|
|
21
21
|
const routeParams = useParams<{ locale: string }>()
|
|
22
22
|
const locale = routeParams.locale ?? 'en'
|
|
23
|
-
const [
|
|
24
|
-
const [
|
|
23
|
+
const [step, setStep] = useState<'send' | 'verify'>('send')
|
|
24
|
+
const [email, setEmail] = useState('')
|
|
25
|
+
const [countdown, setCountdown] = useState(0)
|
|
25
26
|
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const {
|
|
28
|
+
register,
|
|
29
|
+
handleSubmit,
|
|
30
|
+
formState: { errors },
|
|
31
|
+
reset,
|
|
32
|
+
setValue,
|
|
33
|
+
} = useForm<ResetPasswordWithOTPInput>({
|
|
34
|
+
resolver: zodResolver(resetPasswordWithOTPSchema),
|
|
35
|
+
defaultValues: { email: '', token: '', password: '', confirmPassword: '' },
|
|
29
36
|
})
|
|
30
37
|
|
|
31
|
-
const [
|
|
32
|
-
|
|
38
|
+
const [sendState, sendAction, isSendPending] = useActionState<ActionResult | null, FormData>(
|
|
39
|
+
sendPasswordResetOTP,
|
|
33
40
|
null,
|
|
34
41
|
)
|
|
35
42
|
|
|
43
|
+
const [verifyState, verifyAction, isVerifyPending] = useActionState<
|
|
44
|
+
ActionResult | null,
|
|
45
|
+
FormData
|
|
46
|
+
>(resetPasswordWithOTP, null)
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (sendState?.error) {
|
|
50
|
+
toast.error(sendState.error)
|
|
51
|
+
} else if (sendState?.data !== undefined && sendState.error === null) {
|
|
52
|
+
toast.success('验证码已发送,请检查邮箱')
|
|
53
|
+
setStep('verify')
|
|
54
|
+
startCountdown()
|
|
55
|
+
}
|
|
56
|
+
}, [sendState])
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (verifyState?.error) {
|
|
60
|
+
toast.error(verifyState.error)
|
|
61
|
+
}
|
|
62
|
+
}, [verifyState])
|
|
63
|
+
|
|
36
64
|
useEffect(() => {
|
|
37
|
-
if (
|
|
38
|
-
|
|
65
|
+
if (countdown > 0) {
|
|
66
|
+
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
|
|
67
|
+
return () => clearTimeout(timer)
|
|
68
|
+
}
|
|
69
|
+
return undefined
|
|
70
|
+
}, [countdown])
|
|
71
|
+
|
|
72
|
+
function startCountdown() {
|
|
73
|
+
setCountdown(60)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function onSendOTP(data: { email: string }) {
|
|
77
|
+
const formData = new FormData()
|
|
78
|
+
formData.append('email', data.email)
|
|
79
|
+
setEmail(data.email)
|
|
80
|
+
setValue('email', data.email)
|
|
81
|
+
startTransition(async () => {
|
|
82
|
+
await sendAction(formData)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function onResendOTP() {
|
|
87
|
+
if (countdown > 0) return
|
|
88
|
+
const formData = new FormData()
|
|
89
|
+
formData.append('email', email)
|
|
90
|
+
startTransition(async () => {
|
|
91
|
+
await sendAction(formData)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
39
94
|
|
|
40
|
-
async function onSubmit(data:
|
|
95
|
+
async function onSubmit(data: ResetPasswordWithOTPInput) {
|
|
41
96
|
const formData = new FormData()
|
|
42
97
|
formData.append('email', data.email)
|
|
98
|
+
formData.append('token', data.token)
|
|
99
|
+
formData.append('password', data.password)
|
|
100
|
+
formData.append('confirmPassword', data.confirmPassword)
|
|
43
101
|
formData.append('locale', locale)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
reset()
|
|
102
|
+
startTransition(async () => {
|
|
103
|
+
await verifyAction(formData)
|
|
104
|
+
})
|
|
48
105
|
}
|
|
49
106
|
|
|
50
|
-
if (
|
|
107
|
+
if (step === 'send') {
|
|
51
108
|
return (
|
|
52
109
|
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
53
110
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
@@ -61,14 +118,38 @@ export default function ForgotPasswordPage() {
|
|
|
61
118
|
<Card>
|
|
62
119
|
<CardHeader className="text-center">
|
|
63
120
|
<CardTitle className="text-xl">{t('forgotPasswordTitle')}</CardTitle>
|
|
121
|
+
<CardDescription>{t('forgotPasswordSubtitle')}</CardDescription>
|
|
64
122
|
</CardHeader>
|
|
65
|
-
<CardContent
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
123
|
+
<CardContent>
|
|
124
|
+
<form onSubmit={handleSubmit(onSendOTP)}>
|
|
125
|
+
<FieldGroup>
|
|
126
|
+
<Field>
|
|
127
|
+
<FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
|
|
128
|
+
<Input
|
|
129
|
+
id="email"
|
|
130
|
+
type="email"
|
|
131
|
+
placeholder={t('emailPlaceholder')}
|
|
132
|
+
autoComplete="email"
|
|
133
|
+
{...register('email')}
|
|
134
|
+
/>
|
|
135
|
+
{errors.email && (
|
|
136
|
+
<FieldDescription className="text-destructive">
|
|
137
|
+
{errors.email.message}
|
|
138
|
+
</FieldDescription>
|
|
139
|
+
)}
|
|
140
|
+
</Field>
|
|
141
|
+
<Field>
|
|
142
|
+
<Button type="submit" className="w-full" disabled={isSendPending}>
|
|
143
|
+
{isSendPending ? '\u2026' : t('sendVerificationCode')}
|
|
144
|
+
</Button>
|
|
145
|
+
<FieldDescription className="text-center">
|
|
146
|
+
<Link href={`/${locale}/login`} className="underline underline-offset-4">
|
|
147
|
+
{t('backToLogin')}
|
|
148
|
+
</Link>
|
|
149
|
+
</FieldDescription>
|
|
150
|
+
</Field>
|
|
151
|
+
</FieldGroup>
|
|
152
|
+
</form>
|
|
72
153
|
</CardContent>
|
|
73
154
|
</Card>
|
|
74
155
|
</div>
|
|
@@ -76,6 +157,7 @@ export default function ForgotPasswordPage() {
|
|
|
76
157
|
)
|
|
77
158
|
}
|
|
78
159
|
|
|
160
|
+
// step === 'verify'
|
|
79
161
|
return (
|
|
80
162
|
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
81
163
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
@@ -88,8 +170,8 @@ export default function ForgotPasswordPage() {
|
|
|
88
170
|
|
|
89
171
|
<Card>
|
|
90
172
|
<CardHeader className="text-center">
|
|
91
|
-
<CardTitle className="text-xl">{t('
|
|
92
|
-
<CardDescription>{t('
|
|
173
|
+
<CardTitle className="text-xl">{t('resetPasswordTitle')}</CardTitle>
|
|
174
|
+
<CardDescription>{t('resetPasswordSubtitle')}</CardDescription>
|
|
93
175
|
</CardHeader>
|
|
94
176
|
<CardContent>
|
|
95
177
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
@@ -97,27 +179,80 @@ export default function ForgotPasswordPage() {
|
|
|
97
179
|
<FieldGroup>
|
|
98
180
|
<Field>
|
|
99
181
|
<FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
|
|
182
|
+
<Input id="email" type="email" readOnly value={email} {...register('email')} />
|
|
183
|
+
</Field>
|
|
184
|
+
<Field>
|
|
185
|
+
<FieldLabel htmlFor="token">{t('verificationCodeLabel')}</FieldLabel>
|
|
186
|
+
<Input
|
|
187
|
+
id="token"
|
|
188
|
+
type="text"
|
|
189
|
+
placeholder={t('verificationCodePlaceholder')}
|
|
190
|
+
maxLength={6}
|
|
191
|
+
{...register('token')}
|
|
192
|
+
/>
|
|
193
|
+
{errors.token && (
|
|
194
|
+
<FieldDescription className="text-destructive">
|
|
195
|
+
{errors.token.message}
|
|
196
|
+
</FieldDescription>
|
|
197
|
+
)}
|
|
198
|
+
<FieldDescription className="mt-1">
|
|
199
|
+
{countdown > 0 ? (
|
|
200
|
+
<span className="text-muted-foreground">
|
|
201
|
+
{t('resendIn', { seconds: countdown })}
|
|
202
|
+
</span>
|
|
203
|
+
) : (
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={onResendOTP}
|
|
207
|
+
className="text-sm text-primary hover:underline"
|
|
208
|
+
>
|
|
209
|
+
{t('resendCode')}
|
|
210
|
+
</button>
|
|
211
|
+
)}
|
|
212
|
+
</FieldDescription>
|
|
213
|
+
</Field>
|
|
214
|
+
<Field>
|
|
215
|
+
<FieldLabel htmlFor="password">{t('newPasswordLabel')}</FieldLabel>
|
|
216
|
+
<Input
|
|
217
|
+
id="password"
|
|
218
|
+
type="password"
|
|
219
|
+
placeholder={t('newPasswordPlaceholder')}
|
|
220
|
+
autoComplete="new-password"
|
|
221
|
+
{...register('password')}
|
|
222
|
+
/>
|
|
223
|
+
{errors.password && (
|
|
224
|
+
<FieldDescription className="text-destructive">
|
|
225
|
+
{errors.password.message}
|
|
226
|
+
</FieldDescription>
|
|
227
|
+
)}
|
|
228
|
+
</Field>
|
|
229
|
+
<Field>
|
|
230
|
+
<FieldLabel htmlFor="confirmPassword">{t('confirmPasswordLabel')}</FieldLabel>
|
|
100
231
|
<Input
|
|
101
|
-
id="
|
|
102
|
-
type="
|
|
103
|
-
placeholder={t('
|
|
104
|
-
autoComplete="
|
|
105
|
-
{...register('
|
|
232
|
+
id="confirmPassword"
|
|
233
|
+
type="password"
|
|
234
|
+
placeholder={t('confirmPasswordPlaceholder')}
|
|
235
|
+
autoComplete="new-password"
|
|
236
|
+
{...register('confirmPassword')}
|
|
106
237
|
/>
|
|
107
|
-
{errors.
|
|
238
|
+
{errors.confirmPassword && (
|
|
108
239
|
<FieldDescription className="text-destructive">
|
|
109
|
-
{errors.
|
|
240
|
+
{errors.confirmPassword.message}
|
|
110
241
|
</FieldDescription>
|
|
111
242
|
)}
|
|
112
243
|
</Field>
|
|
113
244
|
<Field>
|
|
114
|
-
<Button type="submit" className="w-full" disabled={
|
|
115
|
-
{
|
|
245
|
+
<Button type="submit" className="w-full" disabled={isVerifyPending}>
|
|
246
|
+
{isVerifyPending ? '\u2026' : t('resetPassword')}
|
|
116
247
|
</Button>
|
|
117
248
|
<FieldDescription className="text-center">
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={() => setStep('send')}
|
|
252
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
253
|
+
>
|
|
254
|
+
{t('changeEmail')}
|
|
255
|
+
</button>
|
|
121
256
|
</FieldDescription>
|
|
122
257
|
</Field>
|
|
123
258
|
</FieldGroup>
|
|
@@ -28,7 +28,10 @@ export default function LoginPage() {
|
|
|
28
28
|
const routeParams = useParams<{ locale: string }>()
|
|
29
29
|
const locale = routeParams.locale ?? 'en'
|
|
30
30
|
|
|
31
|
-
const {
|
|
31
|
+
const {
|
|
32
|
+
register,
|
|
33
|
+
formState: { errors },
|
|
34
|
+
} = useForm<SignInInput>({
|
|
32
35
|
resolver: zodResolver(signInSchema),
|
|
33
36
|
defaultValues: { email: '', password: '' },
|
|
34
37
|
})
|
|
@@ -41,7 +44,10 @@ export default function LoginPage() {
|
|
|
41
44
|
|
|
42
45
|
async function handleGithub() {
|
|
43
46
|
const result = await signInWithGithubForLocale(locale)
|
|
44
|
-
if (result.error) {
|
|
47
|
+
if (result.error) {
|
|
48
|
+
toast.error(result.error)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
45
51
|
if (result.data?.url) router.push(result.data.url)
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -72,7 +78,11 @@ export default function LoginPage() {
|
|
|
72
78
|
className="w-full"
|
|
73
79
|
onClick={handleGithub}
|
|
74
80
|
>
|
|
75
|
-
<svg
|
|
81
|
+
<svg
|
|
82
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
83
|
+
viewBox="0 0 24 24"
|
|
84
|
+
className="size-4"
|
|
85
|
+
>
|
|
76
86
|
<path
|
|
77
87
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
78
88
|
fill="currentColor"
|
|
@@ -23,7 +23,10 @@ export default function ResetPasswordPage() {
|
|
|
23
23
|
const locale = routeParams.locale ?? 'en'
|
|
24
24
|
const error = searchParams.get('error')
|
|
25
25
|
|
|
26
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
register,
|
|
28
|
+
formState: { errors },
|
|
29
|
+
} = useForm<ResetPasswordInput>({
|
|
27
30
|
resolver: zodResolver(resetPasswordSchema),
|
|
28
31
|
defaultValues: { password: '', confirmPassword: '' },
|
|
29
32
|
})
|
|
@@ -11,12 +11,7 @@ import { toast } from 'sonner'
|
|
|
11
11
|
import { Button } from '@/components/ui/button'
|
|
12
12
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
13
|
import { Input } from '@/components/ui/input'
|
|
14
|
-
import {
|
|
15
|
-
Field,
|
|
16
|
-
FieldDescription,
|
|
17
|
-
FieldGroup,
|
|
18
|
-
FieldLabel,
|
|
19
|
-
} from '@/components/ui/field'
|
|
14
|
+
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
|
|
20
15
|
import { signUp } from '@/features/auth'
|
|
21
16
|
import { signUpSchema, type SignUpInput } from '@/lib/validations/auth'
|
|
22
17
|
import type { ActionResult } from '@/core/types/auth'
|
|
@@ -28,15 +23,18 @@ export default function SignupPage() {
|
|
|
28
23
|
const locale = routeParams.locale ?? 'en'
|
|
29
24
|
const [verifyEmail, setVerifyEmail] = useState<string | null>(null)
|
|
30
25
|
|
|
31
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
register,
|
|
28
|
+
formState: { errors },
|
|
29
|
+
} = useForm<SignUpInput>({
|
|
32
30
|
resolver: zodResolver(signUpSchema),
|
|
33
31
|
defaultValues: { email: '', password: '', confirmPassword: '' },
|
|
34
32
|
})
|
|
35
33
|
|
|
36
|
-
const [state, formAction, isPending] = useActionState<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
34
|
+
const [state, formAction, isPending] = useActionState<
|
|
35
|
+
ActionResult<SignUpSuccessData> | null,
|
|
36
|
+
FormData
|
|
37
|
+
>(signUp, null)
|
|
40
38
|
|
|
41
39
|
useEffect(() => {
|
|
42
40
|
if (state?.error) toast.error(state.error)
|
|
@@ -56,9 +54,7 @@ export default function SignupPage() {
|
|
|
56
54
|
<Card>
|
|
57
55
|
<CardHeader className="text-center">
|
|
58
56
|
<CardTitle className="text-xl">{t('verifyEmailTitle')}</CardTitle>
|
|
59
|
-
<CardDescription>
|
|
60
|
-
{t('verifyEmailMessage', { email: verifyEmail })}
|
|
61
|
-
</CardDescription>
|
|
57
|
+
<CardDescription>{t('verifyEmailMessage', { email: verifyEmail })}</CardDescription>
|
|
62
58
|
</CardHeader>
|
|
63
59
|
<CardContent>
|
|
64
60
|
<div className="text-center">
|
|
@@ -158,9 +154,14 @@ export default function SignupPage() {
|
|
|
158
154
|
</Card>
|
|
159
155
|
<FieldDescription className="px-6 text-center">
|
|
160
156
|
{t('termsText')}{' '}
|
|
161
|
-
<Link href={`/${locale}/terms`} className="underline underline-offset-4">
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
<Link href={`/${locale}/terms`} className="underline underline-offset-4">
|
|
158
|
+
{t('termsLink')}
|
|
159
|
+
</Link>{' '}
|
|
160
|
+
{t('andText')}{' '}
|
|
161
|
+
<Link href={`/${locale}/privacy`} className="underline underline-offset-4">
|
|
162
|
+
{t('privacyLink')}
|
|
163
|
+
</Link>
|
|
164
|
+
.
|
|
164
165
|
</FieldDescription>
|
|
165
166
|
</div>
|
|
166
167
|
</div>
|
|
@@ -8,11 +8,7 @@ import { getCurrentUser } from '@/features/auth/server'
|
|
|
8
8
|
|
|
9
9
|
import data from '@/components/dashboard/data.json'
|
|
10
10
|
|
|
11
|
-
export default async function DashboardPage({
|
|
12
|
-
params,
|
|
13
|
-
}: {
|
|
14
|
-
params: Promise<{ locale: string }>
|
|
15
|
-
}) {
|
|
11
|
+
export default async function DashboardPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
16
12
|
const { locale } = await params
|
|
17
13
|
const user = await getCurrentUser()
|
|
18
14
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react'
|
|
2
2
|
|
|
3
3
|
export default function SettingsLayout({ children }: { children: ReactNode }) {
|
|
4
|
-
return
|
|
5
|
-
<div className="container mx-auto max-w-4xl px-4 py-10">
|
|
6
|
-
{children}
|
|
7
|
-
</div>
|
|
8
|
-
)
|
|
4
|
+
return <div className="container mx-auto max-w-4xl px-4 py-10">{children}</div>
|
|
9
5
|
}
|