feat: add profile update functionality with form validation and API integration

This commit is contained in:
Ardeman 2025-03-15 19:23:16 +08:00
parent d767055bdb
commit 978d74d226
8 changed files with 160 additions and 14 deletions

View 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)
}
}

View File

@ -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 { useEffect } from 'react'
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 { 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 type { loader } from '~/routes/_admin.lg-admin'
const profileSchema = z.object({
export const profileSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email address'),
profile_picture: z.string().optional(),
email: z.string().email('Email is invalid'),
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 = () => {
const { editProfile, setEditProfile } = useAdminContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin')
const { staffData } = loaderData || {}
const fetcher = useFetcher()
const formMethods = useRemixForm<TProfileSchema>({
mode: 'onSubmit',
fetcher,
resolver: zodResolver(profileSchema),
values: {
name: staffData?.name || '',
email: staffData?.email || '',
profile_picture: staffData?.profile_picture || '',
},
})
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">
<DialogPanel
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}>
<fetcher.Form
method="post"
onSubmit={handleSubmit}
className="space-y-4"
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>
</DialogPanel>
</div>

View File

@ -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">
<DialogPanel
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}>
<fetcher.Form

View File

@ -4,7 +4,7 @@ import { XiorError } from 'xior'
import { deleteAdsRequest } from '~/apis/admin/delete-ads'
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) => {
const { staffToken: accessToken } = await handleCookie(request)

View File

@ -7,7 +7,7 @@ import { updateAdsRequest } from '~/apis/admin/update-ads'
import { handleCookie } from '~/libs/cookies'
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) => {
const { staffToken: accessToken } = await handleCookie(request)

View 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 },
)
}
}

View File

@ -4,7 +4,7 @@ import { XiorError } from 'xior'
import { deleteSubscribePlanRequest } from '~/apis/admin/delete-subscribe-plan'
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) => {
const { staffToken: accessToken } = await handleCookie(request)

View File

@ -4,7 +4,7 @@ import { XiorError } from 'xior'
import { deleteTagsRequest } from '~/apis/admin/delete-tags'
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) => {
const { staffToken: accessToken } = await handleCookie(request)