-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(fe): implement recover account modal (#1526)
* feat(fe): add modal layout * feat(fe): connect find userid api * feat(fe): add resetpassword * feat(fe): add reset password page * fix: add credential include, change fetcher to fetch * fix(fe): remove unnecessary onClick event handler * chore: replace fetch with fetcher
- Loading branch information
Showing
8 changed files
with
524 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { Button } from '@/components/ui/button' | ||
import { Input } from '@/components/ui/input' | ||
import { cn, fetcher } from '@/lib/utils' | ||
import useAuthModalStore from '@/stores/authModal' | ||
import useRecoverAccountModalStore from '@/stores/recoverAccountModal' | ||
import { zodResolver } from '@hookform/resolvers/zod' | ||
import { useState } from 'react' | ||
import { useForm } from 'react-hook-form' | ||
import { z } from 'zod' | ||
|
||
interface FindUserIdInput { | ||
email: string | ||
} | ||
const schema = z.object({ | ||
email: z.string().email({ message: 'Invalid email address' }) | ||
}) | ||
|
||
export default function FindUserId() { | ||
const [userId, setUserId] = useState<string>('') | ||
const [emailError, setEmailError] = useState<string>('') | ||
const [sentEmail, setSentEmail] = useState<boolean>(false) | ||
const { nextModal, setFormData } = useRecoverAccountModalStore( | ||
(state) => state | ||
) | ||
const { showSignIn, showSignUp } = useAuthModalStore((state) => state) | ||
|
||
const { | ||
handleSubmit, | ||
register, | ||
trigger, | ||
getValues, | ||
formState: { errors } | ||
} = useForm<FindUserIdInput>({ | ||
resolver: zodResolver(schema) | ||
}) | ||
|
||
const onSubmit = async (data: FindUserIdInput) => { | ||
const { email } = data | ||
try { | ||
const data: { username: string } = await fetcher | ||
.get('user/email', { | ||
searchParams: { email } | ||
}) | ||
.json() | ||
if (data.username) { | ||
setUserId(data.username) | ||
setEmailError('') | ||
} else setEmailError('* No account confirmed with this email') | ||
} catch { | ||
setEmailError('* No account confirmed with this email') | ||
} | ||
} | ||
|
||
const sendEmail = async () => { | ||
const { email } = getValues() | ||
setFormData({ | ||
email, | ||
verificationCode: '', | ||
headers: { | ||
'email-auth': '' | ||
} | ||
}) | ||
await trigger('email') | ||
if (!errors.email && !sentEmail) { | ||
await fetcher | ||
.post('email-auth/send-email/password-reset', { | ||
json: { email } | ||
}) | ||
.then((res) => { | ||
if (res.status === 401) { | ||
setEmailError( | ||
'Email authentication pin is sent to your email address' | ||
) | ||
} else if (res.status === 201) { | ||
setSentEmail(true) | ||
setEmailError('') | ||
} | ||
}) | ||
.catch(() => { | ||
setEmailError('Something went wrong!') | ||
}) | ||
} | ||
} | ||
|
||
return ( | ||
<> | ||
<form | ||
onSubmit={handleSubmit(onSubmit)} | ||
className="flex w-full flex-col gap-8 px-2" | ||
> | ||
<div className="flex flex-col gap-1"> | ||
<p className="text-primary mb-4 text-left text-xl font-bold"> | ||
Find User ID | ||
</p> | ||
<Input | ||
id="email" | ||
type="email" | ||
placeholder="Email Address" | ||
{...register('email', { | ||
onChange: () => trigger('email') | ||
})} | ||
disabled={!!userId} | ||
/> | ||
{errors.email && ( | ||
<p className="text-xs text-red-500">{errors.email?.message}</p> | ||
)} | ||
<p className="text-xs text-red-500">{emailError}</p> | ||
{userId ? ( | ||
<p className="text-center text-sm text-gray-500"> | ||
your User ID is <span className="text-primary">{userId}</span> | ||
</p> | ||
) : ( | ||
<p className="text-center text-sm text-gray-300"> | ||
your User ID is ___________ | ||
</p> | ||
)} | ||
</div> | ||
|
||
<div className="flex flex-col gap-4"> | ||
{userId ? ( | ||
<Button onClick={() => showSignIn()} type="button"> | ||
Log in | ||
</Button> | ||
) : ( | ||
<Button type="submit">Find User ID</Button> | ||
)} | ||
<Button | ||
type="button" | ||
onClick={() => { | ||
sendEmail() | ||
.then(() => { | ||
nextModal() | ||
}) | ||
.catch(() => { | ||
console.log('error') | ||
}) | ||
}} | ||
className={cn(!userId && 'bg-gray-400')} | ||
disabled={!userId} | ||
> | ||
Reset Password | ||
</Button> | ||
</div> | ||
</form> | ||
<div className="absolute bottom-6 flex items-center justify-center"> | ||
<Button | ||
onClick={() => showSignUp()} | ||
variant={'link'} | ||
className="h-5 w-fit p-0 py-2 text-xs text-gray-500" | ||
> | ||
Register now | ||
</Button> | ||
</div> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
'use client' | ||
|
||
import CodedangLogo from '@/public/codedang.svg' | ||
import useAuthModalStore from '@/stores/authModal' | ||
import useRecoverAccountModalStore from '@/stores/recoverAccountModal' | ||
import Image from 'next/image' | ||
import { IoMdArrowBack } from 'react-icons/io' | ||
import FindUserId from './FindUserId' | ||
import ResetPassword from './ResetPassword' | ||
import ResetPasswordEmailVerify from './ResetPasswordEmailVerify' | ||
|
||
export default function RecoverAccount() { | ||
const { showSignIn } = useAuthModalStore((state) => state) | ||
const { modalPage, backModal } = useRecoverAccountModalStore((state) => state) | ||
|
||
return ( | ||
<div className="flex h-full flex-col items-center justify-center"> | ||
<button | ||
onClick={modalPage === 0 ? showSignIn : backModal} | ||
className="absolute left-4 top-4 h-4 w-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 dark:ring-offset-gray-950 dark:focus:ring-gray-300 dark:data-[state=open]:bg-gray-800 dark:data-[state=open]:text-gray-400" | ||
> | ||
<IoMdArrowBack /> | ||
</button> | ||
|
||
<Image | ||
className="absolute left-8 top-10" | ||
src={CodedangLogo} | ||
alt="codedang" | ||
width={70} | ||
/> | ||
|
||
{modalPage === 0 && <FindUserId />} | ||
{modalPage === 1 && <ResetPasswordEmailVerify />} | ||
{modalPage === 2 && <ResetPassword />} | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { Button } from '@/components/ui/button' | ||
import { Input } from '@/components/ui/input' | ||
import { cn, fetcher } from '@/lib/utils' | ||
import useRecoverAccountModalStore from '@/stores/recoverAccountModal' | ||
import { zodResolver } from '@hookform/resolvers/zod' | ||
import { useState } from 'react' | ||
import { useForm } from 'react-hook-form' | ||
import { FaEye, FaEyeSlash } from 'react-icons/fa' | ||
import { toast } from 'sonner' | ||
import { z } from 'zod' | ||
|
||
interface ResetPasswordInput { | ||
password: string | ||
passwordAgain: string | ||
} | ||
|
||
const schema = z | ||
.object({ | ||
password: z | ||
.string() | ||
.min(8) | ||
.refine((data) => { | ||
const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ | ||
return !invalidPassword.test(data) | ||
}), | ||
passwordAgain: z.string().min(8) | ||
}) | ||
.refine( | ||
(data: { password: string; passwordAgain: string }) => | ||
data.password === data.passwordAgain, | ||
{ | ||
path: ['passwordAgain'] | ||
} | ||
) | ||
|
||
export default function ResetPassword() { | ||
const { | ||
handleSubmit, | ||
register, | ||
trigger, | ||
formState: { errors, isValid } | ||
} = useForm<ResetPasswordInput>({ | ||
resolver: zodResolver(schema) | ||
}) | ||
const [passwordShow, setPasswordShow] = useState<boolean>(false) | ||
const [passwordAgainShow, setPasswordAgainShow] = useState<boolean>(false) | ||
const [inputFocus, setInputFocus] = useState<number>(0) | ||
const { formData } = useRecoverAccountModalStore((state) => state) | ||
|
||
const onSubmit = async (data: ResetPasswordInput) => { | ||
try { | ||
const response = await fetcher.patch('user/password-reset', { | ||
headers: formData.headers, | ||
json: { | ||
newPassword: data.password | ||
} | ||
}) | ||
if (response.ok) { | ||
document.getElementById('closeDialog')?.click() | ||
toast.success('Password reset successfully') | ||
} | ||
} catch { | ||
toast.error('Password reset failed') | ||
} | ||
} | ||
|
||
return ( | ||
<form | ||
onSubmit={handleSubmit(onSubmit)} | ||
className="flex w-full flex-col gap-1 px-2" | ||
> | ||
<p className="text-primary mb-4 text-left text-xl font-bold"> | ||
Reset Password | ||
</p> | ||
<div className="flex flex-col gap-4"> | ||
<div className="flex flex-col gap-1"> | ||
<div className="flex justify-between gap-2"> | ||
<Input | ||
placeholder="Password" | ||
{...register('password', { | ||
onChange: () => trigger('password') | ||
})} | ||
type={passwordShow ? 'text' : 'password'} | ||
onFocus={() => { | ||
setInputFocus(0) | ||
}} | ||
/> | ||
<span | ||
className="flex items-center" | ||
onClick={() => setPasswordShow(!passwordShow)} | ||
> | ||
{passwordShow ? ( | ||
<FaEye className="text-gray-400" /> | ||
) : ( | ||
<FaEyeSlash className="text-gray-400" /> | ||
)} | ||
</span> | ||
</div> | ||
{inputFocus === 0 && ( | ||
<div | ||
className={cn( | ||
errors.password ? 'text-red-500' : 'text-gray-500', | ||
'text-xs' | ||
)} | ||
> | ||
<ul className="pl-4"> | ||
<li className="list-disc"> | ||
Your password must be at least 8 characters | ||
</li> | ||
<li>and include two of the followings:</li> | ||
<li>Capital letters, Small letters, or Numbers</li> | ||
</ul> | ||
</div> | ||
)} | ||
</div> | ||
|
||
<div className="flex flex-col gap-1"> | ||
<div className="flex justify-between gap-2"> | ||
<Input | ||
{...register('passwordAgain', { | ||
onChange: () => trigger('passwordAgain') | ||
})} | ||
placeholder="Re-enter password" | ||
type={passwordAgainShow ? 'text' : 'password'} | ||
onFocus={() => { | ||
setInputFocus(1) | ||
}} | ||
/> | ||
<span | ||
className="flex items-center" | ||
onClick={() => setPasswordAgainShow(!passwordAgainShow)} | ||
> | ||
{passwordAgainShow ? ( | ||
<FaEye className="text-gray-400" /> | ||
) : ( | ||
<FaEyeSlash className="text-gray-400" /> | ||
)} | ||
</span> | ||
</div> | ||
{errors.passwordAgain && ( | ||
<p className="text-xs text-red-500">Incorrect</p> | ||
)} | ||
</div> | ||
<Button | ||
disabled={!isValid} | ||
className={cn(!isValid && 'bg-gray-400')} | ||
type="submit" | ||
> | ||
Save | ||
</Button> | ||
</div> | ||
</form> | ||
) | ||
} |
Oops, something went wrong.