Spaces:
Build error
Build error
'use client' | |
import { useCallback, useState } from 'react' | |
import { useTranslation } from 'react-i18next' | |
import { useRouter, useSearchParams } from 'next/navigation' | |
import cn from 'classnames' | |
import { RiCheckboxCircleFill } from '@remixicon/react' | |
import { useCountDown } from 'ahooks' | |
import Button from '@/app/components/base/button' | |
import { changePasswordWithToken } from '@/service/common' | |
import Toast from '@/app/components/base/toast' | |
import Input from '@/app/components/base/input' | |
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |
const ChangePasswordForm = () => { | |
const { t } = useTranslation() | |
const router = useRouter() | |
const searchParams = useSearchParams() | |
const token = decodeURIComponent(searchParams.get('token') || '') | |
const [password, setPassword] = useState('') | |
const [confirmPassword, setConfirmPassword] = useState('') | |
const [showSuccess, setShowSuccess] = useState(false) | |
const [showPassword, setShowPassword] = useState(false) | |
const [showConfirmPassword, setShowConfirmPassword] = useState(false) | |
const showErrorMessage = useCallback((message: string) => { | |
Toast.notify({ | |
type: 'error', | |
message, | |
}) | |
}, []) | |
const getSignInUrl = () => { | |
if (searchParams.has('invite_token')) { | |
const params = new URLSearchParams() | |
params.set('token', searchParams.get('invite_token') as string) | |
return `/activate?${params.toString()}` | |
} | |
return '/signin' | |
} | |
const AUTO_REDIRECT_TIME = 5000 | |
const [leftTime, setLeftTime] = useState<number | undefined>(undefined) | |
const [countdown] = useCountDown({ | |
leftTime, | |
onEnd: () => { | |
router.replace(getSignInUrl()) | |
}, | |
}) | |
const valid = useCallback(() => { | |
if (!password.trim()) { | |
showErrorMessage(t('login.error.passwordEmpty')) | |
return false | |
} | |
if (!validPassword.test(password)) { | |
showErrorMessage(t('login.error.passwordInvalid')) | |
return false | |
} | |
if (password !== confirmPassword) { | |
showErrorMessage(t('common.account.notEqual')) | |
return false | |
} | |
return true | |
}, [password, confirmPassword, showErrorMessage, t]) | |
const handleChangePassword = useCallback(async () => { | |
if (!valid()) | |
return | |
try { | |
await changePasswordWithToken({ | |
url: '/forgot-password/resets', | |
body: { | |
token, | |
new_password: password, | |
password_confirm: confirmPassword, | |
}, | |
}) | |
setShowSuccess(true) | |
setLeftTime(AUTO_REDIRECT_TIME) | |
} | |
catch (error) { | |
console.error(error) | |
} | |
}, [password, token, valid, confirmPassword]) | |
return ( | |
<div className={ | |
cn( | |
'flex flex-col items-center w-full grow justify-center', | |
'px-6', | |
'md:px-[108px]', | |
) | |
}> | |
{!showSuccess && ( | |
<div className='flex flex-col md:w-[400px]'> | |
<div className="w-full mx-auto"> | |
<h2 className="title-4xl-semi-bold text-text-primary"> | |
{t('login.changePassword')} | |
</h2> | |
<p className='mt-2 body-md-regular text-text-secondary'> | |
{t('login.changePasswordTip')} | |
</p> | |
</div> | |
<div className="w-full mx-auto mt-6"> | |
<div className="bg-white"> | |
{/* Password */} | |
<div className='mb-5'> | |
<label htmlFor="password" className="my-2 system-md-semibold text-text-secondary"> | |
{t('common.account.newPassword')} | |
</label> | |
<div className='relative mt-1'> | |
<Input | |
id="password" type={showPassword ? 'text' : 'password'} | |
value={password} | |
onChange={e => setPassword(e.target.value)} | |
placeholder={t('login.passwordPlaceholder') || ''} | |
/> | |
<div className="absolute inset-y-0 right-0 flex items-center"> | |
<Button | |
type="button" | |
variant='ghost' | |
onClick={() => setShowPassword(!showPassword)} | |
> | |
{showPassword ? '๐' : '๐'} | |
</Button> | |
</div> | |
</div> | |
<div className='mt-1 body-xs-regular text-text-secondary'>{t('login.error.passwordInvalid')}</div> | |
</div> | |
{/* Confirm Password */} | |
<div className='mb-5'> | |
<label htmlFor="confirmPassword" className="my-2 system-md-semibold text-text-secondary"> | |
{t('common.account.confirmPassword')} | |
</label> | |
<div className='relative mt-1'> | |
<Input | |
id="confirmPassword" | |
type={showConfirmPassword ? 'text' : 'password'} | |
value={confirmPassword} | |
onChange={e => setConfirmPassword(e.target.value)} | |
placeholder={t('login.confirmPasswordPlaceholder') || ''} | |
/> | |
<div className="absolute inset-y-0 right-0 flex items-center"> | |
<Button | |
type="button" | |
variant='ghost' | |
onClick={() => setShowConfirmPassword(!showConfirmPassword)} | |
> | |
{showConfirmPassword ? '๐' : '๐'} | |
</Button> | |
</div> | |
</div> | |
</div> | |
<div> | |
<Button | |
variant='primary' | |
className='w-full' | |
onClick={handleChangePassword} | |
> | |
{t('login.changePasswordBtn')} | |
</Button> | |
</div> | |
</div> | |
</div> | |
</div> | |
)} | |
{showSuccess && ( | |
<div className="flex flex-col md:w-[400px]"> | |
<div className="w-full mx-auto"> | |
<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"> | |
<RiCheckboxCircleFill className='w-6 h-6 text-text-success' /> | |
</div> | |
<h2 className="title-4xl-semi-bold text-text-primary"> | |
{t('login.passwordChangedTip')} | |
</h2> | |
</div> | |
<div className="w-full mx-auto mt-6"> | |
<Button variant='primary' className='w-full' onClick={() => { | |
setLeftTime(undefined) | |
router.replace(getSignInUrl()) | |
}}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button> | |
</div> | |
</div> | |
)} | |
</div> | |
) | |
} | |
export default ChangePasswordForm | |