Skip to content

Commit

Permalink
feat(fe): implement recover account modal (#1526)
Browse files Browse the repository at this point in the history
* 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
B0XERCAT authored Mar 11, 2024
1 parent 4cafe55 commit 634e1b4
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 1 deletion.
12 changes: 12 additions & 0 deletions apps/frontend/components/auth/AuthModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import useAuthModalStore from '@/stores/authModal'
import { Transition } from '@headlessui/react'
import RecoverAccount from './RecoverAccount'
import SignIn from './SignIn'
import SignUp from './SignUp'

Expand Down Expand Up @@ -29,6 +30,17 @@ export default function AuthModal() {
>
<SignUp />
</Transition>
<Transition
show={currentModal === 'recoverAccount'}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<RecoverAccount />
</Transition>
</>
)
}
156 changes: 156 additions & 0 deletions apps/frontend/components/auth/FindUserId.tsx
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>
</>
)
}
37 changes: 37 additions & 0 deletions apps/frontend/components/auth/RecoverAccount.tsx
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>
)
}
154 changes: 154 additions & 0 deletions apps/frontend/components/auth/ResetPassword.tsx
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>
)
}
Loading

0 comments on commit 634e1b4

Please sign in to comment.