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
This commit is contained in:
@@ -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<TVerifyOtpRequest>({
|
||||
resolver: zodResolver(verifyEmailSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const { otp, isLoading } = useOtp();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => otp(data));
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading
|
||||
};
|
||||
};
|
||||
@@ -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<TRegisterRequest>({
|
||||
resolver: zodResolver(authRegisterSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const { register, isLoading } = useRegister();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => register(data));
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -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 (
|
||||
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
||||
<div className='bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6'>
|
||||
<RegisterResetBanner />
|
||||
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
|
||||
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Verifikasi Email</h3>
|
||||
<h5 className='mt-2 text-xl font-medium text-primary-500'>Yeay~! Pesan dari dunia lain sudah dikirimkan ke {searchParams.get("email")}</h5>
|
||||
<form onSubmit={onSubmit} className='mt-7'>
|
||||
<ControlledInputField
|
||||
label="OTP"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="XXXXXX"
|
||||
max={6}
|
||||
name={'otp'}
|
||||
maxLength={6}
|
||||
control={form.control}
|
||||
/>
|
||||
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -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 (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white xl:min-w-[1130px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
@@ -24,83 +26,83 @@ export const Components: FC = (): ReactElement => {
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Yosha~! Saatnya Bergabung dengan Dimentorin
|
||||
</h5>
|
||||
<div className="grid grid-flow-row-dense my-7 lg:grid-cols-2 gap-2">
|
||||
<InputField
|
||||
label="First Name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Depan"
|
||||
/>
|
||||
<InputField
|
||||
label="Last Name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Belakang"
|
||||
/>
|
||||
<InputField
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Contoh : yourname@mail.com"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="font-medium">OTP Code</h4>
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
placeholder="Kode OTP"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button size="sm" className="mt-[30px]">
|
||||
Kirim OTP
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='my-7'>
|
||||
<ControlledInputField
|
||||
label="Full name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Lengkap"
|
||||
name={'fullname'}
|
||||
control={form.control}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InputField
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||
/>
|
||||
<h6 className="text-sm text-gray-500 mt-1">
|
||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mt-2">
|
||||
Tips membuat password yang OP:
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||
<span role="img" aria-label="emoji">
|
||||
⚡
|
||||
</span>
|
||||
)
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||
ultimate!
|
||||
<span role="img" aria-label="emoji">
|
||||
🔥
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mb-2">
|
||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||
musuh!
|
||||
<span role="img" aria-label="emoji">
|
||||
🚨
|
||||
</span>
|
||||
</h6>
|
||||
<InputField
|
||||
label="Repeat Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Ulangi password-mu, Senpai~!"
|
||||
/>
|
||||
<Button className="xl:w-full mt-2">Linked Start !!!</Button>
|
||||
<div className="grid grid-flow-row-dense my-7 lg:grid-cols-2 gap-2">
|
||||
<ControlledInputField
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Contoh : yourname@mail.com"
|
||||
type='email'
|
||||
control={form.control}
|
||||
name={'email'}
|
||||
/>
|
||||
<ControlledInputField
|
||||
label="No Hp"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Contoh : 088877665544"
|
||||
control={form.control}
|
||||
name={'phone_number'}
|
||||
/>
|
||||
</div>
|
||||
<ControlledInputField
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||
control={form.control}
|
||||
name={'password'}
|
||||
/>
|
||||
<h6 className="text-sm text-gray-500 mt-1">
|
||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mt-2">
|
||||
Tips membuat password yang OP:
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||
<span role="img" aria-label="emoji">
|
||||
⚡
|
||||
</span>
|
||||
)
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||
ultimate!
|
||||
<span role="img" aria-label="emoji">
|
||||
🔥
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mb-2">
|
||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||
musuh!
|
||||
<span role="img" aria-label="emoji">
|
||||
🚨
|
||||
</span>
|
||||
</h6>
|
||||
<ControlledInputField
|
||||
label="Repeat Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Ulangi password-mu, Senpai~!"
|
||||
control={form.control}
|
||||
name={"confirm_password"}
|
||||
/>
|
||||
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white xl:min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
@@ -41,10 +47,12 @@ export const Components: FC = (): ReactElement => {
|
||||
<h4 className="mt-10 text-primary-500 font-medium text-xl">
|
||||
Kamu akan memasuki isekai dalam 10 dtk
|
||||
</h4>
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
<a href="/auth/login">
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,10 @@ const mappingPublicRoutes = [
|
||||
'/',
|
||||
'/auth/login',
|
||||
'/auth/forgot',
|
||||
'/auth/forgot/otp',
|
||||
'/auth/register',
|
||||
'/auth/register/otp',
|
||||
'/auth/register/success',
|
||||
'/auth/new-password',
|
||||
];
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ export const postVerifyEmail = async (
|
||||
): Promise<TResponseMessage> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
@@ -20,10 +20,10 @@ export type TRegisterRequest = z.infer<typeof authRegisterSchema>;
|
||||
export type TRegisterResponse = TResponseDetail<TResponseMessage>;
|
||||
|
||||
export type TVerifyOtpRequest = {
|
||||
otp: number;
|
||||
otp: string;
|
||||
};
|
||||
|
||||
export type TVerifyEmailRequest = {
|
||||
email: string;
|
||||
otp: number;
|
||||
otp: string;
|
||||
};
|
||||
|
||||
@@ -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<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
'size' | 'type'
|
||||
> & {
|
||||
|
||||
setChange?: React.Dispatch<React.SetStateAction<any[]>>
|
||||
};
|
||||
|
||||
export const OtpForm: FC<TForgotStepProps> = ({
|
||||
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<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
if (!(/^[0-9]?$/.test(value))) return;
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user