diff options
Diffstat (limited to 'src-migrate')
| -rw-r--r-- | src-migrate/common/components/elements/Modal.tsx | 18 | ||||
| -rw-r--r-- | src-migrate/common/components/elements/ReCaptcha.tsx | 17 | ||||
| -rw-r--r-- | src-migrate/common/stores/useRegisterStore.ts | 5 | ||||
| -rw-r--r-- | src-migrate/common/types/auth.ts | 31 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/components/FormEmail.tsx | 7 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/components/FormOTP.tsx | 23 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/components/FormToken.tsx | 83 | ||||
| -rw-r--r-- | src-migrate/modules/account-activation/index.tsx | 12 | ||||
| -rw-r--r-- | src-migrate/modules/register/components/Form.tsx | 53 | ||||
| -rw-r--r-- | src-migrate/modules/register/components/FormCaptcha.tsx | 14 | ||||
| -rw-r--r-- | src-migrate/modules/register/components/Register.tsx | 39 | ||||
| -rw-r--r-- | src-migrate/modules/register/components/TermCondition.tsx | 26 | ||||
| -rw-r--r-- | src-migrate/modules/register/index.tsx | 46 | ||||
| -rw-r--r-- | src-migrate/pages/register.tsx | 9 | ||||
| -rw-r--r-- | src-migrate/services/auth.ts | 35 |
15 files changed, 336 insertions, 82 deletions
diff --git a/src-migrate/common/components/elements/Modal.tsx b/src-migrate/common/components/elements/Modal.tsx index ad1fe51b..91421251 100644 --- a/src-migrate/common/components/elements/Modal.tsx +++ b/src-migrate/common/components/elements/Modal.tsx @@ -11,7 +11,8 @@ type Props = { active: boolean title?: string close?: () => void, - className?: string + className?: string, + mode?: "mobile" | "desktop" } const Modal = ({ @@ -19,11 +20,14 @@ const Modal = ({ active = false, title, close, - className + className, + mode }: Props) => { const { width } = useWindowSize() const [rendered, setRendered] = useState<boolean>(false) + mode = mode || width >= 768 ? "desktop" : "mobile" + useEffect(() => { setRendered(true) }, []) @@ -31,16 +35,16 @@ const Modal = ({ const modalClassNames = clsxm( "fixed bg-white max-h-[80vh] overflow-auto p-4 pt-0 z-[60] border-gray_r-6", { - "left-1/2 -translate-x-1/2 translate-y-1/2 bottom-1/2 md:w-1/4 lg:w-1/3 border rounded-xl": width >= 768, - "left-0 w-full border-t bottom-0 rounded-t-xl": width < 768 + "left-1/2 -translate-x-1/2 translate-y-1/2 bottom-1/2 w-11/12 md:w-1/4 lg:w-1/3 border rounded-xl": mode === 'desktop', + "left-0 w-full border-t bottom-0 rounded-t-xl": mode === 'mobile' }, className ) const variant = { - initial: { bottom: width >= 768 ? '45%' : '-100%', opacity: 0 }, - animate: { bottom: width >= 768 ? '50%' : 0, opacity: 1 }, - exit: { bottom: width >= 768 ? '55%' : '-100%', opacity: 0 }, + initial: { bottom: mode === 'desktop' ? '45%' : '-100%', opacity: 0 }, + animate: { bottom: mode === 'desktop' ? '50%' : 0, opacity: 1 }, + exit: { bottom: mode === 'desktop' ? '55%' : '-100%', opacity: 0 }, transition: { ease: 'linear', duration: 0.25 } } diff --git a/src-migrate/common/components/elements/ReCaptcha.tsx b/src-migrate/common/components/elements/ReCaptcha.tsx new file mode 100644 index 00000000..1bc31d90 --- /dev/null +++ b/src-migrate/common/components/elements/ReCaptcha.tsx @@ -0,0 +1,17 @@ +import ReCAPTCHA, { ReCAPTCHAProps } from "react-google-recaptcha" + +const GOOGLE_RECAPTCHA_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_GOOGLE || '' + +type Props = Omit<ReCAPTCHAProps, 'sitekey'> & { + sitekey?: string; +} + +const ReCaptcha = (props: Props) => { + const { sitekey, ...rest } = props + + return ( + <ReCAPTCHA sitekey={sitekey || GOOGLE_RECAPTCHA_KEY} {...rest} /> + ) +} + +export default ReCaptcha
\ No newline at end of file diff --git a/src-migrate/common/stores/useRegisterStore.ts b/src-migrate/common/stores/useRegisterStore.ts index fcd2cd8b..725bbfda 100644 --- a/src-migrate/common/stores/useRegisterStore.ts +++ b/src-migrate/common/stores/useRegisterStore.ts @@ -6,10 +6,12 @@ type State = { isValid: boolean; isCheckedTNC: boolean; isOpenTNC: boolean; + isValidCaptcha: boolean; }; type Action = { updateForm: (name: string, value: string) => void; + updateValidCaptcha: (value: boolean) => void; toggleCheckTNC: () => void; openTNC: () => void; closeTNC: () => void; @@ -21,10 +23,12 @@ export const useRegisterStore = create<State & Action>((set) => ({ name: '', email: '', password: '', + phone: '', }, isValid: false, isCheckedTNC: false, isOpenTNC: false, + isValidCaptcha: false, updateForm: (name, value) => set((state) => { const updatedForm = { ...state.form, [name]: value }; @@ -49,4 +53,5 @@ export const useRegisterStore = create<State & Action>((set) => ({ toggleCheckTNC: () => set((state) => ({ isCheckedTNC: !state.isCheckedTNC })), openTNC: () => set(() => ({ isOpenTNC: true })), closeTNC: () => set(() => ({ isOpenTNC: false })), + updateValidCaptcha: (value) => set(() => ({ isValidCaptcha: value })), })); diff --git a/src-migrate/common/types/auth.ts b/src-migrate/common/types/auth.ts index 63fac6e0..3d9ffee4 100644 --- a/src-migrate/common/types/auth.ts +++ b/src-migrate/common/types/auth.ts @@ -22,11 +22,32 @@ export type RegisterProps = { email: string; password: string; company: string; + phone: string; +}; + +export type RegisterResApiProps = { + register: boolean; + reason: 'EMAIL_USED' | null; +}; + +type ActivationResProps = { + activation: boolean; + user: AuthProps | null; +}; + +export type ActivationTokenProps = { + token: string; +}; + +export type ActivationTokenResApiProps = ActivationResProps & { + reason: 'INVALID_TOKEN' | null; +}; + +export type ActivationOtpProps = { + email: string; + otp: string; }; -export type RegisterApiProps = OdooApiProps & { - result: { - register: boolean; - reason?: 'EMAIL_USED'; - }; +export type ActivationOtpResApiProps = ActivationResProps & { + reason: 'INVALID_OTP' | null; }; diff --git a/src-migrate/modules/account-activation/components/FormEmail.tsx b/src-migrate/modules/account-activation/components/FormEmail.tsx new file mode 100644 index 00000000..f16ab93f --- /dev/null +++ b/src-migrate/modules/account-activation/components/FormEmail.tsx @@ -0,0 +1,7 @@ +const FormEmail = () => { + return ( + <div>FormEmail</div> + ) +} + +export default FormEmail
\ No newline at end of file diff --git a/src-migrate/modules/account-activation/components/FormOTP.tsx b/src-migrate/modules/account-activation/components/FormOTP.tsx new file mode 100644 index 00000000..24e9e7f6 --- /dev/null +++ b/src-migrate/modules/account-activation/components/FormOTP.tsx @@ -0,0 +1,23 @@ +import { HStack, PinInput, PinInputField } from "@chakra-ui/react" +import Modal from '~/common/components/elements/Modal' + +const FormOTP = () => { + return ( + <Modal active={false} 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> + <PinInputField autoFocus /> + <PinInputField /> + <PinInputField /> + <PinInputField /> + </PinInput> + </HStack> + <div className="mt-4">Kode OTP dikirimkan ke email anda</div> + </div> + </Modal> + ) +} + +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 new file mode 100644 index 00000000..041fbae8 --- /dev/null +++ b/src-migrate/modules/account-activation/components/FormToken.tsx @@ -0,0 +1,83 @@ +import { Alert, AlertDescription, AlertIcon, AlertTitle, Spinner } from "@chakra-ui/react" +import { useRouter } from "next/router" +import { useEffect, useState } from "react" +import Link from "next/link" +import { useMutation } from "react-query" + +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(() => { + if (query?.activation === 'token' && typeof query?.token === 'string') { + mutation.mutate({ token: query.token }) + setActive(true) + } + //eslint-disable-next-line + }, [query.activation, query.token]) + + return ( + <Modal active={active} mode="desktop"> + <div className="pb-6 flex flex-col items-center gap-y-6"> + {status === 'loading' && ( + <> + <Spinner size='lg' borderWidth='3px' /> + <div className="text-h-sm">Mohon tunggu sedang memverifikasi akun</div> + </> + )} + + {status !== 'loading' && ( + <Alert + status={status} + flexDirection="column" + alignItems="center" + justifyContent="center" + textAlign="center" + height="200px" + bg="transparent" + > + <AlertIcon boxSize="40px" mr={0} /> + <AlertTitle className="mt-4 mb-1 text-h-sm"> + Aktivasi akun {status === 'success' ? 'berhasil' : 'gagal'} + </AlertTitle> + <AlertDescription maxWidth="sm"> + {status === 'success' && ( + <> + 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' && ( + <> + Token sudah kadaluwarsa, silahkan coba kembali. + <Link href='/register?activation=email' className="block mt-8 text-red-700 underline">Aktivasi Akun</Link> + </> + )} + </AlertDescription> + </Alert> + )} + </div> + </Modal> + ) +} + +export default FormToken
\ No newline at end of file diff --git a/src-migrate/modules/account-activation/index.tsx b/src-migrate/modules/account-activation/index.tsx new file mode 100644 index 00000000..edcc6652 --- /dev/null +++ b/src-migrate/modules/account-activation/index.tsx @@ -0,0 +1,12 @@ +import { useRouter } from "next/router" +import FormToken from "./components/FormToken" + +const AccountActivation = () => { + return ( + <> + <FormToken /> + </> + ) +} + +export default AccountActivation
\ No newline at end of file diff --git a/src-migrate/modules/register/components/Form.tsx b/src-migrate/modules/register/components/Form.tsx index ac194b46..fc1567ab 100644 --- a/src-migrate/modules/register/components/Form.tsx +++ b/src-migrate/modules/register/components/Form.tsx @@ -3,9 +3,17 @@ 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"; const Form = () => { - const { form, isValid, isCheckedTNC, updateForm, toggleCheckTNC, openTNC } = useRegisterStore() + const { + form, + isValid, + isCheckedTNC, + isValidCaptcha, + updateForm, + } = useRegisterStore() const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { const { name, value } = event.target; @@ -20,8 +28,10 @@ const Form = () => { e.preventDefault() const response = await mutation.mutateAsync(form) - console.log(response); + if (response?.register === true) { + + } } return ( @@ -58,6 +68,20 @@ const Form = () => { </div> <div> + <label htmlFor='phone'>No Handphone</label> + + <input + type='tel' + id='phone' + name='phone' + className='form-input mt-3' + placeholder='08xxxxxxxx' + value={form.phone} + onChange={handleInputChange} + /> + </div> + + <div> <label htmlFor='email'>Alamat Email</label> <input @@ -68,6 +92,7 @@ const Form = () => { placeholder='Masukan alamat email anda' value={form.email} onChange={handleInputChange} + autoComplete="username" /> </div> @@ -81,32 +106,18 @@ const Form = () => { placeholder='••••••••••••' value={form.password} onChange={handleInputChange} + autoComplete="current-password" /> </div> - <div className="mt-4"> - <input - type="checkbox" - name="tnc" - id="tnc" - checked={isCheckedTNC} - onClick={toggleCheckTNC} - /> - <label htmlFor="tnc" className="ml-2 cursor-pointer">Dengan ini saya menyetujui</label> - {' '} - <span - className="font-medium text-danger-500 cursor-pointer" - onClick={openTNC} - > - syarat dan ketentuan - </span> - <label htmlFor="tnc" className="ml-2 cursor-pointer">yang berlaku</label> - </div> + <FormCaptcha /> + + <TermCondition /> <button type="submit" className="btn-yellow w-full mt-2" - disabled={!isValid || !isCheckedTNC || mutation.isLoading} + disabled={!isValid || !isCheckedTNC || mutation.isLoading || !isValidCaptcha} > {mutation.isLoading ? 'Loading...' : 'Daftar'} </button> diff --git a/src-migrate/modules/register/components/FormCaptcha.tsx b/src-migrate/modules/register/components/FormCaptcha.tsx new file mode 100644 index 00000000..967be017 --- /dev/null +++ b/src-migrate/modules/register/components/FormCaptcha.tsx @@ -0,0 +1,14 @@ +import ReCaptcha from '~/common/components/elements/ReCaptcha' +import { useRegisterStore } from '~/common/stores/useRegisterStore' + +const FormCaptcha = () => { + const { updateValidCaptcha } = useRegisterStore() + + const handleCaptchaChange = (value: string | null) => { + updateValidCaptcha(value !== null) + } + + return <ReCaptcha onChange={handleCaptchaChange} /> +} + +export default FormCaptcha
\ No newline at end of file diff --git a/src-migrate/modules/register/components/Register.tsx b/src-migrate/modules/register/components/Register.tsx deleted file mode 100644 index e3e29b9f..00000000 --- a/src-migrate/modules/register/components/Register.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import PageContent from "~/modules/page-content" -import Form from "./Form" -import Link from "next/link" -import Modal from "~/common/components/elements/Modal" -import TermCondition from "./TermCondition" - -const Register = () => { - return ( - <div className="container"> - <div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 pt-16"> - <section> - <h1 className="text-2xl font-semibold"> - Daftar Akun Indoteknik - </h1> - <h2 className="text-gray_r-11 mt-1 mb-4"> - Buat akun sekarang lebih mudah dan terverifikasi - </h2> - - <Form /> - - <div className='text-gray_r-11 mt-10'> - Sudah punya akun Indoteknik?{' '} - <Link href='/login' className='inline font-medium text-danger-500'> - Masuk - </Link> - </div> - </section> - - <section className="my-10 md:my-0"> - <PageContent path="/register" /> - </section> - </div> - - <TermCondition /> - </div> - ) -} - -export default Register
\ No newline at end of file diff --git a/src-migrate/modules/register/components/TermCondition.tsx b/src-migrate/modules/register/components/TermCondition.tsx index 304ffd69..aaba6604 100644 --- a/src-migrate/modules/register/components/TermCondition.tsx +++ b/src-migrate/modules/register/components/TermCondition.tsx @@ -1,15 +1,33 @@ +import { Checkbox } from '@chakra-ui/react' import React from 'react' import Modal from '~/common/components/elements/Modal' import { useRegisterStore } from '~/common/stores/useRegisterStore' import PageContent from '~/modules/page-content' const TermCondition = () => { - const { isOpenTNC, closeTNC } = useRegisterStore() + const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } = useRegisterStore() return ( - <Modal active={isOpenTNC} close={closeTNC} > - <PageContent path='/register#tnd' /> - </Modal> + <> + <div className="mt-4 flex items-center gap-x-2"> + <Checkbox id='tnc' name='tnc' checked={isCheckedTNC} onChange={toggleCheckTNC} /> + <div> + <label htmlFor="tnc" className="cursor-pointer">Dengan ini saya menyetujui</label> + {' '} + <span + className="font-medium text-danger-500 cursor-pointer" + onClick={openTNC} + > + syarat dan ketentuan + </span> + <label htmlFor="tnc" className="ml-2 cursor-pointer">yang berlaku</label> + </div> + </div> + + <Modal active={isOpenTNC} close={closeTNC} > + <PageContent path='/register#tnd' /> + </Modal> + </> ) } diff --git a/src-migrate/modules/register/index.tsx b/src-migrate/modules/register/index.tsx index ba5efa3a..6325ee09 100644 --- a/src-migrate/modules/register/index.tsx +++ b/src-migrate/modules/register/index.tsx @@ -1,3 +1,47 @@ -import Register from "./components/Register"; +import PageContent from "~/modules/page-content" +import Form from "./components/Form" +import Link from "next/link" +import Image from "next/image" +import IndoteknikLogo from "~/images/logo.png" +import AccountActivation from "../account-activation" + +const LOGO_WIDTH = 150; +const LOGO_HEIGHT = LOGO_WIDTH / 3; + +const Register = () => { + return ( + <div className="container"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 pt-10 px-2 md:pt-16"> + <section> + <Link href='/' className="block md:hidden"> + <Image src={IndoteknikLogo} alt='Logo Indoteknik' width={LOGO_WIDTH} height={LOGO_HEIGHT} className="mx-auto mb-4 w-auto h-auto" priority /> + </Link> + + <h1 className="text-2xl font-semibold text-center md:text-left"> + Daftar Akun Indoteknik + </h1> + <h2 className="text-gray_r-11 mt-1 mb-10 text-center md:text-left"> + Buat akun sekarang lebih mudah dan terverifikasi + </h2> + + <Form /> + + <div className='text-gray_r-11 mt-4 text-center md:text-left'> + Sudah punya akun Indoteknik?{' '} + <Link href='/login' className='inline font-medium text-danger-500'> + Masuk + </Link> + </div> + </section> + + <section className="my-10 md:my-0"> + <PageContent path="/register" /> + </section> + </div> + + <AccountActivation /> + </div> + ) +} export default Register
\ No newline at end of file diff --git a/src-migrate/pages/register.tsx b/src-migrate/pages/register.tsx index 4ba72cbc..bd5c37f4 100644 --- a/src-migrate/pages/register.tsx +++ b/src-migrate/pages/register.tsx @@ -1,12 +1,17 @@ import BasicLayout from "@/core/components/layouts/BasicLayout" +import { useWindowSize } from "usehooks-ts" import Register from "~/modules/register" const RegisterPage = () => { + const { width } = useWindowSize() + + const Layout = width > 768 ? BasicLayout : "div"; + return ( - <BasicLayout> + <Layout> <Register /> - </BasicLayout> + </Layout> ) } diff --git a/src-migrate/services/auth.ts b/src-migrate/services/auth.ts index f2fd7761..feaabec8 100644 --- a/src-migrate/services/auth.ts +++ b/src-migrate/services/auth.ts @@ -1,10 +1,39 @@ import odooApi from '~/common/libs/odooApi'; -import { RegisterApiProps, RegisterProps } from '~/common/types/auth'; +import { + RegisterResApiProps, + RegisterProps, + ActivationTokenProps, + ActivationTokenResApiProps, + ActivationOtpProps, + ActivationOtpResApiProps, +} from '~/common/types/auth'; + +const BASE_PATH = '/api/v1/user'; export const registerUser = async ( data: RegisterProps -): Promise<RegisterApiProps> => { - const response = await odooApi('POST', '/api/v1/user/register', data); +): Promise<RegisterResApiProps> => { + const response = await odooApi('POST', `${BASE_PATH}/register`, data); + + return response; +}; + +export const activationUserToken = async ( + params: ActivationTokenProps +): Promise<ActivationTokenResApiProps> => { + const response = await odooApi( + 'POST', + `${BASE_PATH}/activation-token`, + params + ); + + return response; +}; + +export const activationUserOTP = async ( + params: ActivationOtpProps +): Promise<ActivationOtpResApiProps> => { + const response = await odooApi('POST', `${BASE_PATH}/activation-otp`, params); return response; }; |
