feat: add profile update functionality with form validation and API integration
This commit is contained in:
parent
d767055bdb
commit
978d74d226
28
app/apis/admin/update-profile.ts
Normal file
28
app/apis/admin/update-profile.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import type { TProfileSchema } from '~/layouts/admin/dialog-profile'
|
||||||
|
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||||
|
|
||||||
|
const updateProfileResponseSchema = z.object({
|
||||||
|
data: z.object({
|
||||||
|
Message: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
type TParameter = {
|
||||||
|
payload: TProfileSchema
|
||||||
|
} & THttpServer
|
||||||
|
|
||||||
|
export const updateProfileRequest = async (parameters: TParameter) => {
|
||||||
|
const { payload, ...restParameters } = parameters
|
||||||
|
try {
|
||||||
|
const { data } = await HttpServer(restParameters).put(
|
||||||
|
'/api/staff/update',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return updateProfileResponseSchema.parse(data)
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,29 +1,47 @@
|
|||||||
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBackdrop,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@headlessui/react'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useFetcher } from 'react-router'
|
import { useFetcher, useRouteLoaderData } from 'react-router'
|
||||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
import { Input } from '~/components/ui/input'
|
||||||
|
import { InputFile } from '~/components/ui/input-file'
|
||||||
import { useAdminContext } from '~/contexts/admin'
|
import { useAdminContext } from '~/contexts/admin'
|
||||||
|
import type { loader } from '~/routes/_admin.lg-admin'
|
||||||
|
|
||||||
const profileSchema = z.object({
|
export const profileSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
email: z.string().email('Invalid email address'),
|
email: z.string().email('Email is invalid'),
|
||||||
profile_picture: z.string().optional(),
|
profile_picture: z.string().url({
|
||||||
|
message: 'URL must be valid',
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
type TProfileSchema = z.infer<typeof profileSchema>
|
export type TProfileSchema = z.infer<typeof profileSchema>
|
||||||
|
|
||||||
export const DialogProfile = () => {
|
export const DialogProfile = () => {
|
||||||
const { editProfile, setEditProfile } = useAdminContext()
|
const { editProfile, setEditProfile } = useAdminContext()
|
||||||
|
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin')
|
||||||
|
const { staffData } = loaderData || {}
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
|
|
||||||
const formMethods = useRemixForm<TProfileSchema>({
|
const formMethods = useRemixForm<TProfileSchema>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
fetcher,
|
fetcher,
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
|
values: {
|
||||||
|
name: staffData?.name || '',
|
||||||
|
email: staffData?.email || '',
|
||||||
|
profile_picture: staffData?.profile_picture || '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { handleSubmit } = formMethods
|
const { handleSubmit } = formMethods
|
||||||
@ -57,16 +75,49 @@ export const DialogProfile = () => {
|
|||||||
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
transition
|
transition
|
||||||
className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
className="w-full max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
||||||
>
|
>
|
||||||
|
<DialogTitle
|
||||||
|
as="h3"
|
||||||
|
className="text-xl font-bold"
|
||||||
|
>
|
||||||
|
Update Profile
|
||||||
|
</DialogTitle>
|
||||||
<RemixFormProvider {...formMethods}>
|
<RemixFormProvider {...formMethods}>
|
||||||
<fetcher.Form
|
<fetcher.Form
|
||||||
method="post"
|
method="post"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
action="/actions/admin/profile"
|
action="/actions/admin/profile"
|
||||||
encType="multipart/form-data"
|
>
|
||||||
></fetcher.Form>
|
<Input
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Enter your name"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
label="Email"
|
||||||
|
placeholder="Contoh: legal@legalgo.id"
|
||||||
|
name="email"
|
||||||
|
/>
|
||||||
|
<InputFile
|
||||||
|
name="profile_picture"
|
||||||
|
id="profile_picture"
|
||||||
|
label="Profile Picture"
|
||||||
|
placeholder="Upload your profile picture"
|
||||||
|
category="profile_picture"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
type="submit"
|
||||||
|
className="w-full rounded-md py-2"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</fetcher.Form>
|
||||||
</RemixFormProvider>
|
</RemixFormProvider>
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export const DialogUpload = () => {
|
|||||||
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
transition
|
transition
|
||||||
className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
className="w-full max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
||||||
>
|
>
|
||||||
<RemixFormProvider {...formMethods}>
|
<RemixFormProvider {...formMethods}>
|
||||||
<fetcher.Form
|
<fetcher.Form
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { XiorError } from 'xior'
|
|||||||
import { deleteAdsRequest } from '~/apis/admin/delete-ads'
|
import { deleteAdsRequest } from '~/apis/admin/delete-ads'
|
||||||
import { handleCookie } from '~/libs/cookies'
|
import { handleCookie } from '~/libs/cookies'
|
||||||
|
|
||||||
import type { Route } from './+types/actions.admin.advertisements.create'
|
import type { Route } from './+types/actions.admin.advertisements.delete.$id'
|
||||||
|
|
||||||
export const action = async ({ request, params }: Route.ActionArgs) => {
|
export const action = async ({ request, params }: Route.ActionArgs) => {
|
||||||
const { staffToken: accessToken } = await handleCookie(request)
|
const { staffToken: accessToken } = await handleCookie(request)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { updateAdsRequest } from '~/apis/admin/update-ads'
|
|||||||
import { handleCookie } from '~/libs/cookies'
|
import { handleCookie } from '~/libs/cookies'
|
||||||
import { adsSchema, type TAdsSchema } from '~/pages/form-advertisements'
|
import { adsSchema, type TAdsSchema } from '~/pages/form-advertisements'
|
||||||
|
|
||||||
import type { Route } from './+types/actions.admin.advertisements.create'
|
import type { Route } from './+types/actions.admin.advertisements.update'
|
||||||
|
|
||||||
export const action = async ({ request }: Route.ActionArgs) => {
|
export const action = async ({ request }: Route.ActionArgs) => {
|
||||||
const { staffToken: accessToken } = await handleCookie(request)
|
const { staffToken: accessToken } = await handleCookie(request)
|
||||||
|
|||||||
67
app/routes/actions.admin.profile.ts
Normal file
67
app/routes/actions.admin.profile.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { data } from 'react-router'
|
||||||
|
import { getValidatedFormData } from 'remix-hook-form'
|
||||||
|
import { XiorError } from 'xior'
|
||||||
|
|
||||||
|
import { updateProfileRequest } from '~/apis/admin/update-profile'
|
||||||
|
import {
|
||||||
|
profileSchema,
|
||||||
|
type TProfileSchema,
|
||||||
|
} from '~/layouts/admin/dialog-profile'
|
||||||
|
import { handleCookie } from '~/libs/cookies'
|
||||||
|
|
||||||
|
import type { Route } from './+types/actions.admin.profile'
|
||||||
|
|
||||||
|
export const action = async ({ request }: Route.ActionArgs) => {
|
||||||
|
const { staffToken: accessToken } = await handleCookie(request)
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
errors,
|
||||||
|
data: payload,
|
||||||
|
receivedValues: defaultValues,
|
||||||
|
} = await getValidatedFormData<TProfileSchema>(
|
||||||
|
request,
|
||||||
|
zodResolver(profileSchema),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
return data({ success: false, errors, defaultValues }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: profileData } = await updateProfileRequest({
|
||||||
|
accessToken,
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
profileData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import { XiorError } from 'xior'
|
|||||||
import { deleteSubscribePlanRequest } from '~/apis/admin/delete-subscribe-plan'
|
import { deleteSubscribePlanRequest } from '~/apis/admin/delete-subscribe-plan'
|
||||||
import { handleCookie } from '~/libs/cookies'
|
import { handleCookie } from '~/libs/cookies'
|
||||||
|
|
||||||
import type { Route } from './+types/actions.admin.advertisements.create'
|
import type { Route } from './+types/actions.admin.subscribe-plan.delete.$id'
|
||||||
|
|
||||||
export const action = async ({ request, params }: Route.ActionArgs) => {
|
export const action = async ({ request, params }: Route.ActionArgs) => {
|
||||||
const { staffToken: accessToken } = await handleCookie(request)
|
const { staffToken: accessToken } = await handleCookie(request)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { XiorError } from 'xior'
|
|||||||
import { deleteTagsRequest } from '~/apis/admin/delete-tags'
|
import { deleteTagsRequest } from '~/apis/admin/delete-tags'
|
||||||
import { handleCookie } from '~/libs/cookies'
|
import { handleCookie } from '~/libs/cookies'
|
||||||
|
|
||||||
import type { Route } from './+types/actions.admin.advertisements.create'
|
import type { Route } from './+types/actions.admin.tags.delete.$id'
|
||||||
|
|
||||||
export const action = async ({ request, params }: Route.ActionArgs) => {
|
export const action = async ({ request, params }: Route.ActionArgs) => {
|
||||||
const { staffToken: accessToken } = await handleCookie(request)
|
const { staffToken: accessToken } = await handleCookie(request)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user