From 3958ae1098490cfcf756dede0df07816a6a2a602 Mon Sep 17 00:00:00 2001 From: Hafid Nur Date: Sat, 5 Apr 2025 07:39:57 +0700 Subject: [PATCH] refactor: fix validation for add gacha roll item - Pisahkan type dan schema untuk gacha roll item - Perbaiki validasi gacha roll item - Parsing angka pada component ControlledInputField - Update type number untuk component Input dan InputField --- .../gacha-roll/_components/modal-add-item.tsx | 11 +++++--- .../src/app/gacha-roll/_hook/use-add-item.ts | 26 +++++-------------- libs/service/src/schemas/gacha/index.ts | 23 ++++++++++++++++ libs/service/src/schemas/index.ts | 1 + libs/service/src/types/gacha/index.ts | 6 ++++- libs/ui/src/atoms/input/input.tsx | 2 +- .../src/molecules/input-field/input-field.tsx | 2 +- .../controlled-input-field.tsx | 14 +++++++++- 8 files changed, 58 insertions(+), 27 deletions(-) create mode 100644 libs/service/src/schemas/gacha/index.ts diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx b/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx index 5da4435..967d89f 100644 --- a/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx +++ b/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx @@ -78,7 +78,8 @@ const StepOne = ({ nextStep }: IStepOneProps) => { control={form.control} label="Quantity" name="quantity" - type="text" + type="number" + min={1} placeholder="Masukkan Kuantitas Item" size="lg" className="w-full" @@ -87,8 +88,12 @@ const StepOne = ({ nextStep }: IStepOneProps) => { control={form.control} label="Chance Rate" name="chanceRate" - type="text" - placeholder="Masukkan Chance Rate (0.1 - 1)" + type="number" + value={0.1} + min={0.1} + step={0.1} + max={1} + placeholder="Masukkan Chance Rate (0,1 - 1)" size="lg" className="w-full" /> diff --git a/apps/backoffice/src/app/gacha-roll/_hook/use-add-item.ts b/apps/backoffice/src/app/gacha-roll/_hook/use-add-item.ts index 2fc48d1..21c36e4 100644 --- a/apps/backoffice/src/app/gacha-roll/_hook/use-add-item.ts +++ b/apps/backoffice/src/app/gacha-roll/_hook/use-add-item.ts @@ -1,27 +1,13 @@ import { useForm } from 'react-hook-form'; -import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; - -const addItemSchema = z.object({ - itemName: z.string().min(1, 'Item name is required'), - quantity: z.string().min(1, 'Quantity is required'), - chanceRate: z - .string() - .min(1, 'Chance rate is required') - .refine( - (val) => { - const num = parseFloat(val); - return num >= 0.1 && num <= 1; - }, - { message: 'Chance rate must be between 0.1 and 1' } - ), -}); - -type TAddItemForm = z.infer; +import { + gachaRollItemSchema, + TGachaRollItem +} from '@imphnen-frontend-service/service'; export const useAddItem = (nextStep: () => void) => { - const form = useForm({ - resolver: zodResolver(addItemSchema), + const form = useForm({ + resolver: zodResolver(gachaRollItemSchema), mode: 'all', }); diff --git a/libs/service/src/schemas/gacha/index.ts b/libs/service/src/schemas/gacha/index.ts new file mode 100644 index 0000000..2e9065b --- /dev/null +++ b/libs/service/src/schemas/gacha/index.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const gachaRollItemSchema = z.object({ + itemName: z + .string({ + required_error: 'Nama item tidak boleh kosong', + invalid_type_error: 'Nama item harus berupa string', + }) + .min(1, 'Nama item tidak boleh kosong'), + quantity: z + .number({ + required_error: 'Quantity tidak boleh kosong', + invalid_type_error: 'Quantity harus berupa angka', + }) + .min(1, 'Quantity paling sedikit adalah 1'), + chanceRate: z + .number({ + required_error: 'Chance rate tidak boleh kosong', + invalid_type_error: 'Chance rate harus berupa angka', + }) + .min(0.1, 'Chance rate paling sedikit adalah 0,1') + .max(1, 'Chance rate paling banyak adalah 1'), +}); diff --git a/libs/service/src/schemas/index.ts b/libs/service/src/schemas/index.ts index 269586e..94e94e5 100644 --- a/libs/service/src/schemas/index.ts +++ b/libs/service/src/schemas/index.ts @@ -1 +1,2 @@ export * from './auth'; +export * from './gacha'; diff --git a/libs/service/src/types/gacha/index.ts b/libs/service/src/types/gacha/index.ts index cb0ff5c..3c0493d 100644 --- a/libs/service/src/types/gacha/index.ts +++ b/libs/service/src/types/gacha/index.ts @@ -1 +1,5 @@ -export {}; +export type TGachaRollItem = { + itemName: string; + quantity: number; + chanceRate: number; +}; diff --git a/libs/ui/src/atoms/input/input.tsx b/libs/ui/src/atoms/input/input.tsx index e780059..d8a6bf3 100644 --- a/libs/ui/src/atoms/input/input.tsx +++ b/libs/ui/src/atoms/input/input.tsx @@ -9,7 +9,7 @@ import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import import { cn } from '@imphnen-frontend-service/utils'; import { Button } from '../button'; -type TInputType = 'text' | 'email' | 'password' | 'file'; +type TInputType = 'text' | 'email' | 'number' | 'password' | 'file'; type TInputSize = 'sm' | 'md' | 'lg'; type TInputProps = Omit< diff --git a/libs/ui/src/molecules/input-field/input-field.tsx b/libs/ui/src/molecules/input-field/input-field.tsx index d7b4901..463be6c 100644 --- a/libs/ui/src/molecules/input-field/input-field.tsx +++ b/libs/ui/src/molecules/input-field/input-field.tsx @@ -7,7 +7,7 @@ import { import { Input } from '../../atoms'; import { cn } from '@imphnen-frontend-service/utils'; -export type TInputType = 'text' | 'email' | 'password' | 'file'; +export type TInputType = 'text' | 'email' | 'number' | 'password' | 'file'; export type TInputSize = 'sm' | 'md' | 'lg'; export type TInputFieldProps = Omit< DetailedHTMLProps, HTMLInputElement>, diff --git a/libs/ui/src/organisms/controlled-field/controlled-input-field.tsx b/libs/ui/src/organisms/controlled-field/controlled-input-field.tsx index 08f8552..e8e10b9 100644 --- a/libs/ui/src/organisms/controlled-field/controlled-input-field.tsx +++ b/libs/ui/src/organisms/controlled-field/controlled-input-field.tsx @@ -15,7 +15,19 @@ export const ControlledInputField = ( props: TControlledInputFieldProps ) => { const { field, fieldState } = useController(props); + + const handleChange = (e: React.ChangeEvent) => { + const value = + props.type === 'number' ? Number(e.target.value) : e.target.value; + field.onChange(value); + }; + return ( - + ); };