diff options
Diffstat (limited to 'src-migrate')
| -rw-r--r-- | src-migrate/common/components/elements/Modal.tsx | 4 | ||||
| -rw-r--r-- | src-migrate/common/types/auth.ts | 11 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/components/FormEmail.tsx | 91 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/components/FormOTP.tsx | 112 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/components/FormToken.tsx | 24 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/index.tsx | 4 | ||||
| -rw-r--r-- | src-migrate/modules/register/components/Form.tsx | 35 | ||||
| -rw-r--r-- | src-migrate/modules/register/index.tsx | 7 | ||||
| -rw-r--r-- | src-migrate/services/auth.ts | 14 |
9 files changed, 278 insertions, 24 deletions
diff --git a/src-migrate/common/components/elements/Modal.tsx b/src-migrate/common/components/elements/Modal.tsx index 91421251..8e488a3a 100644 --- a/src-migrate/common/components/elements/Modal.tsx +++ b/src-migrate/common/components/elements/Modal.tsx @@ -1,5 +1,6 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import { AnimatePresence, motion } from "framer-motion" +import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import { useWindowSize } from "usehooks-ts"; @@ -23,6 +24,7 @@ const Modal = ({ className, mode }: Props) => { + const router = useRouter() const { width } = useWindowSize() const [rendered, setRendered] = useState<boolean>(false) @@ -49,7 +51,7 @@ const Modal = ({ } return rendered && ReactDOM.createPortal( - <AnimatePresence> + <AnimatePresence key={router.asPath}> {active && ( <motion.div className="overlay" diff --git a/src-migrate/common/types/auth.ts b/src-migrate/common/types/auth.ts index 3d9ffee4..ca7b562a 100644 --- a/src-migrate/common/types/auth.ts +++ b/src-migrate/common/types/auth.ts @@ -27,7 +27,7 @@ export type RegisterProps = { export type RegisterResApiProps = { register: boolean; - reason: 'EMAIL_USED' | null; + reason: 'EMAIL_USED' | 'NOT_ACTIVE' | null; }; type ActivationResProps = { @@ -51,3 +51,12 @@ export type ActivationOtpProps = { export type ActivationOtpResApiProps = ActivationResProps & { reason: 'INVALID_OTP' | null; }; + +export type ActivationReqProps = { + email: string; +}; + +export type ActivationReqResApiProps = { + activation_request: boolean; + reason: 'NOT_FOUND' | 'ACTIVE' | null; +}; diff --git a/src-migrate/modules/account-activation/components/FormEmail.tsx b/src-migrate/modules/account-activation/components/FormEmail.tsx index f16ab93f..cd917bc9 100644 --- a/src-migrate/modules/account-activation/components/FormEmail.tsx +++ b/src-migrate/modules/account-activation/components/FormEmail.tsx @@ -1,6 +1,95 @@ +import { Alert, AlertIcon } from "@chakra-ui/react" +import Link from "next/link" +import { useRouter } from "next/router" +import { ChangeEvent, useEffect, useState } from "react" +import { useMutation } from "react-query" +import Modal from "~/common/components/elements/Modal" +import { useRegisterStore } from "~/common/stores/useRegisterStore" +import { ActivationReqProps } from "~/common/types/auth" +import { activationReq } from "~/services/auth" + const FormEmail = () => { + const router = useRouter() + const { query } = router + const [active, setActive] = useState<boolean>(false) + const [isBtnDisabled, setIsBtnDisabled] = useState<boolean>(true) + const [email, setEmail] = useState<string>('') + + const { form } = useRegisterStore() + + useEffect(() => { + if (form) setEmail(form?.email) + }, [form]) + + useEffect(() => { + setIsBtnDisabled(email === '') + }, [email]) + + useEffect(() => { + setActive(query?.activation === 'email') + }, [query.activation]) + + const mutation = useMutation({ + mutationFn: (data: ActivationReqProps) => activationReq(data) + }) + + const handleSubmit = (e: ChangeEvent<HTMLFormElement>) => { + e.preventDefault() + mutation.data = undefined + mutation.mutate({ email }) + } + + useEffect(() => { + if (mutation.data?.activation_request === true) { + const urlParams = new URLSearchParams({ + activation: 'otp', + email + }) + router.push(`${router.route}?${urlParams}`) + } + }, [mutation.data?.activation_request, router, email]) + return ( - <div>FormEmail</div> + <Modal active={active} mode="desktop" close={() => router.push(router.route)} title="Aktivasi Akun"> + <form onSubmit={handleSubmit} className="py-3 flex flex-col items-center gap-y-4"> + { + mutation.data !== undefined && + !mutation.data?.activation_request && + ( + <Alert status="warning"> + <AlertIcon /> + {mutation.data?.reason === 'ACTIVE' && 'Akun sudah aktif.'} + {mutation.data?.reason === 'NOT_FOUND' && 'Akun tidak ditemukan.'} + + {mutation.data?.reason === 'ACTIVE' && ( + <Link href="/login" className="ml-1 text-yellow-700 underline">Klik untuk masuk akun anda</Link> + )} + + {mutation.data?.reason === 'NOT_FOUND' && ( + <Link href="/register" className="ml-1 text-yellow-700 underline">Klik untuk daftar akun baru</Link> + )} + </Alert> + ) + } + + <input + type="text" + className="form-input w-full" + placeholder="Masukan alamat email anda" + autoComplete="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + autoFocus + /> + <button + type="submit" + className="btn-yellow w-full" + disabled={isBtnDisabled || mutation.isLoading} + > + {mutation.isLoading ? 'Loading...' : 'Kirim Aktivasi'} + </button> + </form> + </Modal> ) } diff --git a/src-migrate/modules/account-activation/components/FormOTP.tsx b/src-migrate/modules/account-activation/components/FormOTP.tsx index 24e9e7f6..e38fa8fe 100644 --- a/src-migrate/modules/account-activation/components/FormOTP.tsx +++ b/src-migrate/modules/account-activation/components/FormOTP.tsx @@ -1,23 +1,127 @@ -import { HStack, PinInput, PinInputField } from "@chakra-ui/react" +import { HStack, PinInput, PinInputField, Spinner } from "@chakra-ui/react" +import Link from "next/link" +import { useRouter } from "next/router" +import { useEffect, useState } from "react" +import { useMutation } from "react-query" +import { useCountdown } from "usehooks-ts" import Modal from '~/common/components/elements/Modal' +import { ActivationOtpProps, ActivationReqProps } from "~/common/types/auth" +import { activationReq, activationUserOTP } from "~/services/auth" const FormOTP = () => { + const router = useRouter() + const { query } = router + const [active, setActive] = useState<boolean>(false) + const [email, setEmail] = useState<string>('') + const [otp, setOtp] = useState<string>('') + const [count, { startCountdown, resetCountdown }] = useCountdown({ + countStart: 60 * 3, + intervalMs: 1000, + }) + + useEffect(() => { + setEmail((query?.email || '') as string) + }, [query?.email]) + + useEffect(() => { + setActive(query?.activation === 'otp') + }, [query.activation]) + + useEffect(() => { + !!active && startCountdown() + }, [active, startCountdown]) + + const mutationActivationReq = useMutation({ + mutationFn: (data: ActivationReqProps) => activationReq(data), + onSuccess: () => { + resetCountdown() + startCountdown() + } + }) + + const mutationActivation = useMutation({ + mutationFn: (data: ActivationOtpProps) => activationUserOTP(data) + }) + + useEffect(() => { + if (otp.length === 4 && !mutationActivation.data?.activation) { + mutationActivation.mutate({ email, otp }) + } + //eslint-disable-next-line + }, [otp]) + + // TODO: Save user to local storage + return ( - <Modal active={false} className="w-10/12 md:w-fit px-10" mode="desktop"> + <Modal active={active} className="w-10/12 md:w-fit px-10" mode="desktop"> <div className="pb-8 flex flex-col items-center"> <div className="text-title-sm font-medium mb-4">Masukan Kode OTP</div> <HStack> - <PinInput size='lg' otp> + <PinInput size='lg' otp onChange={(val) => setOtp(val)}> <PinInputField autoFocus /> <PinInputField /> <PinInputField /> <PinInputField /> </PinInput> </HStack> - <div className="mt-4">Kode OTP dikirimkan ke email anda</div> + <div className="mt-4 text-center"> + {mutationActivation.isLoading && <Spinner size='sm' />} + + {!mutationActivation.isLoading && + mutationActivation.data?.reason === 'INVALID_OTP' && + ( + <span className="text-red-700">Mohon maaf kode OTP yand anda masukan salah</span> + ) + } + + {mutationActivation.data?.activation && ( + <span className="text-success-700">Akun anda berhasil diaktifkan, selamat berbelanja di Indoteknik.</span> + )} + </div> + + {!mutationActivation.data?.activation && ( + <div className="mt-4 text-center text-gray-700"> + Kode verifikasi telah dikirimkan ke alamat email <span className="font-medium">{email}</span>. Silakan periksa kotak masuk atau folder spam. + </div> + )} + + <div className="mt-4"> + {!mutationActivation.data?.activation && ( + <> + {count > 0 && timeFormat(count)} + + {!mutationActivation.data?.activation && count == 0 && ( + <button + className="text-red-700 underline" + type="button" + onClick={() => mutationActivationReq.mutate({ email })} + disabled={mutationActivationReq.isLoading} + > + {mutationActivationReq.isLoading ? <Spinner size='sm' /> : 'Kirim Ulang'} + </button> + )} + </> + )} + + {mutationActivation.data?.activation && ( + <Link href='/' className="text-success-700 underline">Kembali ke halaman utama</Link> + )} + + + </div> </div> </Modal> ) } +const timeFormat = (time: number): string => { + const minute = Math.floor(time / 60); + const second = time % 60; + + const minuteFormat = minute < 10 ? `0${minute}` : `${minute}` + const secondFormat = second < 10 ? `0${second}` : `${second}` + + return `${minuteFormat}:${secondFormat}` +} + export default FormOTP
\ No newline at end of file diff --git a/src-migrate/modules/account-activation/components/FormToken.tsx b/src-migrate/modules/account-activation/components/FormToken.tsx index 041fbae8..6af24413 100644 --- a/src-migrate/modules/account-activation/components/FormToken.tsx +++ b/src-migrate/modules/account-activation/components/FormToken.tsx @@ -8,22 +8,12 @@ import Modal from "~/common/components/elements/Modal" import { ActivationTokenProps } from "~/common/types/auth" import { activationUserToken } from "~/services/auth" -type StatusProps = "success" | "error" | "loading"; - const FormToken = () => { const { query } = useRouter() - const [status, setStatus] = useState<StatusProps>("loading") const [active, setActive] = useState<boolean>(false) const mutation = useMutation({ mutationFn: (data: ActivationTokenProps) => activationUserToken(data), - onSuccess: (data) => { - if (data.activation === true) { - setStatus("success") - } else { - setStatus("error") - } - } }) useEffect(() => { @@ -34,19 +24,21 @@ const FormToken = () => { //eslint-disable-next-line }, [query.activation, query.token]) + // TODO: Save user to local storage + return ( <Modal active={active} mode="desktop"> <div className="pb-6 flex flex-col items-center gap-y-6"> - {status === 'loading' && ( + {mutation.isLoading && ( <> <Spinner size='lg' borderWidth='3px' /> <div className="text-h-sm">Mohon tunggu sedang memverifikasi akun</div> </> )} - {status !== 'loading' && ( + {!mutation.isLoading && ( <Alert - status={status} + status={mutation.data?.activation ? 'success' : 'error'} flexDirection="column" alignItems="center" justifyContent="center" @@ -56,17 +48,17 @@ const FormToken = () => { > <AlertIcon boxSize="40px" mr={0} /> <AlertTitle className="mt-4 mb-1 text-h-sm"> - Aktivasi akun {status === 'success' ? 'berhasil' : 'gagal'} + Aktivasi akun {mutation.data?.activation ? 'berhasil' : 'gagal'} </AlertTitle> <AlertDescription maxWidth="sm"> - {status === 'success' && ( + {mutation.data?.activation && ( <> Akun anda berhasil diaktifkan, selamat berbelanja di Indoteknik. <Link href='/' className="block mt-8 text-success-700 underline">Kembali ke halaman utama</Link> </> )} - {status === 'error' && mutation.data?.reason === 'INVALID_TOKEN' && ( + {!mutation.data?.activation && mutation.data?.reason === 'INVALID_TOKEN' && ( <> Token sudah kadaluwarsa, silahkan coba kembali. <Link href='/register?activation=email' className="block mt-8 text-red-700 underline">Aktivasi Akun</Link> diff --git a/src-migrate/modules/account-activation/index.tsx b/src-migrate/modules/account-activation/index.tsx index edcc6652..97c96953 100644 --- a/src-migrate/modules/account-activation/index.tsx +++ b/src-migrate/modules/account-activation/index.tsx @@ -1,10 +1,14 @@ import { useRouter } from "next/router" import FormToken from "./components/FormToken" +import FormEmail from "./components/FormEmail" +import FormOTP from "./components/FormOTP" const AccountActivation = () => { return ( <> + <FormEmail /> <FormToken /> + <FormOTP /> </> ) } diff --git a/src-migrate/modules/register/components/Form.tsx b/src-migrate/modules/register/components/Form.tsx index fc1567ab..3227a549 100644 --- a/src-migrate/modules/register/components/Form.tsx +++ b/src-migrate/modules/register/components/Form.tsx @@ -1,10 +1,13 @@ -import { ChangeEvent } from "react"; +import { ChangeEvent, useEffect } from "react"; import { useMutation } from "react-query"; import { useRegisterStore } from "~/common/stores/useRegisterStore"; import { RegisterProps } from "~/common/types/auth"; import { registerUser } from "~/services/auth"; import TermCondition from "./TermCondition"; import FormCaptcha from "./FormCaptcha"; +import { useRouter } from "next/router"; +import { UseToastOptions, useToast } from "@chakra-ui/react"; +import Link from "next/link"; const Form = () => { const { @@ -14,6 +17,8 @@ const Form = () => { isValidCaptcha, updateForm, } = useRegisterStore() + const router = useRouter() + const toast = useToast() const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { const { name, value } = event.target; @@ -30,7 +35,35 @@ const Form = () => { const response = await mutation.mutateAsync(form) if (response?.register === true) { + const urlParams = new URLSearchParams({ + activation: 'otp', + email: form.email + }) + router.push(`${router.route}?${urlParams}`) + } + + const toastProps: UseToastOptions = { + duration: 5000, + isClosable: true + } + switch (response?.reason) { + case 'EMAIL_USED': + toast({ + ...toastProps, + title: 'Email sudah digunakan', + status: 'warning' + }) + break; + case 'NOT_ACTIVE': + const activationUrl = `${router.route}?activation=email` + toast({ + ...toastProps, + title: 'Akun belum aktif', + description: <>Akun sudah terdaftar namun belum aktif. <Link href={activationUrl} className="underline">Klik untuk aktivasi akun</Link></>, + status: 'warning' + }) + break } } diff --git a/src-migrate/modules/register/index.tsx b/src-migrate/modules/register/index.tsx index 6325ee09..00931284 100644 --- a/src-migrate/modules/register/index.tsx +++ b/src-migrate/modules/register/index.tsx @@ -32,6 +32,13 @@ const Register = () => { Masuk </Link> </div> + + <div className='text-gray_r-11 mt-4 text-center md:text-left'> + Akun anda belum aktif?{' '} + <Link href='/register?activation=email' className='inline font-medium text-danger-500'> + Aktivasi + </Link> + </div> </section> <section className="my-10 md:my-0"> diff --git a/src-migrate/services/auth.ts b/src-migrate/services/auth.ts index feaabec8..a5d02754 100644 --- a/src-migrate/services/auth.ts +++ b/src-migrate/services/auth.ts @@ -6,6 +6,8 @@ import { ActivationTokenResApiProps, ActivationOtpProps, ActivationOtpResApiProps, + ActivationReqProps, + ActivationReqResApiProps, } from '~/common/types/auth'; const BASE_PATH = '/api/v1/user'; @@ -37,3 +39,15 @@ export const activationUserOTP = async ( return response; }; + +export const activationReq = async ( + params: ActivationReqProps +): Promise<ActivationReqResApiProps> => { + const response = await odooApi( + 'POST', + `${BASE_PATH}/activation-request`, + params + ); + + return response; +}; |
