From eac8e0cb43334053e5d9862f9477ba9d52df3240 Mon Sep 17 00:00:00 2001 From: "fredy.siswanto" Date: Fri, 7 Mar 2025 05:53:52 +0700 Subject: [PATCH] feat: add category creation functionality with validation and API integration --- app/apis/admin/create-category.ts | 37 +++++++ app/pages/category-create/index.tsx | 101 ++++++++++++++++++ .../_admin.lg-admin.category.create.tsx | 11 ++ app/routes/actions.admin.category.create.ts | 67 ++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 app/apis/admin/create-category.ts create mode 100644 app/pages/category-create/index.tsx create mode 100644 app/routes/_admin.lg-admin.category.create.tsx create mode 100644 app/routes/actions.admin.category.create.ts diff --git a/app/apis/admin/create-category.ts b/app/apis/admin/create-category.ts new file mode 100644 index 0000000..9b50b7d --- /dev/null +++ b/app/apis/admin/create-category.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' + +import { HttpServer } from '~/libs/http-server' + +type TCategorySchema = { + code: string + name: string +} +const categoryResponseSchema = z.object({ + data: z.object({ + Message: z.string(), + }), +}) + +type TParameter = { + accessToken: string + payload: TCategorySchema +} + +export const createCategoryRequest = async (parameters: TParameter) => { + const { accessToken, payload } = parameters + try { + const { ...restPayload } = payload + const transformedPayload = { + ...restPayload, + } + + const { data } = await HttpServer({ accessToken }).post( + '/api/category/create', + transformedPayload, + ) + return categoryResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/pages/category-create/index.tsx b/app/pages/category-create/index.tsx new file mode 100644 index 0000000..920b463 --- /dev/null +++ b/app/pages/category-create/index.tsx @@ -0,0 +1,101 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect, useState, type ChangeEvent } 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 { TitleDashboard } from '~/components/ui/title-dashboard' + +export const createCategorySchema = z.object({ + code: z.string().min(3, 'Kode minimal 3 karakter'), + name: z.string().min(3, 'Nama minimal 3 karakter'), +}) +export type TCategorySchema = z.infer + +export const CreateCategoryPage = () => { + const fetcher = useFetcher() + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(createCategorySchema), + }) + const [error, setError] = useState() + const [disabled, setDisabled] = useState(false) + const [code, setCode] = useState('') + const [name, setName] = useState('') + + const { handleSubmit } = formMethods + + useEffect(() => { + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + setDisabled(false) + return + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher]) + + const handelCodeAuto = (name: ChangeEvent) => { + const inputName = name.target.value + setName(inputName) + const urlFriendlyCode = inputName + .toLowerCase() + .replaceAll(/\s+/g, '-') + .replaceAll(/[^\w-]+/g, '') + setCode(urlFriendlyCode) + } + + return ( +
+ +
+ + + {error && ( +
{error}
+ )} +
+ + + +
+
+
+
+
+ ) +} diff --git a/app/routes/_admin.lg-admin.category.create.tsx b/app/routes/_admin.lg-admin.category.create.tsx new file mode 100644 index 0000000..53fa3d5 --- /dev/null +++ b/app/routes/_admin.lg-admin.category.create.tsx @@ -0,0 +1,11 @@ +import { AdminDashboardLayout } from '~/layouts/admin/dashboard' +import { CreateCategoryPage } from '~/pages/category-create' + +const DashboardContentsLayout = () => { + return ( + + + + ) +} +export default DashboardContentsLayout diff --git a/app/routes/actions.admin.category.create.ts b/app/routes/actions.admin.category.create.ts new file mode 100644 index 0000000..1f27a93 --- /dev/null +++ b/app/routes/actions.admin.category.create.ts @@ -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 { createCategoryRequest } from '~/apis/admin/create-category' +import { handleCookie } from '~/libs/cookies' +import { + createCategorySchema, + type TCategorySchema, +} from '~/pages/category-create' + +import type { Route } from './+types/actions.register' + +export const action = async ({ request }: Route.ActionArgs) => { + const { staffToken } = await handleCookie(request) + try { + const { + errors, + data: payload, + receivedValues: defaultValues, + } = await getValidatedFormData( + request, + zodResolver(createCategorySchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: categoryData } = await createCategoryRequest({ + accessToken: staffToken, + payload, + }) + + return data( + { + success: true, + categoryData, + }, + { + 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 }, + ) + } +}