From 13f44f336d38dbedf10383988710c68c514593c3 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Fri, 28 Feb 2025 22:50:58 +0800 Subject: [PATCH 1/5] feat: update News context to use consistent setter naming and refactor FormLogin to utilize context --- app/contexts/news.tsx | 6 +++--- app/layouts/news/default.tsx | 11 +++-------- app/layouts/news/form-login.tsx | 13 +++---------- app/layouts/news/form-register.tsx | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/contexts/news.tsx b/app/contexts/news.tsx index c26404f..336a966 100644 --- a/app/contexts/news.tsx +++ b/app/contexts/news.tsx @@ -15,7 +15,7 @@ export type NewsContextProperties = { isRegisterOpen: boolean setIsRegisterOpen: Dispatch> isForgetOpen: boolean - setForgetOpen: Dispatch> + setIsForgetOpen: Dispatch> isSuccessOpen: ModalProperties['isOpen'] setIsSuccessOpen: Dispatch< SetStateAction @@ -29,7 +29,7 @@ const NewsContext = createContext(undefined) export const NewsProvider = ({ children }: PropsWithChildren) => { const [isLoginOpen, setIsLoginOpen] = useState(false) const [isRegisterOpen, setIsRegisterOpen] = useState(false) - const [isForgetOpen, setForgetOpen] = useState(false) + const [isForgetOpen, setIsForgetOpen] = useState(false) const [isSuccessOpen, setIsSuccessOpen] = useState() const [isInitSubscribeOpen, setIsInitSubscribeOpen] = useState(false) @@ -42,7 +42,7 @@ export const NewsProvider = ({ children }: PropsWithChildren) => { isRegisterOpen, setIsRegisterOpen, isForgetOpen, - setForgetOpen, + setIsForgetOpen, isSuccessOpen, setIsSuccessOpen, isInitSubscribeOpen, diff --git a/app/layouts/news/default.tsx b/app/layouts/news/default.tsx index 5caf422..64b704a 100644 --- a/app/layouts/news/default.tsx +++ b/app/layouts/news/default.tsx @@ -22,7 +22,7 @@ export const NewsDefaultLayout = (properties: PropsWithChildren) => { isRegisterOpen, setIsRegisterOpen, isForgetOpen, - setForgetOpen, + setIsForgetOpen, isSuccessOpen, setIsSuccessOpen, isInitSubscribeOpen, @@ -49,12 +49,7 @@ export const NewsDefaultLayout = (properties: PropsWithChildren) => { onClose={() => setIsLoginOpen(false)} description="Selamat Datang, silakan daftarkan akun Anda untuk melanjutkan!" > - + { setForgetOpen(false)} + onClose={() => setIsForgetOpen(false)} description="Selamat Datang, silakan isi keterangan akun Anda untuk melanjutkan!" > diff --git a/app/layouts/news/form-login.tsx b/app/layouts/news/form-login.tsx index 86135fd..e379a08 100644 --- a/app/layouts/news/form-login.tsx +++ b/app/layouts/news/form-login.tsx @@ -6,7 +6,7 @@ import { z } from 'zod' import { Button } from '~/components/ui/button' import { Input } from '~/components/ui/input' -import type { NewsContextProperties } from '~/contexts/news' +import { useNewsContext } from '~/contexts/news' export const loginSchema = z.object({ email: z.string().email('Email tidak valid'), @@ -15,20 +15,13 @@ export const loginSchema = z.object({ export type TLoginSchema = z.infer -type TProperties = { - setIsRegisterOpen: NewsContextProperties['setIsRegisterOpen'] - setIsLoginOpen: NewsContextProperties['setIsLoginOpen'] - setIsForgetOpen: NewsContextProperties['setForgetOpen'] - setIsInitSubscribeOpen: NewsContextProperties['setIsInitSubscribeOpen'] -} - -export const FormLogin = (properties: TProperties) => { +export const FormLogin = () => { const { setIsRegisterOpen, setIsLoginOpen, setIsForgetOpen, setIsInitSubscribeOpen, - } = properties + } = useNewsContext() const fetcher = useFetcher() const [error, setError] = useState() const [disabled, setDisabled] = useState(false) diff --git a/app/layouts/news/form-register.tsx b/app/layouts/news/form-register.tsx index aadb932..4f5cb48 100644 --- a/app/layouts/news/form-register.tsx +++ b/app/layouts/news/form-register.tsx @@ -1,9 +1,12 @@ import { useState } from 'react' import { Button } from '~/components/ui/button' +import { useNewsContext } from '~/contexts/news' export const FormRegister = () => { + const { setIsLoginOpen, setIsRegisterOpen } = useNewsContext() const [showPassword, setShowPassword] = useState(false) + return (
@@ -104,6 +107,22 @@ export const FormRegister = () => { Daftar + + {/* Link Login */} +
+ Sudah punya akun?{' '} + +
) From 057797584602e1d9cb64506aeb76b9ffb985abc3 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Fri, 28 Feb 2025 23:56:47 +0800 Subject: [PATCH 2/5] feat: implement HttpServer for API requests and add getUser function for user profile retrieval --- app/apis/news/get-user.ts | 23 +++++++++++++++++++++ app/apis/news/login.ts | 4 ++-- app/layouts/news/form-login.tsx | 18 +++++++--------- app/libs/{http-client.ts => http-server.ts} | 11 +++++----- app/routes/actions.login.ts | 5 +++++ 5 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 app/apis/news/get-user.ts rename app/libs/{http-client.ts => http-server.ts} (69%) diff --git a/app/apis/news/get-user.ts b/app/apis/news/get-user.ts new file mode 100644 index 0000000..858c943 --- /dev/null +++ b/app/apis/news/get-user.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' + +import { HttpServer, type THttpServer } from '~/libs/http-server' + +const playerSchema = z.object({ + data: z.object({ + id: z.string(), + email: z.string(), + subscribe_plan_code: z.string(), + subscribe_plan_name: z.string(), + subscribe_status: z.string(), + }), +}) + +export const getUser = async (parameters: THttpServer) => { + try { + const { data } = await HttpServer(parameters).get(`/api/user/profile`) + return playerSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/apis/news/login.ts b/app/apis/news/login.ts index 506ea27..5c74146 100644 --- a/app/apis/news/login.ts +++ b/app/apis/news/login.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { type TLoginSchema } from '~/layouts/news/form-login' -import HttpClient from '~/libs/http-client' +import { HttpServer } from '~/libs/http-server' const loginResponseSchema = z.object({ data: z.object({ @@ -11,7 +11,7 @@ const loginResponseSchema = z.object({ export const newsLoginRequest = async (payload: TLoginSchema) => { try { - const { data } = await HttpClient().post('/api/user/login', payload) + const { data } = await HttpServer().post('/api/user/login', payload) return loginResponseSchema.parse(data) } catch (error) { // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject diff --git a/app/layouts/news/form-login.tsx b/app/layouts/news/form-login.tsx index e379a08..52ec536 100644 --- a/app/layouts/news/form-login.tsx +++ b/app/layouts/news/form-login.tsx @@ -35,20 +35,16 @@ export const FormLogin = () => { const { handleSubmit } = formMethods useEffect(() => { - if (fetcher.state !== 'idle' || fetcher.data?.success) { - setDisabled(true) + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + setDisabled(false) return } - setDisabled(false) - }, [fetcher]) - useEffect(() => { - if (fetcher.data?.success) { - setIsInitSubscribeOpen(true) - setIsLoginOpen(false) - return - } - setError(fetcher.data?.message) + setDisabled(true) + setError(undefined) + setIsInitSubscribeOpen(true) + setIsLoginOpen(false) // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetcher]) diff --git a/app/libs/http-client.ts b/app/libs/http-server.ts similarity index 69% rename from app/libs/http-client.ts rename to app/libs/http-server.ts index e02c267..f279afc 100644 --- a/app/libs/http-client.ts +++ b/app/libs/http-server.ts @@ -2,9 +2,10 @@ import xior, { merge } from 'xior' const baseURL = import.meta.env.VITE_API_URL -type THttpClient = { token?: string } -const HttpClient = (parameters?: THttpClient) => { - const { token } = parameters || {} +export type THttpServer = { accessToken?: string } + +export const HttpServer = (parameters?: THttpServer) => { + const { accessToken } = parameters || {} const instance = xior.create({ baseURL, }) @@ -14,7 +15,7 @@ const HttpClient = (parameters?: THttpClient) => { return merge(config, { headers: { - ...(token && { Authorization: `Bearer ${token}` }), + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }, }) }) @@ -30,5 +31,3 @@ const HttpClient = (parameters?: THttpClient) => { return instance } - -export default HttpClient diff --git a/app/routes/actions.login.ts b/app/routes/actions.login.ts index af9961b..28118c2 100644 --- a/app/routes/actions.login.ts +++ b/app/routes/actions.login.ts @@ -3,6 +3,7 @@ import { data } from 'react-router' import { getValidatedFormData } from 'remix-hook-form' import { XiorError } from 'xior' +import { getUser } from '~/apis/news/get-user' import { newsLoginRequest } from '~/apis/news/login' import { loginSchema, type TLoginSchema } from '~/layouts/news/form-login' import { generateTokenCookie } from '~/utils/token' @@ -27,6 +28,9 @@ export const action = async ({ request }: Route.ActionArgs) => { const { data: loginData } = await newsLoginRequest(payload) const { token } = loginData + const { data: userData } = await getUser({ + accessToken: token, + }) const tokenCookie = generateTokenCookie({ token, }) @@ -37,6 +41,7 @@ export const action = async ({ request }: Route.ActionArgs) => { return data( { success: true, + user: userData, }, { headers, From fd745c20a01c6ac1a037eb1dd67b0f0d179bdb22 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Sat, 1 Mar 2025 00:07:57 +0800 Subject: [PATCH 3/5] feat: conditionally open subscription modal based on user subscribe status in FormLogin --- app/layouts/news/form-login.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/layouts/news/form-login.tsx b/app/layouts/news/form-login.tsx index 52ec536..797985b 100644 --- a/app/layouts/news/form-login.tsx +++ b/app/layouts/news/form-login.tsx @@ -43,8 +43,11 @@ export const FormLogin = () => { setDisabled(true) setError(undefined) - setIsInitSubscribeOpen(true) setIsLoginOpen(false) + + if (fetcher.data?.user.subscribe_status === 'inactive') { + setIsInitSubscribeOpen(true) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetcher]) From 42eb159a5257f9a05ed76e85ae2e6d67be12efdc Mon Sep 17 00:00:00 2001 From: Ardeman Date: Sat, 1 Mar 2025 01:00:52 +0800 Subject: [PATCH 4/5] feat: implement user registration functionality with form validation and API integration --- app/apis/news/register.ts | 20 ++++ app/layouts/news/form-login.tsx | 4 +- app/layouts/news/form-register.tsx | 182 +++++++++++++++-------------- app/routes/actions.register.ts | 75 ++++++++++++ 4 files changed, 191 insertions(+), 90 deletions(-) create mode 100644 app/apis/news/register.ts create mode 100644 app/routes/actions.register.ts diff --git a/app/apis/news/register.ts b/app/apis/news/register.ts new file mode 100644 index 0000000..7004157 --- /dev/null +++ b/app/apis/news/register.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +import type { TRegisterSchema } from '~/layouts/news/form-register' +import { HttpServer } from '~/libs/http-server' + +const loginResponseSchema = z.object({ + data: z.object({ + token: z.string(), + }), +}) + +export const newsRegisterRequest = async (payload: TRegisterSchema) => { + try { + const { data } = await HttpServer().post('/api/user/register', payload) + return loginResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/layouts/news/form-login.tsx b/app/layouts/news/form-login.tsx index 797985b..551ae72 100644 --- a/app/layouts/news/form-login.tsx +++ b/app/layouts/news/form-login.tsx @@ -63,7 +63,7 @@ export const FormLogin = () => { > @@ -80,7 +80,6 @@ export const FormLogin = () => {
{error}
)} - {/* Lupa Kata Sandi */}
Lupa Kata Sandi?
- {/* Tombol Masuk */} - - {/* Reinput Password */} -
- - - -
- {/* No Telepon */} -
- - -
- {/* Subscribe*/} -
-
*/} + + {error && ( +
{error}
+ )} + + - + Daftar + + + {/* Link Login */}
diff --git a/app/routes/actions.register.ts b/app/routes/actions.register.ts new file mode 100644 index 0000000..ae30723 --- /dev/null +++ b/app/routes/actions.register.ts @@ -0,0 +1,75 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'react-router' +import { getValidatedFormData } from 'remix-hook-form' +import { XiorError } from 'xior' + +import { getUser } from '~/apis/news/get-user' +import { newsRegisterRequest } from '~/apis/news/register' +import { + registerSchema, + type TRegisterSchema, +} from '~/layouts/news/form-register' +import { generateTokenCookie } from '~/utils/token' + +import type { Route } from './+types/actions.register' + +export const action = async ({ request }: Route.ActionArgs) => { + try { + const { + errors, + data: payload, + receivedValues: defaultValues, + } = await getValidatedFormData( + request, + zodResolver(registerSchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: registerData } = await newsRegisterRequest(payload) + const { token } = registerData + const { data: userData } = await getUser({ + accessToken: token, + }) + const tokenCookie = generateTokenCookie({ + token, + }) + + const headers = new Headers() + headers.append('Set-Cookie', await tokenCookie) + + return data( + { + success: true, + user: userData, + }, + { + headers, + status: 200, + statusText: 'OK', + }, + ) + } catch (error) { + if (error instanceof XiorError) { + return data( + { + success: false, + message: error?.response?.data?.error?.message || error.message, + }, + { + status: error?.response?.status || 500, + }, + ) + } + return data( + { + success: false, + message: 'Internal server error', + }, + { status: 500 }, + ) + } +} From 9ef33d72d3df435a928e4957c5869ff67948ff3e Mon Sep 17 00:00:00 2001 From: Ardeman Date: Sat, 1 Mar 2025 06:32:20 +0800 Subject: [PATCH 5/5] feat: refactor Input component to use Headless UI and add disabled prop --- app/components/ui/input.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/components/ui/input.tsx b/app/components/ui/input.tsx index 3a7ee75..490954d 100644 --- a/app/components/ui/input.tsx +++ b/app/components/ui/input.tsx @@ -1,3 +1,4 @@ +import { Field, Label, Input as UIInput } from '@headlessui/react' import { useState, type ComponentProps, type ReactNode } from 'react' import { get, @@ -33,6 +34,7 @@ export const Input = >( rules, type = 'text', placeholder, + disabled, ...rest } = properties const [inputType, setInputType] = useState(type) @@ -45,15 +47,15 @@ export const Input = >( const error: FieldError = get(errors, name) return ( -
-
+ ) }