feat: register and otp integration
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 { FC, ReactElement } from 'react';
|
||||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
import { useRegisterHook } from '../../_hooks/use-register';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const { form, onSubmit, isLoading } = useRegisterHook();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
<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">
|
<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">
|
<h5 className="text-primary-500 font-medium">
|
||||||
Yosha~! Saatnya Bergabung dengan Dimentorin
|
Yosha~! Saatnya Bergabung dengan Dimentorin
|
||||||
</h5>
|
</h5>
|
||||||
<div className="grid grid-flow-row-dense my-7 lg:grid-cols-2 gap-2">
|
<form onSubmit={onSubmit}>
|
||||||
<InputField
|
<div className='my-7'>
|
||||||
label="First Name"
|
<ControlledInputField
|
||||||
size="lg"
|
label="Full name"
|
||||||
className="w-full"
|
size="lg"
|
||||||
placeholder="Nama Depan"
|
className="w-full"
|
||||||
/>
|
placeholder="Nama Lengkap"
|
||||||
<InputField
|
name={'fullname'}
|
||||||
label="Last Name"
|
control={form.control}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid grid-flow-row-dense my-7 lg:grid-cols-2 gap-2">
|
||||||
<InputField
|
<ControlledInputField
|
||||||
label="Password"
|
label="Email"
|
||||||
className="w-full"
|
size="lg"
|
||||||
size="lg"
|
className="w-full"
|
||||||
type="password"
|
placeholder="Contoh : yourname@mail.com"
|
||||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
type='email'
|
||||||
/>
|
control={form.control}
|
||||||
<h6 className="text-sm text-gray-500 mt-1">
|
name={'email'}
|
||||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
/>
|
||||||
</h6>
|
<ControlledInputField
|
||||||
<h6 className="text-sm text-gray-500 mt-2">
|
label="No Hp"
|
||||||
Tips membuat password yang OP:
|
size="lg"
|
||||||
</h6>
|
className="w-full"
|
||||||
<h6 className="text-sm text-gray-500">
|
placeholder="Contoh : 088877665544"
|
||||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
control={form.control}
|
||||||
<span role="img" aria-label="emoji">
|
name={'phone_number'}
|
||||||
⚡
|
/>
|
||||||
</span>
|
</div>
|
||||||
)
|
<ControlledInputField
|
||||||
</h6>
|
label="Password"
|
||||||
<h6 className="text-sm text-gray-500">
|
className="w-full"
|
||||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
size="lg"
|
||||||
ultimate!
|
type="password"
|
||||||
<span role="img" aria-label="emoji">
|
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||||
🔥
|
control={form.control}
|
||||||
</span>
|
name={'password'}
|
||||||
</h6>
|
/>
|
||||||
<h6 className="text-sm text-gray-500 mb-2">
|
<h6 className="text-sm text-gray-500 mt-1">
|
||||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||||
musuh!
|
</h6>
|
||||||
<span role="img" aria-label="emoji">
|
<h6 className="text-sm text-gray-500 mt-2">
|
||||||
🚨
|
Tips membuat password yang OP:
|
||||||
</span>
|
</h6>
|
||||||
</h6>
|
<h6 className="text-sm text-gray-500">
|
||||||
<InputField
|
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||||
label="Repeat Password"
|
<span role="img" aria-label="emoji">
|
||||||
className="w-full"
|
⚡
|
||||||
size="lg"
|
</span>
|
||||||
type="password"
|
)
|
||||||
placeholder="Ulangi password-mu, Senpai~!"
|
</h6>
|
||||||
/>
|
<h6 className="text-sm text-gray-500">
|
||||||
<Button className="xl:w-full mt-2">Linked Start !!!</Button>
|
- 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>
|
</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 { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.location.href = "/auth/login"
|
||||||
|
}, 10000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
<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">
|
<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">
|
<h4 className="mt-10 text-primary-500 font-medium text-xl">
|
||||||
Kamu akan memasuki isekai dalam 10 dtk
|
Kamu akan memasuki isekai dalam 10 dtk
|
||||||
</h4>
|
</h4>
|
||||||
<Button className="gap-3 mt-10" size="lg">
|
<a href="/auth/login">
|
||||||
Masuk Isekai
|
<Button className="gap-3 mt-10" size="lg">
|
||||||
<ArrowRightOutlined />
|
Masuk Isekai
|
||||||
</Button>
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ const mappingPublicRoutes = [
|
|||||||
'/',
|
'/',
|
||||||
'/auth/login',
|
'/auth/login',
|
||||||
'/auth/forgot',
|
'/auth/forgot',
|
||||||
|
'/auth/forgot/otp',
|
||||||
'/auth/register',
|
'/auth/register',
|
||||||
|
'/auth/register/otp',
|
||||||
|
'/auth/register/success',
|
||||||
'/auth/new-password',
|
'/auth/new-password',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export const postVerifyEmail = async (
|
|||||||
): Promise<TResponseMessage> => {
|
): Promise<TResponseMessage> => {
|
||||||
const { data } = await api({
|
const { data } = await api({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/auth/verify',
|
url: '/auth/verify-email',
|
||||||
data: payload,
|
data: {otp: parseInt(payload.otp), email: payload.email},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,3 +57,12 @@ export const authRegisterSchema = z
|
|||||||
message: 'Password dan Konfirmasi Password harus sama',
|
message: 'Password dan Konfirmasi Password harus sama',
|
||||||
path: ['confirm_password'],
|
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 TRegisterResponse = TResponseDetail<TResponseMessage>;
|
||||||
|
|
||||||
export type TVerifyOtpRequest = {
|
export type TVerifyOtpRequest = {
|
||||||
otp: number;
|
otp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyEmailRequest = {
|
export type TVerifyEmailRequest = {
|
||||||
email: string;
|
email: string;
|
||||||
otp: number;
|
otp: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
// eslint-disable-next-line @nx/enforce-module-boundaries
|
// eslint-disable-next-line @nx/enforce-module-boundaries
|
||||||
import { Input } from "@imphnen-frontend-service/ui/atoms";
|
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<
|
type TForgotStepProps = Omit<
|
||||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||||
'size' | 'type'
|
'size' | 'type'
|
||||||
> & {
|
> & {
|
||||||
|
setChange?: React.Dispatch<React.SetStateAction<any[]>>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OtpForm: FC<TForgotStepProps> = ({
|
export const OtpForm: FC<TForgotStepProps> = ({
|
||||||
step = 1,
|
step = 1,
|
||||||
|
setChange = (x: string[]) => {return},
|
||||||
...rest
|
...rest
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
const [cell, setCell] = useState(new Array(6).fill(""))
|
const [cell, setCell] = useState(new Array(6).fill(""))
|
||||||
const inputRef = useRef([])
|
const inputRef = useRef([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChange(cell)
|
||||||
|
}, [cell, setChange])
|
||||||
|
|
||||||
const handleChange = (index: number, e: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (index: number, e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
if (!(/^[0-9]?$/.test(value))) return;
|
if (!(/^[0-9]?$/.test(value))) return;
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ export * from './use-query-state';
|
|||||||
export * from './use-auth-store';
|
export * from './use-auth-store';
|
||||||
export * from './use-modal-login';
|
export * from './use-modal-login';
|
||||||
export * from './use-session';
|
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