page.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. 'use client'
  2. import { useCallback, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { useRouter, useSearchParams } from 'next/navigation'
  5. import cn from 'classnames'
  6. import { RiCheckboxCircleFill } from '@remixicon/react'
  7. import { useCountDown } from 'ahooks'
  8. import Button from '@/app/components/base/button'
  9. import { changePasswordWithToken } from '@/service/common'
  10. import Toast from '@/app/components/base/toast'
  11. import Input from '@/app/components/base/input'
  12. const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
  13. const ChangePasswordForm = () => {
  14. const { t } = useTranslation()
  15. const router = useRouter()
  16. const searchParams = useSearchParams()
  17. const token = decodeURIComponent(searchParams.get('token') || '')
  18. const [password, setPassword] = useState('')
  19. const [confirmPassword, setConfirmPassword] = useState('')
  20. const [showSuccess, setShowSuccess] = useState(false)
  21. const [showPassword, setShowPassword] = useState(false)
  22. const [showConfirmPassword, setShowConfirmPassword] = useState(false)
  23. const showErrorMessage = useCallback((message: string) => {
  24. Toast.notify({
  25. type: 'error',
  26. message,
  27. })
  28. }, [])
  29. const getSignInUrl = () => {
  30. if (searchParams.has('invite_token')) {
  31. const params = new URLSearchParams()
  32. params.set('token', searchParams.get('invite_token') as string)
  33. return `/activate?${params.toString()}`
  34. }
  35. return '/signin'
  36. }
  37. const AUTO_REDIRECT_TIME = 5000
  38. const [leftTime, setLeftTime] = useState<number | undefined>(undefined)
  39. const [countdown] = useCountDown({
  40. leftTime,
  41. onEnd: () => {
  42. router.replace(getSignInUrl())
  43. },
  44. })
  45. const valid = useCallback(() => {
  46. if (!password.trim()) {
  47. showErrorMessage(t('login.error.passwordEmpty'))
  48. return false
  49. }
  50. if (!validPassword.test(password)) {
  51. showErrorMessage(t('login.error.passwordInvalid'))
  52. return false
  53. }
  54. if (password !== confirmPassword) {
  55. showErrorMessage(t('common.account.notEqual'))
  56. return false
  57. }
  58. return true
  59. }, [password, confirmPassword, showErrorMessage, t])
  60. const handleChangePassword = useCallback(async () => {
  61. if (!valid())
  62. return
  63. try {
  64. await changePasswordWithToken({
  65. url: '/forgot-password/resets',
  66. body: {
  67. token,
  68. new_password: password,
  69. password_confirm: confirmPassword,
  70. },
  71. })
  72. setShowSuccess(true)
  73. setLeftTime(AUTO_REDIRECT_TIME)
  74. }
  75. catch (error) {
  76. console.error(error)
  77. }
  78. }, [password, token, valid, confirmPassword])
  79. return (
  80. <div className={
  81. cn(
  82. 'flex flex-col items-center w-full grow justify-center',
  83. 'px-6',
  84. 'md:px-[108px]',
  85. )
  86. }>
  87. {!showSuccess && (
  88. <div className='flex flex-col md:w-[400px]'>
  89. <div className="w-full mx-auto">
  90. <h2 className="title-4xl-semi-bold text-text-primary">
  91. {t('login.changePassword')}
  92. </h2>
  93. <p className='mt-2 body-md-regular text-text-secondary'>
  94. {t('login.changePasswordTip')}
  95. </p>
  96. </div>
  97. <div className="w-full mx-auto mt-6">
  98. <div className="bg-white">
  99. {/* Password */}
  100. <div className='mb-5'>
  101. <label htmlFor="password" className="my-2 system-md-semibold text-text-secondary">
  102. {t('common.account.newPassword')}
  103. </label>
  104. <div className='relative mt-1'>
  105. <Input
  106. id="password" type={showPassword ? 'text' : 'password'}
  107. value={password}
  108. onChange={e => setPassword(e.target.value)}
  109. placeholder={t('login.passwordPlaceholder') || ''}
  110. />
  111. <div className="absolute inset-y-0 right-0 flex items-center">
  112. <Button
  113. type="button"
  114. variant='ghost'
  115. onClick={() => setShowPassword(!showPassword)}
  116. >
  117. {showPassword ? '👀' : '😝'}
  118. </Button>
  119. </div>
  120. </div>
  121. <div className='mt-1 body-xs-regular text-text-secondary'>{t('login.error.passwordInvalid')}</div>
  122. </div>
  123. {/* Confirm Password */}
  124. <div className='mb-5'>
  125. <label htmlFor="confirmPassword" className="my-2 system-md-semibold text-text-secondary">
  126. {t('common.account.confirmPassword')}
  127. </label>
  128. <div className='relative mt-1'>
  129. <Input
  130. id="confirmPassword"
  131. type={showConfirmPassword ? 'text' : 'password'}
  132. value={confirmPassword}
  133. onChange={e => setConfirmPassword(e.target.value)}
  134. placeholder={t('login.confirmPasswordPlaceholder') || ''}
  135. />
  136. <div className="absolute inset-y-0 right-0 flex items-center">
  137. <Button
  138. type="button"
  139. variant='ghost'
  140. onClick={() => setShowConfirmPassword(!showConfirmPassword)}
  141. >
  142. {showConfirmPassword ? '👀' : '😝'}
  143. </Button>
  144. </div>
  145. </div>
  146. </div>
  147. <div>
  148. <Button
  149. variant='primary'
  150. className='w-full'
  151. onClick={handleChangePassword}
  152. >
  153. {t('login.changePasswordBtn')}
  154. </Button>
  155. </div>
  156. </div>
  157. </div>
  158. </div>
  159. )}
  160. {showSuccess && (
  161. <div className="flex flex-col md:w-[400px]">
  162. <div className="w-full mx-auto">
  163. <div className="mb-3 flex justify-center items-center w-14 h-14 rounded-2xl border border-components-panel-border-subtle shadow-lg font-bold">
  164. <RiCheckboxCircleFill className='w-6 h-6 text-text-success' />
  165. </div>
  166. <h2 className="title-4xl-semi-bold text-text-primary">
  167. {t('login.passwordChangedTip')}
  168. </h2>
  169. </div>
  170. <div className="w-full mx-auto mt-6">
  171. <Button variant='primary' className='w-full' onClick={() => {
  172. setLeftTime(undefined)
  173. router.replace(getSignInUrl())
  174. }}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button>
  175. </div>
  176. </div>
  177. )}
  178. </div>
  179. )
  180. }
  181. export default ChangePasswordForm