@cogito.ai/cli 0.4.2 → 0.4.4

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.
Files changed (135) hide show
  1. package/README.md +29 -22
  2. package/dist/index.js +9 -15
  3. package/dist/templates/web-nextjs/.github/copilot-instructions.md +5 -6
  4. package/dist/templates/web-nextjs/README.md +25 -24
  5. package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +1 -1
  6. package/dist/templates/web-nextjs/apps/docs/.source/server.ts +6 -6
  7. package/dist/templates/web-nextjs/apps/docs/app/docs/[[...slug]]/page.tsx +1 -6
  8. package/dist/templates/web-nextjs/apps/docs/app/docs/layout.tsx +1 -4
  9. package/dist/templates/web-nextjs/apps/docs/app/llms-full.txt/route.ts +3 -1
  10. package/dist/templates/web-nextjs/apps/docs/next-env.d.ts +1 -1
  11. package/dist/templates/web-nextjs/apps/web/.env.example +35 -0
  12. package/dist/templates/web-nextjs/apps/web/.github/copilot-instructions.md +53 -6
  13. package/dist/templates/web-nextjs/apps/web/.github/skills/impeccable/SKILL.md +55 -0
  14. package/dist/templates/web-nextjs/apps/web/DESIGN.md +65 -0
  15. package/dist/templates/web-nextjs/apps/web/messages/en.json +151 -5
  16. package/dist/templates/web-nextjs/apps/web/messages/zh.json +151 -5
  17. package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
  18. package/dist/templates/web-nextjs/apps/web/next.config.ts +3 -3
  19. package/dist/templates/web-nextjs/apps/web/package.json +4 -0
  20. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +167 -38
  21. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +13 -3
  22. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +4 -1
  23. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +18 -17
  24. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +1 -5
  25. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx +88 -0
  26. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx +170 -0
  27. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/pricing/page.tsx +120 -0
  28. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +45 -2
  29. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/profile/page.tsx +2 -8
  30. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/subscription/page.tsx +128 -0
  31. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/about/page.tsx +3 -6
  32. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +17 -5
  33. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/help/page.tsx +1 -5
  34. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +10 -8
  35. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/page.tsx +22 -6
  36. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/privacy/page.tsx +3 -6
  37. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/terms/page.tsx +1 -5
  38. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/create/route.ts +43 -0
  39. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/notify/route.ts +105 -0
  40. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/query/route.ts +54 -0
  41. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/return/route.ts +20 -0
  42. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/create/route.ts +52 -0
  43. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/create/route.ts +58 -0
  44. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/notify/route.ts +92 -0
  45. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/query/route.ts +54 -0
  46. package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +2 -3
  47. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/app-sidebar.tsx +13 -16
  48. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/chart-area-interactive.tsx +122 -146
  49. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/data-table.tsx +84 -149
  50. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-documents.tsx +7 -16
  51. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-main.tsx +4 -4
  52. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-secondary.tsx +4 -4
  53. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-user.tsx +12 -21
  54. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/page.tsx +10 -13
  55. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/section-cards.tsx +5 -9
  56. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/site-header.tsx +6 -7
  57. package/dist/templates/web-nextjs/apps/web/src/components/landing/features.tsx +63 -0
  58. package/dist/templates/web-nextjs/apps/web/src/components/landing/footer.tsx +48 -0
  59. package/dist/templates/web-nextjs/apps/web/src/components/landing/header.tsx +97 -0
  60. package/dist/templates/web-nextjs/apps/web/src/components/landing/hero.tsx +45 -0
  61. package/dist/templates/web-nextjs/apps/web/src/components/landing/how-it-works.tsx +35 -0
  62. package/dist/templates/web-nextjs/apps/web/src/components/landing/pricing-teaser.tsx +23 -0
  63. package/dist/templates/web-nextjs/apps/web/src/components/profile/profile-form.tsx +6 -4
  64. package/dist/templates/web-nextjs/apps/web/src/components/providers/theme-provider.tsx +16 -0
  65. package/dist/templates/web-nextjs/apps/web/src/components/ui/alert-dialog.tsx +32 -49
  66. package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +16 -23
  67. package/dist/templates/web-nextjs/apps/web/src/components/ui/avatar.tsx +25 -38
  68. package/dist/templates/web-nextjs/apps/web/src/components/ui/badge.tsx +16 -18
  69. package/dist/templates/web-nextjs/apps/web/src/components/ui/breadcrumb.tsx +19 -26
  70. package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +23 -24
  71. package/dist/templates/web-nextjs/apps/web/src/components/ui/card.tsx +19 -36
  72. package/dist/templates/web-nextjs/apps/web/src/components/ui/chart.tsx +60 -94
  73. package/dist/templates/web-nextjs/apps/web/src/components/ui/checkbox.tsx +8 -11
  74. package/dist/templates/web-nextjs/apps/web/src/components/ui/collapsible.tsx +5 -17
  75. package/dist/templates/web-nextjs/apps/web/src/components/ui/command.tsx +25 -48
  76. package/dist/templates/web-nextjs/apps/web/src/components/ui/dialog.tsx +21 -35
  77. package/dist/templates/web-nextjs/apps/web/src/components/ui/drawer.tsx +24 -35
  78. package/dist/templates/web-nextjs/apps/web/src/components/ui/dropdown-menu.tsx +26 -55
  79. package/dist/templates/web-nextjs/apps/web/src/components/ui/field.tsx +62 -76
  80. package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +19 -34
  81. package/dist/templates/web-nextjs/apps/web/src/components/ui/input-otp.tsx +13 -20
  82. package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +6 -6
  83. package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +6 -6
  84. package/dist/templates/web-nextjs/apps/web/src/components/ui/pagination.tsx +21 -42
  85. package/dist/templates/web-nextjs/apps/web/src/components/ui/popover.tsx +16 -31
  86. package/dist/templates/web-nextjs/apps/web/src/components/ui/progress.tsx +5 -8
  87. package/dist/templates/web-nextjs/apps/web/src/components/ui/radio-group.tsx +8 -8
  88. package/dist/templates/web-nextjs/apps/web/src/components/ui/scroll-area.tsx +10 -12
  89. package/dist/templates/web-nextjs/apps/web/src/components/ui/select.tsx +26 -41
  90. package/dist/templates/web-nextjs/apps/web/src/components/ui/separator.tsx +7 -7
  91. package/dist/templates/web-nextjs/apps/web/src/components/ui/sheet.tsx +29 -38
  92. package/dist/templates/web-nextjs/apps/web/src/components/ui/sidebar.tsx +157 -189
  93. package/dist/templates/web-nextjs/apps/web/src/components/ui/skeleton.tsx +3 -3
  94. package/dist/templates/web-nextjs/apps/web/src/components/ui/slider.tsx +10 -15
  95. package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +13 -7
  96. package/dist/templates/web-nextjs/apps/web/src/components/ui/switch.tsx +9 -9
  97. package/dist/templates/web-nextjs/apps/web/src/components/ui/table.tsx +24 -48
  98. package/dist/templates/web-nextjs/apps/web/src/components/ui/tabs.tsx +21 -31
  99. package/dist/templates/web-nextjs/apps/web/src/components/ui/textarea.tsx +5 -5
  100. package/dist/templates/web-nextjs/apps/web/src/components/ui/theme-toggle.tsx +23 -0
  101. package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle-group.tsx +15 -16
  102. package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle.tsx +14 -15
  103. package/dist/templates/web-nextjs/apps/web/src/components/ui/tooltip.tsx +8 -12
  104. package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +2 -0
  105. package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +1 -3
  106. package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +57 -1
  107. package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +2 -0
  108. package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/client.ts +36 -0
  109. package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/server.ts +83 -0
  110. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/index.ts +4 -0
  111. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-badge.tsx +9 -0
  112. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-feature-comparison.tsx +70 -0
  113. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/quota-warning-banner.tsx +37 -0
  114. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/upgrade-button.tsx +42 -0
  115. package/dist/templates/web-nextjs/apps/web/src/features/subscription/hooks.ts +141 -0
  116. package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-helpers.ts +27 -0
  117. package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-service.ts +56 -0
  118. package/dist/templates/web-nextjs/apps/web/src/features/subscription/service.ts +55 -0
  119. package/dist/templates/web-nextjs/apps/web/src/features/subscription/types.ts +73 -0
  120. package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/client.ts +33 -0
  121. package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/server.ts +147 -0
  122. package/dist/templates/web-nextjs/apps/web/src/hooks/use-mobile.ts +4 -4
  123. package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +1 -1
  124. package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +48 -4
  125. package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +1 -4
  126. package/dist/templates/web-nextjs/apps/web/src/lib/supabase/admin.ts +23 -0
  127. package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +2 -2
  128. package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +13 -0
  129. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/client.ts +66 -0
  130. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/crypto.ts +99 -0
  131. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/types.ts +10 -0
  132. package/dist/templates/web-nextjs/apps/web/src/styles/tokens.css +58 -0
  133. package/dist/templates/web-nextjs/pnpm-lock.yaml +319 -0
  134. package/dist/templates/web-nextjs/supabase/migrations/20250608_add_subscription_tables.sql +125 -0
  135. package/package.json +1 -1
@@ -12,42 +12,93 @@ 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 { requestPasswordReset } from '@/features/auth'
16
- import { forgotPasswordSchema, type ForgotPasswordInput } from '@/lib/validations/auth'
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 [isSent, setIsSent] = useState(false)
24
- const [submittedEmail, setSubmittedEmail] = useState('')
23
+ const [step, setStep] = useState<'send' | 'verify'>('send')
24
+ const [email, setEmail] = useState('')
25
+ const [countdown, setCountdown] = useState(0)
25
26
 
26
- const { register, handleSubmit, formState: { errors }, reset } = useForm<ForgotPasswordInput>({
27
- resolver: zodResolver(forgotPasswordSchema),
28
- defaultValues: { email: '' },
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 [state, formAction, isPending] = useActionState<ActionResult | null, FormData>(
32
- requestPasswordReset,
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 (state?.error) toast.error(state.error)
38
- }, [state])
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
+ sendAction(formData)
82
+ }
83
+
84
+ async function onResendOTP() {
85
+ if (countdown > 0) return
86
+ const formData = new FormData()
87
+ formData.append('email', email)
88
+ sendAction(formData)
89
+ }
39
90
 
40
- async function onSubmit(data: ForgotPasswordInput) {
91
+ async function onSubmit(data: ResetPasswordWithOTPInput) {
41
92
  const formData = new FormData()
42
93
  formData.append('email', data.email)
94
+ formData.append('token', data.token)
95
+ formData.append('password', data.password)
96
+ formData.append('confirmPassword', data.confirmPassword)
43
97
  formData.append('locale', locale)
44
- await formAction(formData)
45
- setSubmittedEmail(data.email)
46
- setIsSent(true)
47
- reset()
98
+ verifyAction(formData)
48
99
  }
49
100
 
50
- if (isSent) {
101
+ if (step === 'send') {
51
102
  return (
52
103
  <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
53
104
  <div className="flex w-full max-w-sm flex-col gap-6">
@@ -61,14 +112,38 @@ export default function ForgotPasswordPage() {
61
112
  <Card>
62
113
  <CardHeader className="text-center">
63
114
  <CardTitle className="text-xl">{t('forgotPasswordTitle')}</CardTitle>
115
+ <CardDescription>{t('forgotPasswordSubtitle')}</CardDescription>
64
116
  </CardHeader>
65
- <CardContent className="space-y-4">
66
- <p className="text-center text-sm text-muted-foreground">
67
- {t('forgotPasswordSent', { email: submittedEmail })}
68
- </p>
69
- <Button asChild variant="outline" className="w-full">
70
- <Link href={`/${locale}/login`}>{t('backToLogin')}</Link>
71
- </Button>
117
+ <CardContent>
118
+ <form onSubmit={handleSubmit(onSendOTP)}>
119
+ <FieldGroup>
120
+ <Field>
121
+ <FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
122
+ <Input
123
+ id="email"
124
+ type="email"
125
+ placeholder={t('emailPlaceholder')}
126
+ autoComplete="email"
127
+ {...register('email')}
128
+ />
129
+ {errors.email && (
130
+ <FieldDescription className="text-destructive">
131
+ {errors.email.message}
132
+ </FieldDescription>
133
+ )}
134
+ </Field>
135
+ <Field>
136
+ <Button type="submit" className="w-full" disabled={isSendPending}>
137
+ {isSendPending ? '\u2026' : t('sendVerificationCode')}
138
+ </Button>
139
+ <FieldDescription className="text-center">
140
+ <Link href={`/${locale}/login`} className="underline underline-offset-4">
141
+ {t('backToLogin')}
142
+ </Link>
143
+ </FieldDescription>
144
+ </Field>
145
+ </FieldGroup>
146
+ </form>
72
147
  </CardContent>
73
148
  </Card>
74
149
  </div>
@@ -76,6 +151,7 @@ export default function ForgotPasswordPage() {
76
151
  )
77
152
  }
78
153
 
154
+ // step === 'verify'
79
155
  return (
80
156
  <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
81
157
  <div className="flex w-full max-w-sm flex-col gap-6">
@@ -88,8 +164,8 @@ export default function ForgotPasswordPage() {
88
164
 
89
165
  <Card>
90
166
  <CardHeader className="text-center">
91
- <CardTitle className="text-xl">{t('forgotPasswordTitle')}</CardTitle>
92
- <CardDescription>{t('forgotPasswordSubtitle')}</CardDescription>
167
+ <CardTitle className="text-xl">{t('resetPasswordTitle')}</CardTitle>
168
+ <CardDescription>{t('resetPasswordSubtitle')}</CardDescription>
93
169
  </CardHeader>
94
170
  <CardContent>
95
171
  <form onSubmit={handleSubmit(onSubmit)}>
@@ -97,27 +173,80 @@ export default function ForgotPasswordPage() {
97
173
  <FieldGroup>
98
174
  <Field>
99
175
  <FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
176
+ <Input id="email" type="email" readOnly value={email} {...register('email')} />
177
+ </Field>
178
+ <Field>
179
+ <FieldLabel htmlFor="token">{t('verificationCodeLabel')}</FieldLabel>
180
+ <Input
181
+ id="token"
182
+ type="text"
183
+ placeholder={t('verificationCodePlaceholder')}
184
+ maxLength={6}
185
+ {...register('token')}
186
+ />
187
+ {errors.token && (
188
+ <FieldDescription className="text-destructive">
189
+ {errors.token.message}
190
+ </FieldDescription>
191
+ )}
192
+ <FieldDescription className="mt-1">
193
+ {countdown > 0 ? (
194
+ <span className="text-muted-foreground">
195
+ {t('resendIn', { seconds: countdown })}
196
+ </span>
197
+ ) : (
198
+ <button
199
+ type="button"
200
+ onClick={onResendOTP}
201
+ className="text-sm text-primary hover:underline"
202
+ >
203
+ {t('resendCode')}
204
+ </button>
205
+ )}
206
+ </FieldDescription>
207
+ </Field>
208
+ <Field>
209
+ <FieldLabel htmlFor="password">{t('newPasswordLabel')}</FieldLabel>
210
+ <Input
211
+ id="password"
212
+ type="password"
213
+ placeholder={t('newPasswordPlaceholder')}
214
+ autoComplete="new-password"
215
+ {...register('password')}
216
+ />
217
+ {errors.password && (
218
+ <FieldDescription className="text-destructive">
219
+ {errors.password.message}
220
+ </FieldDescription>
221
+ )}
222
+ </Field>
223
+ <Field>
224
+ <FieldLabel htmlFor="confirmPassword">{t('confirmPasswordLabel')}</FieldLabel>
100
225
  <Input
101
- id="email"
102
- type="email"
103
- placeholder={t('emailPlaceholder')}
104
- autoComplete="email"
105
- {...register('email')}
226
+ id="confirmPassword"
227
+ type="password"
228
+ placeholder={t('confirmPasswordPlaceholder')}
229
+ autoComplete="new-password"
230
+ {...register('confirmPassword')}
106
231
  />
107
- {errors.email && (
232
+ {errors.confirmPassword && (
108
233
  <FieldDescription className="text-destructive">
109
- {errors.email.message}
234
+ {errors.confirmPassword.message}
110
235
  </FieldDescription>
111
236
  )}
112
237
  </Field>
113
238
  <Field>
114
- <Button type="submit" className="w-full" disabled={isPending}>
115
- {isPending ? '\u2026' : t('sendResetLink')}
239
+ <Button type="submit" className="w-full" disabled={isVerifyPending}>
240
+ {isVerifyPending ? '\u2026' : t('resetPassword')}
116
241
  </Button>
117
242
  <FieldDescription className="text-center">
118
- <Link href={`/${locale}/login`} className="underline underline-offset-4">
119
- {t('backToLogin')}
120
- </Link>
243
+ <button
244
+ type="button"
245
+ onClick={() => setStep('send')}
246
+ className="text-sm text-muted-foreground hover:text-foreground"
247
+ >
248
+ {t('changeEmail')}
249
+ </button>
121
250
  </FieldDescription>
122
251
  </Field>
123
252
  </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 { register, formState: { errors } } = useForm<SignInInput>({
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) { toast.error(result.error); return }
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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="size-4">
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 { register, formState: { errors } } = useForm<ResetPasswordInput>({
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 { register, formState: { errors } } = useForm<SignUpInput>({
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<ActionResult<SignUpSuccessData> | null, FormData>(
37
- signUp,
38
- null,
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">{t('termsLink')}</Link>
162
- {' '}{t('andText')}{' '}
163
- <Link href={`/${locale}/privacy`} className="underline underline-offset-4">{t('privacyLink')}</Link>.
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
 
@@ -0,0 +1,88 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useParams, useRouter } from 'next/navigation'
5
+ import { useTranslations } from 'next-intl'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
8
+ import { Badge } from '@/components/ui/badge'
9
+ import { usePaymentStatus } from '@/features/subscription/hooks'
10
+ import { createBrowserClient } from '@supabase/ssr'
11
+
12
+ export default function PaymentStatusPage() {
13
+ const t = useTranslations('payment')
14
+ const router = useRouter()
15
+ const params = useParams()
16
+ const paymentId = params.paymentId as string
17
+
18
+ const [paymentMethod, setPaymentMethod] = useState<string>('')
19
+ const [orderNo, setOrderNo] = useState<string>('')
20
+
21
+ useEffect(() => {
22
+ async function fetchPayment() {
23
+ const supabase = createBrowserClient(
24
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
25
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
26
+ )
27
+ const { data } = await supabase.from('payments').select('*').eq('id', paymentId).single()
28
+
29
+ if (data) {
30
+ setPaymentMethod(data.payment_method)
31
+ setOrderNo(data.order_no)
32
+ }
33
+ }
34
+ fetchPayment()
35
+ }, [paymentId])
36
+
37
+ const { data: paymentStatus } = usePaymentStatus(orderNo, paymentMethod, {
38
+ enabled: !!orderNo && !!paymentMethod,
39
+ })
40
+
41
+ const status = paymentStatus?.status || 'pending'
42
+
43
+ return (
44
+ <div className="container mx-auto max-w-2xl px-4 py-10">
45
+ <h1 className="text-2xl font-bold mb-6">{t('statusTitle')}</h1>
46
+
47
+ <Card>
48
+ <CardHeader>
49
+ <CardTitle className="flex items-center gap-2">
50
+ {t('paymentStatus')}
51
+ <Badge
52
+ variant={
53
+ status === 'paid' ? 'default' : status === 'failed' ? 'destructive' : 'secondary'
54
+ }
55
+ >
56
+ {t(status)}
57
+ </Badge>
58
+ </CardTitle>
59
+ </CardHeader>
60
+ <CardContent className="space-y-6">
61
+ {status === 'paid' && (
62
+ <div className="text-center space-y-4">
63
+ <div className="text-6xl">✓</div>
64
+ <p className="text-lg font-medium">{t('paymentSuccess')}</p>
65
+ <Button onClick={() => router.push('/dashboard')}>{t('goToDashboard')}</Button>
66
+ </div>
67
+ )}
68
+
69
+ {status === 'failed' && (
70
+ <div className="text-center space-y-4">
71
+ <div className="text-6xl">✗</div>
72
+ <p className="text-lg font-medium">{t('paymentFailed')}</p>
73
+ <Button onClick={() => router.push('/pricing')}>{t('tryAgain')}</Button>
74
+ </div>
75
+ )}
76
+
77
+ {status === 'pending' && (
78
+ <div className="text-center space-y-4">
79
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent mx-auto" />
80
+ <p className="text-lg font-medium">{t('paymentPending')}</p>
81
+ <p className="text-sm text-muted-foreground">{t('paymentPendingDescription')}</p>
82
+ </div>
83
+ )}
84
+ </CardContent>
85
+ </Card>
86
+ </div>
87
+ )
88
+ }
@@ -0,0 +1,170 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
5
+ import { useTranslations } from 'next-intl'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
8
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
9
+ import { Label } from '@/components/ui/label'
10
+ import { Input } from '@/components/ui/input'
11
+ import { useSubscriptionPlan } from '@/features/subscription/hooks'
12
+ import { createBrowserClient } from '@supabase/ssr'
13
+
14
+ export default function CheckoutPage() {
15
+ const t = useTranslations('payment')
16
+ const router = useRouter()
17
+ const searchParams = useSearchParams()
18
+ const planCode = searchParams.get('plan') || ''
19
+ const cycle = (searchParams.get('cycle') as 'monthly' | 'yearly') || 'monthly'
20
+
21
+ const { data: plan, isLoading } = useSubscriptionPlan(planCode)
22
+ const [paymentMethod, setPaymentMethod] = useState<'alipay' | 'wechat'>('alipay')
23
+ const [coupon, setCoupon] = useState('')
24
+ const [couponError, setCouponError] = useState('')
25
+ const [isProcessing, setIsProcessing] = useState(false)
26
+
27
+ if (!planCode) {
28
+ router.push('/pricing')
29
+ return null
30
+ }
31
+
32
+ if (isLoading) {
33
+ return (
34
+ <div className="container mx-auto max-w-2xl px-4 py-10">
35
+ <div className="flex justify-center py-20">
36
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
37
+ </div>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ const amount = cycle === 'monthly' ? plan?.price_monthly : plan?.price_yearly
43
+
44
+ const handleSubmit = async () => {
45
+ setIsProcessing(true)
46
+ try {
47
+ // Validate coupon (stub)
48
+ if (coupon && coupon !== 'DISCOUNT10') {
49
+ setCouponError(t('invalidCoupon'))
50
+ setIsProcessing(false)
51
+ return
52
+ }
53
+ setCouponError('')
54
+
55
+ const supabase = createBrowserClient(
56
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
57
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
58
+ )
59
+ const {
60
+ data: { user },
61
+ } = await supabase.auth.getUser()
62
+ if (!user) {
63
+ router.push('/login')
64
+ return
65
+ }
66
+
67
+ // Create payment record
68
+ const res = await fetch('/api/payments/create', {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({
72
+ userId: user.id,
73
+ planId: plan?.id,
74
+ amount,
75
+ paymentMethod,
76
+ billingCycle: cycle,
77
+ }),
78
+ })
79
+
80
+ if (!res.ok) throw new Error('Failed to create payment')
81
+ const payment = await res.json()
82
+
83
+ // Initiate payment
84
+ if (paymentMethod === 'alipay') {
85
+ const { initiateAlipayPayment } = await import('@/features/subscription/alipay/client')
86
+ await initiateAlipayPayment(payment.id, user.id)
87
+ } else {
88
+ const { initiateWechatPayment } = await import('@/features/subscription/wechat/client')
89
+ const result = await initiateWechatPayment(payment.id, user.id)
90
+ if (result.type === 'qrcode') {
91
+ router.push(`/payment/${payment.id}`)
92
+ }
93
+ }
94
+ } catch (error) {
95
+ setCouponError(t('paymentError'))
96
+ } finally {
97
+ setIsProcessing(false)
98
+ }
99
+ }
100
+
101
+ return (
102
+ <div className="container mx-auto max-w-2xl px-4 py-10">
103
+ <h1 className="text-2xl font-bold mb-6">{t('checkoutTitle')}</h1>
104
+
105
+ <Card className="mb-6">
106
+ <CardHeader>
107
+ <CardTitle>{t('orderSummary')}</CardTitle>
108
+ </CardHeader>
109
+ <CardContent className="space-y-4">
110
+ <div className="flex justify-between">
111
+ <span>{t('plan')}</span>
112
+ <span className="font-medium">{plan?.plan_name}</span>
113
+ </div>
114
+ <div className="flex justify-between">
115
+ <span>{t('billingCycle')}</span>
116
+ <span className="font-medium">{cycle === 'monthly' ? t('monthly') : t('yearly')}</span>
117
+ </div>
118
+ <div className="flex justify-between text-lg font-bold">
119
+ <span>{t('total')}</span>
120
+ <span>¥{((amount || 0) / 100).toFixed(2)}</span>
121
+ </div>
122
+ </CardContent>
123
+ </Card>
124
+
125
+ <Card className="mb-6">
126
+ <CardHeader>
127
+ <CardTitle>{t('paymentMethod')}</CardTitle>
128
+ </CardHeader>
129
+ <CardContent>
130
+ <RadioGroup
131
+ value={paymentMethod}
132
+ onValueChange={(v) => setPaymentMethod(v as 'alipay' | 'wechat')}
133
+ className="space-y-3"
134
+ >
135
+ <div className="flex items-center space-x-2 border rounded-lg p-4">
136
+ <RadioGroupItem value="alipay" id="alipay" />
137
+ <Label htmlFor="alipay" className="flex-1 cursor-pointer">
138
+ {t('alipay')}
139
+ </Label>
140
+ </div>
141
+ <div className="flex items-center space-x-2 border rounded-lg p-4">
142
+ <RadioGroupItem value="wechat" id="wechat" />
143
+ <Label htmlFor="wechat" className="flex-1 cursor-pointer">
144
+ {t('wechatPay')}
145
+ </Label>
146
+ </div>
147
+ </RadioGroup>
148
+ </CardContent>
149
+ </Card>
150
+
151
+ <Card className="mb-6">
152
+ <CardHeader>
153
+ <CardTitle>{t('coupon')}</CardTitle>
154
+ </CardHeader>
155
+ <CardContent>
156
+ <Input
157
+ placeholder={t('couponPlaceholder')}
158
+ value={coupon}
159
+ onChange={(e) => setCoupon(e.target.value)}
160
+ />
161
+ {couponError && <p className="text-sm text-destructive mt-2">{couponError}</p>}
162
+ </CardContent>
163
+ </Card>
164
+
165
+ <Button className="w-full" size="lg" onClick={handleSubmit} disabled={isProcessing}>
166
+ {isProcessing ? t('processing') : t('confirmPayment')}
167
+ </Button>
168
+ </div>
169
+ )
170
+ }