feat: refactor user authentication and subscription dialogs for improved structure and consistency

This commit is contained in:
Ardeman 2025-03-15 15:24:01 +08:00
parent 6d99a37f20
commit c7195b7428
13 changed files with 404 additions and 389 deletions

View File

@ -1,6 +1,6 @@
import { z } from 'zod'
import { type TLoginSchema } from '~/layouts/news/form-login'
import { type TLoginSchema } from '~/layouts/news/dialog-login'
import { HttpServer } from '~/libs/http-server'
export const loginResponseSchema = z.object({

View File

@ -1,4 +1,4 @@
import type { TRegisterSchema } from '~/layouts/news/form-register'
import type { TRegisterSchema } from '~/layouts/news/dialog-register'
import { HttpServer } from '~/libs/http-server'
import { loginResponseSchema } from './login-user'

View File

@ -1,34 +1,22 @@
import { type PropsWithChildren } from 'react'
import { Toaster } from 'react-hot-toast'
import { DialogNews } from '~/components/dialog/news'
import { DialogSuccess } from '~/components/dialog/success'
import { useNewsContext } from '~/contexts/news'
import { Banner } from '~/layouts/news/banner'
import { FormForgotPassword } from '~/layouts/news/form-forgot-password'
import { FormLogin } from '~/layouts/news/form-login'
import { FormRegister } from '~/layouts/news/form-register'
import { DialogForgotPassword } from '~/layouts/news/dialog-forgot-password'
import { DialogLogin } from '~/layouts/news/dialog-login'
import { DialogRegister } from './dialog-register'
import { DialogSubscribePlan } from './dialog-subscribe-plan'
import { FooterLinks } from './footer-links'
import { FooterNewsletter } from './footer-newsletter'
import { FormSubscribePlan } from './form-subscribe-plan'
import { HeaderMenu } from './header-menu'
import { HeaderTop } from './header-top'
export const NewsDefaultLayout = (properties: PropsWithChildren) => {
const { children } = properties
const {
isLoginOpen,
setIsLoginOpen,
isRegisterOpen,
setIsRegisterOpen,
isForgetOpen,
setIsForgetOpen,
isSuccessOpen,
setIsSuccessOpen,
isSubscribeOpen,
setIsSubscribeOpen,
} = useNewsContext()
const { isSuccessOpen, setIsSuccessOpen } = useNewsContext()
return (
<main className="relative min-h-dvh bg-[#ECECEC]">
<header>
@ -46,39 +34,10 @@ export const NewsDefaultLayout = (properties: PropsWithChildren) => {
</footer>
<Toaster />
<DialogNews
isOpen={isLoginOpen}
onClose={() => setIsLoginOpen(false)}
description="Selamat Datang, silakan daftarkan akun Anda untuk melanjutkan!"
>
<FormLogin />
</DialogNews>
<DialogNews
isOpen={isRegisterOpen}
onClose={() => setIsRegisterOpen(false)}
description="Selamat Datang, silakan isi keterangan akun Anda untuk melanjutkan!"
>
<FormRegister />
</DialogNews>
<DialogNews
isOpen={isForgetOpen}
onClose={() => setIsForgetOpen(false)}
description="Selamat Datang, silakan isi keterangan akun Anda untuk melanjutkan!"
>
<FormForgotPassword />
</DialogNews>
<DialogNews
isOpen={isSubscribeOpen}
onClose={() => setIsSubscribeOpen(false)}
description="Selamat Datang, silakan Pilih Subscribe Plan Anda untuk melanjutkan!"
>
<FormSubscribePlan />
</DialogNews>
<DialogLogin />
<DialogRegister />
<DialogForgotPassword />
<DialogSubscribePlan />
<DialogSuccess
isOpen={isSuccessOpen}
onClose={() => {

View File

@ -0,0 +1,49 @@
import { useFetcher } from 'react-router'
import { DialogNews } from '~/components/dialog/news'
import { Button } from '~/components/ui/button'
import { useNewsContext } from '~/contexts/news'
export const DialogForgotPassword = () => {
const { isForgetOpen, setIsForgetOpen } = useNewsContext()
const fetcher = useFetcher()
return (
<DialogNews
isOpen={isForgetOpen}
onClose={() => {
if (fetcher.state === 'idle') {
setIsForgetOpen(false)
}
}}
description="Selamat Datang, silakan isi keterangan akun Anda untuk melanjutkan!"
>
<div className="flex flex-col items-center justify-center">
<div className="w-full max-w-md">
<form>
{/* Input Email / No Telepon */}
<div className="mb-4">
<label
htmlFor="email"
className="mb-1 block text-gray-700"
>
Email/No. Telepon
</label>
<input
type="text"
placeholder="Contoh: legal@legalgo.id"
className="focus:inheriten w-full rounded-md border border-[#DFDFDF] p-2"
required
/>
</div>
{/* Tombol Masuk */}
<Button className="mt-5 w-full rounded-md py-2">
Reset Password
</Button>
</form>
</div>
</div>
</DialogNews>
)
}

View File

@ -0,0 +1,134 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState } from 'react'
import { useFetcher } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
import { DialogNews } from '~/components/dialog/news'
import { Button } from '~/components/ui/button'
import { Input } from '~/components/ui/input'
import { useNewsContext } from '~/contexts/news'
export const loginSchema = z.object({
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Kata sandi minimal 6 karakter'),
})
export type TLoginSchema = z.infer<typeof loginSchema>
export const DialogLogin = () => {
const {
setIsRegisterOpen,
setIsLoginOpen,
setIsForgetOpen,
setIsSubscribeOpen,
isLoginOpen,
} = useNewsContext()
const fetcher = useFetcher()
const [error, setError] = useState<string>()
const formMethods = useRemixForm<TLoginSchema>({
mode: 'onSubmit',
fetcher,
resolver: zodResolver(loginSchema),
})
const { handleSubmit } = formMethods
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
return
}
setError(undefined)
setIsLoginOpen(false)
if (fetcher.data?.user.subscribe_plan_code === 'basic') {
setIsSubscribeOpen(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
return (
<DialogNews
isOpen={isLoginOpen}
onClose={() => {
if (fetcher.state === 'idle') {
setIsLoginOpen(false)
}
}}
description="Selamat Datang, silakan daftarkan akun Anda untuk melanjutkan!"
>
<div className="flex items-center justify-center">
<div className="w-full max-w-md">
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
action="/actions/login"
>
<Input
id="email"
label="Email"
placeholder="Contoh: legal@legalgo.id"
name="email"
/>
<Input
id="password"
label="Kata Sandi"
placeholder="Masukkan Kata Sandi"
name="password"
type="password"
/>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Lupa Kata Sandi?</span>
<Button
onClick={() => {
setIsLoginOpen(false)
setIsForgetOpen(true)
}}
variant="link"
size="fit"
>
Reset Kata Sandi
</Button>
</div>
<Button
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md py-2"
>
Masuk
</Button>
</fetcher.Form>
</RemixFormProvider>
{/* Link Daftar */}
<div className="mt-4 text-center text-sm">
Belum punya akun?{' '}
<Button
onClick={() => {
setIsLoginOpen(false)
setIsRegisterOpen(true)
}}
variant="link"
size="fit"
>
Daftar Disini
</Button>
</div>
</div>
</div>
</DialogNews>
)
}

View File

@ -0,0 +1,163 @@
import { DevTool } from '@hookform/devtools'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState } from 'react'
import { useFetcher, useRouteLoaderData } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
import { DialogNews } from '~/components/dialog/news'
import { Button } from '~/components/ui/button'
import { Combobox } from '~/components/ui/combobox'
import { Input } from '~/components/ui/input'
import { useNewsContext } from '~/contexts/news'
import type { loader } from '~/routes/_news'
export const registerSchema = z
.object({
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Kata sandi minimal 6 karakter'),
rePassword: z.string().min(6, 'Kata sandi minimal 6 karakter'),
phone: z.string().min(10, 'No telepon tidak valid'),
subscribe_plan: z
.object({
id: z.string(),
code: z.string(),
name: z.string(),
})
.optional()
.nullable()
.refine((data) => !!data, {
message: 'Please select a Subscribe Plan',
}),
})
.refine((field) => field.password === field.rePassword, {
message: 'Kata sandi tidak sama',
path: ['rePassword'],
})
export type TRegisterSchema = z.infer<typeof registerSchema>
export const DialogRegister = () => {
const {
setIsLoginOpen,
setIsRegisterOpen,
setIsSuccessOpen,
isRegisterOpen,
} = useNewsContext()
const [error, setError] = useState<string>()
const fetcher = useFetcher()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const { subscribePlanData: subscribePlan } = loaderData || {}
const formMethods = useRemixForm<TRegisterSchema>({
mode: 'onSubmit',
fetcher,
resolver: zodResolver(registerSchema),
})
const { handleSubmit, control } = formMethods
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
return
}
setError(undefined)
setIsRegisterOpen(false)
setIsSuccessOpen('register')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
return (
<DialogNews
isOpen={isRegisterOpen}
onClose={() => {
if (fetcher.state === 'idle') {
setIsRegisterOpen(false)
}
}}
description="Selamat Datang, silakan isi keterangan akun Anda untuk melanjutkan!"
>
<div className="flex flex-col items-center justify-center">
<div className="w-full max-w-md">
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
action="/actions/register"
>
<Input
id="email"
label="Email"
placeholder="Contoh: legal@legalgo.id"
name="email"
/>
<Input
id="password"
label="Kata Sandi"
placeholder="Masukkan Kata Sandi"
name="password"
type="password"
/>
<Input
id="re-password"
label="Ulangi Kata Sandi"
placeholder="Masukkan Kata Sandi"
name="rePassword"
type="password"
/>
<Input
id="phone"
label="No. Telepon"
placeholder="Masukkan No. Telepon"
name="phone"
/>
<Combobox
id="subscribe_plan"
name="subscribe_plan"
label="Subscribe Plan"
placeholder="Pilih Subscribe Plan"
options={subscribePlan}
/>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<Button
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md py-2"
>
Daftar
</Button>
</fetcher.Form>
</RemixFormProvider>
{/* Link Login */}
<div className="mt-4 text-center text-sm">
Sudah punya akun?{' '}
<Button
onClick={() => {
setIsLoginOpen(true)
setIsRegisterOpen(false)
}}
variant="link"
size="fit"
>
Masuk Disini
</Button>
</div>
</div>
<DevTool control={control} />
</div>
</DialogNews>
)
}

View File

@ -4,6 +4,7 @@ import { useFetcher, useRouteLoaderData } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
import { DialogNews } from '~/components/dialog/news'
import { Button } from '~/components/ui/button'
import { Combobox } from '~/components/ui/combobox'
import { useNewsContext } from '~/contexts/news'
@ -25,8 +26,9 @@ export const subscribeSchema = z.object({
export type TSubscribeSchema = z.infer<typeof subscribeSchema>
export const FormSubscribePlan = () => {
const { setIsSubscribeOpen, setIsSuccessOpen } = useNewsContext()
export const DialogSubscribePlan = () => {
const { setIsSubscribeOpen, setIsSuccessOpen, isSubscribeOpen } =
useNewsContext()
const fetcher = useFetcher()
const [error, setError] = useState<string>()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
@ -53,36 +55,46 @@ export const FormSubscribePlan = () => {
}, [fetcher])
return (
<div className="flex flex-col items-center justify-center">
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="w-full max-w-md"
action="/actions/subscribe"
>
<Combobox
id="subscribe_plan"
name="subscribe_plan"
label="Subscribe Plan"
placeholder="Pilih Subscribe Plan"
options={subscribePlan}
/>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<Button
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="mt-5 w-full rounded-md py-2"
<DialogNews
isOpen={isSubscribeOpen}
onClose={() => {
if (fetcher.state === 'idle') {
setIsSubscribeOpen(false)
}
}}
description="Selamat Datang, silakan Pilih Subscribe Plan Anda untuk melanjutkan!"
>
<div className="flex flex-col items-center justify-center">
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="w-full max-w-md"
action="/actions/subscribe"
>
Lanjutkan
</Button>
</fetcher.Form>
</RemixFormProvider>
</div>
<Combobox
id="subscribe_plan"
name="subscribe_plan"
label="Subscribe Plan"
placeholder="Pilih Subscribe Plan"
options={subscribePlan}
/>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<Button
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="mt-5 w-full rounded-md py-2"
>
Lanjutkan
</Button>
</fetcher.Form>
</RemixFormProvider>
</div>
</DialogNews>
)
}

View File

@ -1,32 +0,0 @@
import { Button } from '~/components/ui/button'
export const FormForgotPassword = () => {
return (
<div className="flex flex-col items-center justify-center">
<div className="w-full max-w-md">
<form>
{/* Input Email / No Telepon */}
<div className="mb-4">
<label
htmlFor="email"
className="mb-1 block text-gray-700"
>
Email/No. Telepon
</label>
<input
type="text"
placeholder="Contoh: legal@legalgo.id"
className="focus:inheriten w-full rounded-md border border-[#DFDFDF] p-2"
required
/>
</div>
{/* Tombol Masuk */}
<Button className="mt-5 w-full rounded-md py-2">
Reset Password
</Button>
</form>
</div>
</div>
)
}

View File

@ -1,122 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState } from 'react'
import { useFetcher } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
import { Button } from '~/components/ui/button'
import { Input } from '~/components/ui/input'
import { useNewsContext } from '~/contexts/news'
export const loginSchema = z.object({
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Kata sandi minimal 6 karakter'),
})
export type TLoginSchema = z.infer<typeof loginSchema>
export const FormLogin = () => {
const {
setIsRegisterOpen,
setIsLoginOpen,
setIsForgetOpen,
setIsSubscribeOpen,
} = useNewsContext()
const fetcher = useFetcher()
const [error, setError] = useState<string>()
const formMethods = useRemixForm<TLoginSchema>({
mode: 'onSubmit',
fetcher,
resolver: zodResolver(loginSchema),
})
const { handleSubmit } = formMethods
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
return
}
setError(undefined)
setIsLoginOpen(false)
if (fetcher.data?.user.subscribe_plan_code === 'basic') {
setIsSubscribeOpen(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
return (
<div className="flex items-center justify-center">
<div className="w-full max-w-md">
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
action="/actions/login"
>
<Input
id="email"
label="Email"
placeholder="Contoh: legal@legalgo.id"
name="email"
/>
<Input
id="password"
label="Kata Sandi"
placeholder="Masukkan Kata Sandi"
name="password"
type="password"
/>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Lupa Kata Sandi?</span>
<Button
onClick={() => {
setIsLoginOpen(false)
setIsForgetOpen(true)
}}
variant="link"
size="fit"
>
Reset Kata Sandi
</Button>
</div>
<Button
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md py-2"
>
Masuk
</Button>
</fetcher.Form>
</RemixFormProvider>
{/* Link Daftar */}
<div className="mt-4 text-center text-sm">
Belum punya akun?{' '}
<Button
onClick={() => {
setIsLoginOpen(false)
setIsRegisterOpen(true)
}}
variant="link"
size="fit"
>
Daftar Disini
</Button>
</div>
</div>
</div>
)
}

View File

@ -1,148 +0,0 @@
import { DevTool } from '@hookform/devtools'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState } from 'react'
import { useFetcher, useRouteLoaderData } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
import { Button } from '~/components/ui/button'
import { Combobox } from '~/components/ui/combobox'
import { Input } from '~/components/ui/input'
import { useNewsContext } from '~/contexts/news'
import type { loader } from '~/routes/_news'
export const registerSchema = z
.object({
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Kata sandi minimal 6 karakter'),
rePassword: z.string().min(6, 'Kata sandi minimal 6 karakter'),
phone: z.string().min(10, 'No telepon tidak valid'),
subscribe_plan: z
.object({
id: z.string(),
code: z.string(),
name: z.string(),
})
.optional()
.nullable()
.refine((data) => !!data, {
message: 'Please select a Subscribe Plan',
}),
})
.refine((field) => field.password === field.rePassword, {
message: 'Kata sandi tidak sama',
path: ['rePassword'],
})
export type TRegisterSchema = z.infer<typeof registerSchema>
export const FormRegister = () => {
const { setIsLoginOpen, setIsRegisterOpen, setIsSuccessOpen } =
useNewsContext()
const [error, setError] = useState<string>()
const fetcher = useFetcher()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const { subscribePlanData: subscribePlan } = loaderData || {}
const formMethods = useRemixForm<TRegisterSchema>({
mode: 'onSubmit',
fetcher,
resolver: zodResolver(registerSchema),
})
const { handleSubmit, control } = formMethods
useEffect(() => {
if (!fetcher.data?.success) {
setError(fetcher.data?.message)
return
}
setError(undefined)
setIsRegisterOpen(false)
setIsSuccessOpen('register')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher])
return (
<div className="flex flex-col items-center justify-center">
<div className="w-full max-w-md">
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
action="/actions/register"
>
<Input
id="email"
label="Email"
placeholder="Contoh: legal@legalgo.id"
name="email"
/>
<Input
id="password"
label="Kata Sandi"
placeholder="Masukkan Kata Sandi"
name="password"
type="password"
/>
<Input
id="re-password"
label="Ulangi Kata Sandi"
placeholder="Masukkan Kata Sandi"
name="rePassword"
type="password"
/>
<Input
id="phone"
label="No. Telepon"
placeholder="Masukkan No. Telepon"
name="phone"
/>
<Combobox
id="subscribe_plan"
name="subscribe_plan"
label="Subscribe Plan"
placeholder="Pilih Subscribe Plan"
options={subscribePlan}
/>
{error && (
<div className="text-sm text-red-500 capitalize">{error}</div>
)}
<Button
isLoading={fetcher.state !== 'idle'}
disabled={fetcher.state !== 'idle'}
type="submit"
className="w-full rounded-md py-2"
>
Daftar
</Button>
</fetcher.Form>
</RemixFormProvider>
{/* Link Login */}
<div className="mt-4 text-center text-sm">
Sudah punya akun?{' '}
<Button
onClick={() => {
setIsLoginOpen(true)
setIsRegisterOpen(false)
}}
variant="link"
size="fit"
>
Masuk Disini
</Button>
</div>
</div>
<DevTool control={control} />
</div>
)
}

View File

@ -5,7 +5,7 @@ import { XiorError } from 'xior'
import { getUser } from '~/apis/news/get-user'
import { userLoginRequest } from '~/apis/news/login-user'
import { loginSchema, type TLoginSchema } from '~/layouts/news/form-login'
import { loginSchema, type TLoginSchema } from '~/layouts/news/dialog-login'
import { generateUserTokenCookie } from '~/utils/token'
import type { Route } from './+types/actions.login'

View File

@ -8,7 +8,7 @@ import { userRegisterRequest } from '~/apis/news/register-user'
import {
registerSchema,
type TRegisterSchema,
} from '~/layouts/news/form-register'
} from '~/layouts/news/dialog-register'
import { generateUserTokenCookie } from '~/utils/token'
import type { Route } from './+types/actions.register'

View File

@ -8,7 +8,7 @@ import { getUser } from '~/apis/news/get-user'
import {
subscribeSchema,
type TSubscribeSchema,
} from '~/layouts/news/form-subscribe-plan'
} from '~/layouts/news/dialog-subscribe-plan'
import { handleCookie } from '~/libs/cookies'
import type { Route } from './+types/actions.subscribe'