From aaf5b4262dc977f0a7d676d5d28cb0b939b9c19e Mon Sep 17 00:00:00 2001 From: Naufal Azmi <109430358+BoboiAzumi@users.noreply.github.com> Date: Mon, 26 May 2025 20:49:57 +0700 Subject: [PATCH] Register and OTP (#31) * feat: responsive * fix: success register * feat: auth login responsive && auth login implementation * fix: login page button disabled & register page * fix: conflict * fix: button disabled when loading * feat: register and otp integration * Update middleware.ts --- apps/dimentorin/src/app/_hooks/use-otp.ts | 24 +++ .../dimentorin/src/app/_hooks/use-register.ts | 24 +++ .../src/app/auth/register/otp/layout.tsx | 12 ++ .../src/app/auth/register/otp/page.tsx | 37 ++++ .../dimentorin/src/app/auth/register/page.tsx | 160 +++++++++--------- .../src/app/auth/register/success/page.tsx | 18 +- apps/dimentorin/src/middleware.ts | 3 + libs/service/src/api/auth/index.ts | 4 +- libs/service/src/schemas/auth/index.ts | 9 + libs/service/src/types/auth/index.ts | 4 +- libs/ui/src/molecules/otp-form/otp-form.tsx | 9 +- libs/utils/src/hooks/index.ts | 2 + libs/utils/src/hooks/use-otp.ts | 29 ++++ libs/utils/src/hooks/use-register.ts | 28 +++ 14 files changed, 273 insertions(+), 90 deletions(-) create mode 100644 apps/dimentorin/src/app/_hooks/use-otp.ts create mode 100644 apps/dimentorin/src/app/auth/register/otp/layout.tsx create mode 100644 apps/dimentorin/src/app/auth/register/otp/page.tsx create mode 100644 libs/utils/src/hooks/use-otp.ts create mode 100644 libs/utils/src/hooks/use-register.ts diff --git a/apps/dimentorin/src/app/_hooks/use-otp.ts b/apps/dimentorin/src/app/_hooks/use-otp.ts new file mode 100644 index 0000000..30ffe14 --- /dev/null +++ b/apps/dimentorin/src/app/_hooks/use-otp.ts @@ -0,0 +1,24 @@ +import { useForm } from 'react-hook-form'; +import { + TVerifyOtpRequest, + verifyEmailSchema, +} from '@imphnen-frontend-service/service'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useOtp } from '@imphnen-frontend-service/utils'; + +export const useOtpHook = () => { + const form = useForm({ + resolver: zodResolver(verifyEmailSchema), + mode: 'all', + }); + + const { otp, isLoading } = useOtp(); + + const onSubmit = form.handleSubmit((data) => otp(data)); + + return { + form, + onSubmit, + isLoading + }; +}; \ No newline at end of file diff --git a/apps/dimentorin/src/app/_hooks/use-register.ts b/apps/dimentorin/src/app/_hooks/use-register.ts index e69de29..beb927a 100644 --- a/apps/dimentorin/src/app/_hooks/use-register.ts +++ b/apps/dimentorin/src/app/_hooks/use-register.ts @@ -0,0 +1,24 @@ +import { useForm } from 'react-hook-form'; +import { + authRegisterSchema, + TRegisterRequest, +} from '@imphnen-frontend-service/service'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useRegister } from '@imphnen-frontend-service/utils'; + +export const useRegisterHook = () => { + const form = useForm({ + resolver: zodResolver(authRegisterSchema), + mode: 'all', + }); + + const { register, isLoading } = useRegister(); + + const onSubmit = form.handleSubmit((data) => register(data)); + + return { + form, + onSubmit, + isLoading + }; +}; \ No newline at end of file diff --git a/apps/dimentorin/src/app/auth/register/otp/layout.tsx b/apps/dimentorin/src/app/auth/register/otp/layout.tsx new file mode 100644 index 0000000..2d8a74d --- /dev/null +++ b/apps/dimentorin/src/app/auth/register/otp/layout.tsx @@ -0,0 +1,12 @@ +import { FC, ReactElement } from 'react'; +import { Outlet } from 'react-router-dom'; + +export const AppLayout: FC = (): ReactElement => { + return ( +
+ +
+ ); +}; + +export default AppLayout; diff --git a/apps/dimentorin/src/app/auth/register/otp/page.tsx b/apps/dimentorin/src/app/auth/register/otp/page.tsx new file mode 100644 index 0000000..249ca78 --- /dev/null +++ b/apps/dimentorin/src/app/auth/register/otp/page.tsx @@ -0,0 +1,37 @@ +import { FC, ReactElement } from 'react'; +import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms"; +import { useOtpHook } from '../../../_hooks/use-otp'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { useSearchParams } from 'react-router-dom'; + +export const Components: FC = (): ReactElement => { + const { form, onSubmit, isLoading } = useOtpHook() + const [ searchParams ] = useSearchParams() + + return ( +
+
+ +
+

Verifikasi Email

+
Yeay~! Pesan dari dunia lain sudah dikirimkan ke {searchParams.get("email")}
+
+ + + +
+
+
+ ); +}; + +export default Components; \ No newline at end of file diff --git a/apps/dimentorin/src/app/auth/register/page.tsx b/apps/dimentorin/src/app/auth/register/page.tsx index 3de638d..bdf60a6 100644 --- a/apps/dimentorin/src/app/auth/register/page.tsx +++ b/apps/dimentorin/src/app/auth/register/page.tsx @@ -1,10 +1,12 @@ import { FC, ReactElement } from 'react'; -import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms'; -import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; -import { InputField } from '@imphnen-frontend-service/ui/molecules'; +import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; import { ArrowLeftOutlined } from '@ant-design/icons'; +import { useRegisterHook } from '../../_hooks/use-register'; export const Components: FC = (): ReactElement => { + const { form, onSubmit, isLoading } = useRegisterHook(); + return (
@@ -24,83 +26,83 @@ export const Components: FC = (): ReactElement => {
Yosha~! Saatnya Bergabung dengan Dimentorin
-
- - - -
-
-

OTP Code

- -
-
- -
+
+
+
-
- -
- "Senpai~! Pastikan password-mu sekuat pertahanan kastil!" -
-
- Tips membuat password yang OP: -
-
- - Minimal 8 karakter (semakin panjang, semakin power-up!{' '} - - ⚡ - - ) -
-
- - Campur huruf besar, kecil, angka, dan simbol untuk kombinasi - ultimate! - - 🔥 - -
-
- - Jangan pakai password yang gampang ditebak, nanti ketahuan - musuh! - - 🚨 - -
- - +
+ + +
+ +
+ "Senpai~! Pastikan password-mu sekuat pertahanan kastil!" +
+
+ Tips membuat password yang OP: +
+
+ - Minimal 8 karakter (semakin panjang, semakin power-up!{' '} + + ⚡ + + ) +
+
+ - Campur huruf besar, kecil, angka, dan simbol untuk kombinasi + ultimate! + + 🔥 + +
+
+ - Jangan pakai password yang gampang ditebak, nanti ketahuan + musuh! + + 🚨 + +
+ + +
diff --git a/apps/dimentorin/src/app/auth/register/success/page.tsx b/apps/dimentorin/src/app/auth/register/success/page.tsx index 4a59405..7c6ed5b 100644 --- a/apps/dimentorin/src/app/auth/register/success/page.tsx +++ b/apps/dimentorin/src/app/auth/register/success/page.tsx @@ -1,9 +1,15 @@ -import { FC, ReactElement } from 'react'; +import { FC, ReactElement, useEffect } from 'react'; import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms'; import { Button } from '@imphnen-frontend-service/ui/atoms'; import { ArrowRightOutlined } from '@ant-design/icons'; export const Components: FC = (): ReactElement => { + useEffect(() => { + setTimeout(() => { + document.location.href = "/auth/login" + }, 10000) + }, []) + return (
@@ -41,10 +47,12 @@ export const Components: FC = (): ReactElement => {

Kamu akan memasuki isekai dalam 10 dtk

- + + +
diff --git a/apps/dimentorin/src/middleware.ts b/apps/dimentorin/src/middleware.ts index e8c9cd2..c172d70 100644 --- a/apps/dimentorin/src/middleware.ts +++ b/apps/dimentorin/src/middleware.ts @@ -9,7 +9,10 @@ const mappingPublicRoutes = [ '/', '/auth/login', '/auth/forgot', + '/auth/forgot/otp', '/auth/register', + '/auth/register/otp', + '/auth/register/success', '/auth/new-password', ]; diff --git a/libs/service/src/api/auth/index.ts b/libs/service/src/api/auth/index.ts index 0241638..efec499 100644 --- a/libs/service/src/api/auth/index.ts +++ b/libs/service/src/api/auth/index.ts @@ -34,8 +34,8 @@ export const postVerifyEmail = async ( ): Promise => { const { data } = await api({ method: 'POST', - url: '/auth/verify', - data: payload, + url: '/auth/verify-email', + data: {otp: parseInt(payload.otp), email: payload.email}, }); return data; }; diff --git a/libs/service/src/schemas/auth/index.ts b/libs/service/src/schemas/auth/index.ts index 3f99256..436d2db 100644 --- a/libs/service/src/schemas/auth/index.ts +++ b/libs/service/src/schemas/auth/index.ts @@ -57,3 +57,12 @@ export const authRegisterSchema = z message: 'Password dan Konfirmasi Password harus sama', path: ['confirm_password'], }); + +export const verifyEmailSchema = z + .object({ + otp: z + .string({ + required_error: 'OTP tidak boleh kosong', + }) + .min(6, 'Masukkan kode 6 digit yang dikirimkan ke email') + }) \ No newline at end of file diff --git a/libs/service/src/types/auth/index.ts b/libs/service/src/types/auth/index.ts index 874ad0f..8471fcd 100644 --- a/libs/service/src/types/auth/index.ts +++ b/libs/service/src/types/auth/index.ts @@ -20,10 +20,10 @@ export type TRegisterRequest = z.infer; export type TRegisterResponse = TResponseDetail; export type TVerifyOtpRequest = { - otp: number; + otp: string; }; export type TVerifyEmailRequest = { email: string; - otp: number; + otp: string; }; diff --git a/libs/ui/src/molecules/otp-form/otp-form.tsx b/libs/ui/src/molecules/otp-form/otp-form.tsx index 6c62c7b..de1fb64 100644 --- a/libs/ui/src/molecules/otp-form/otp-form.tsx +++ b/libs/ui/src/molecules/otp-form/otp-form.tsx @@ -1,21 +1,26 @@ // eslint-disable-next-line @nx/enforce-module-boundaries import { Input } from "@imphnen-frontend-service/ui/atoms"; -import { ChangeEvent, DetailedHTMLProps, FC, InputHTMLAttributes, ReactElement, useRef, useState } from "react" +import { ChangeEvent, DetailedHTMLProps, FC, InputHTMLAttributes, ReactElement, useEffect, useRef, useState } from "react" type TForgotStepProps = Omit< DetailedHTMLProps, HTMLInputElement>, 'size' | 'type' > & { - + setChange?: React.Dispatch> }; export const OtpForm: FC = ({ step = 1, + setChange = (x: string[]) => {return}, ...rest }): ReactElement => { const [cell, setCell] = useState(new Array(6).fill("")) const inputRef = useRef([]) + useEffect(() => { + setChange(cell) + }, [cell, setChange]) + const handleChange = (index: number, e: ChangeEvent) => { const value = e.target.value if (!(/^[0-9]?$/.test(value))) return; diff --git a/libs/utils/src/hooks/index.ts b/libs/utils/src/hooks/index.ts index 2f20b90..40fb27d 100644 --- a/libs/utils/src/hooks/index.ts +++ b/libs/utils/src/hooks/index.ts @@ -2,3 +2,5 @@ export * from './use-query-state'; export * from './use-auth-store'; export * from './use-modal-login'; export * from './use-session'; +export * from './use-register'; +export * from './use-otp'; \ No newline at end of file diff --git a/libs/utils/src/hooks/use-otp.ts b/libs/utils/src/hooks/use-otp.ts new file mode 100644 index 0000000..7a1d0f8 --- /dev/null +++ b/libs/utils/src/hooks/use-otp.ts @@ -0,0 +1,29 @@ +import { TVerifyOtpRequest, usePostVerifyEmail } from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { useNavigate, useSearchParams } from 'react-router'; + +export const useOtp = () => { + const navigate = useNavigate(); + const { mutate, isPending } = usePostVerifyEmail(); + const [ searchParams ] = useSearchParams(); + + const otp = (payload: TVerifyOtpRequest) => { + mutate({ ...payload, email: searchParams.get("email") || "" }, { + onSuccess: (data) => { + toast.success('Berhasil Registrasi'); + navigate("/auth/register/success"); + }, + onError: (err) => { + toast.error( + err?.response?.data?.message ?? + 'Terjadi Kesalahan yang tidak diketahui' + ); + }, + }); + }; + + return { + otp, + isLoading: isPending, + }; +}; diff --git a/libs/utils/src/hooks/use-register.ts b/libs/utils/src/hooks/use-register.ts new file mode 100644 index 0000000..d1cd357 --- /dev/null +++ b/libs/utils/src/hooks/use-register.ts @@ -0,0 +1,28 @@ +import { TRegisterRequest, usePostRegister } from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { useNavigate } from 'react-router'; + +export const useRegister = () => { + const navigate = useNavigate(); + const { mutate, isPending } = usePostRegister(); + + const register = (payload: TRegisterRequest) => { + mutate(payload, { + onSuccess: (data) => { + toast.success('Berhasil Registrasi'); + navigate(`/auth/register/otp?email=${payload.email}`); + }, + onError: (err) => { + toast.error( + err?.response?.data?.message ?? + 'Terjadi Kesalahan yang tidak diketahui' + ); + }, + }); + }; + + return { + register, + isLoading: isPending, + }; +};