diff --git a/apps/backoffice/src/app/dashboard/_components/modal-add-item.tsx b/apps/backoffice/src/app/dashboard/_components/modal-add-item.tsx index 520cfe4..5e0aa55 100644 --- a/apps/backoffice/src/app/dashboard/_components/modal-add-item.tsx +++ b/apps/backoffice/src/app/dashboard/_components/modal-add-item.tsx @@ -1,10 +1,12 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { useConfirmItem, useItem } from '../_hook/use-item'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; interface IModalAddItem { isOpen: boolean; onClose: () => void; - handleAddItem: () => void; + handleAddItem?: () => Promise; currentStep?: number; nextStep: () => void; prevStep: () => void; @@ -46,91 +48,114 @@ interface IStepOneProps { onClose: () => void; } -const StepOne = ({ nextStep }: IStepOneProps) => ( - <> - -

- Tambah Item Gacha -

-

- Lengkapi detail di bawah ini untuk menambahkan item gacha -

-
- -
- - - -
+const StepOne = ({ nextStep }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep); - -
- -); + return ( + <> + +

+ Tambah Item Gacha +

+

+ Lengkapi detail di bawah ini untuk menambahkan item gacha +

+
+ +
+
+ + + +
+ + +
+
+ + ); +}; interface IStepTwoProps { onClose: () => void; - handleAddItem?: () => void; + handleAddItem?: () => Promise; resetStep: () => void; } -const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => ( - <> - -

- Tambah Item -

-

- Apakah kamu yakin ingin -
menambahkan item ini? -

-
- - - - - -); +const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => { + const { onConfirm, onCancel } = useConfirmItem( + onClose, + resetStep, + handleAddItem, + { + success: 'Item ditambahkan ke gacha item', + error: 'Item gagal ditambahkan ke gacha item', + } + ); + + return ( + <> + +

+ Tambah Item +

+

+ Apakah kamu yakin ingin +
menambahkan item ini? +

+
+ + + + + + ); +}; export default ModalAddItem; diff --git a/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx b/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx index 4d9bc7e..19a22fe 100644 --- a/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx +++ b/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx @@ -1,10 +1,12 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { useConfirmItem, useItem } from '../_hook/use-item'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; interface IModalEditItem { isOpen: boolean; onClose: () => void; - handleEditItem?: () => void; + handleEditItem?: () => Promise; currentStep?: number; nextStep: () => void; prevStep: () => void; @@ -46,91 +48,117 @@ interface IStepOneProps { onClose: () => void; } -const StepOne = ({ nextStep }: IStepOneProps) => ( - <> - -

- Edit Item Gacha -

-

- Silakan mengubah detail dari item yang diperlukan -

-
- -
- - - -
+const StepOne = ({ nextStep }: IStepOneProps) => { + const initialValues = { + itemName: 'Hoodie IMPHNEN Official 2025', + quantity: 10, + }; - -
- -); + const { form, onSubmit } = useItem(nextStep, initialValues); + + return ( + <> + +

+ Edit Item Gacha +

+

+ Silakan mengubah detail dari item yang diperlukan +

+
+ +
+
+ + + +
+ + +
+
+ + ); +}; interface IStepTwoProps { onClose: () => void; - handleEditItem?: () => void; + handleEditItem?: () => Promise; resetStep: () => void; } -const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => ( - <> - -

- Update Item -

-

- Apakah kamu yakin dengan -
perubahan yang dilakukan? -

-
- - - - - -); +const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => { + const { onConfirm, onCancel } = useConfirmItem( + onClose, + resetStep, + handleEditItem, + { + success: 'Perubahan item berhasil dilakukan', + error: 'Perubahan item gagal dilakukan', + } + ); + return ( + <> + +

+ Update Item +

+

+ Apakah kamu yakin dengan +
perubahan yang dilakukan? +

+
+ + + + + + ); +}; export default ModalEditItem; diff --git a/apps/backoffice/src/app/dashboard/_hook/use-item.ts b/apps/backoffice/src/app/dashboard/_hook/use-item.ts new file mode 100644 index 0000000..d432d56 --- /dev/null +++ b/apps/backoffice/src/app/dashboard/_hook/use-item.ts @@ -0,0 +1,61 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + gachaItemSchema, + TGachaItem +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; + +export const useItem = ( + nextStep: () => void, + initialValues?: TGachaItem +) => { + const form = useForm({ + resolver: zodResolver(gachaItemSchema), + mode: 'all', + defaultValues: initialValues, + }); + + const onSubmit = form.handleSubmit((data) => { + console.log('Form data:', data); + nextStep(); + }); + + return { + form, + onSubmit, + }; +}; + +export const useConfirmItem = ( + onClose: () => void, + resetStep: () => void, + actionFunction?: () => Promise, + messages?: { + success?: string; + error?: string; + } +) => { + const onConfirm = async () => { + try { + // const result = await actionFunction?.(); + // result ? toast.success(messages?.success) : toast.error(messages?.error); + toast.success(messages?.success); + onClose(); + resetStep(); + } catch (error) { + console.log(error); + toast.error(messages?.error); + } + }; + + const onCancel = () => { + onClose(); + resetStep(); + }; + + return { + onConfirm, + onCancel, + }; +}; diff --git a/apps/backoffice/src/app/dashboard/page.tsx b/apps/backoffice/src/app/dashboard/page.tsx index d337e53..be30690 100644 --- a/apps/backoffice/src/app/dashboard/page.tsx +++ b/apps/backoffice/src/app/dashboard/page.tsx @@ -31,20 +31,17 @@ export const Components: FC = (): ReactElement => { return (
- {/* Dashboard Header */}

Dashboard

- {/* Summary Section */}

Summary

- {/* Participants */}
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
- {/* Roll and Reroll */}
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
- {/* Redeem */}
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
- {/* Inactive Users */}
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
- {/* Gacha Items Section */}

@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {

- {/* Gacha Items List */}
{[1, 2, 3, 4, 5, 6].map((item) => (
-
+

Lanyard IMPHNEN

-
+
Prize {item} - Chance Rate: (0.1%) + Quantity: 10
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
- {/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */} - Lanyard IMPHNEN + Lanyard IMPHNEN
))}
@@ -174,9 +161,6 @@ export const Components: FC = (): ReactElement => { currentStep={currentStep} isOpen={showModalAddItem} onClose={() => setShowModalAddItem(false)} - handleAddItem={() => { - console.log('Item added'); - }} nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} @@ -187,9 +171,6 @@ export const Components: FC = (): ReactElement => { currentStep={currentStep} isOpen={showModalEditItem} onClose={() => setShowModalEditItem(false)} - handleEditItem={() => { - console.log('Item edited'); - }} nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} @@ -199,9 +180,6 @@ export const Components: FC = (): ReactElement => { setShowModalDeleteItem(false)} - handleDeleteItem={() => { - console.log('Item deleted'); - }} /> ); diff --git a/apps/backoffice/src/app/gacha-roll/page.tsx b/apps/backoffice/src/app/gacha-roll/page.tsx index 0bc7499..c57d204 100644 --- a/apps/backoffice/src/app/gacha-roll/page.tsx +++ b/apps/backoffice/src/app/gacha-roll/page.tsx @@ -184,9 +184,6 @@ export const Components: FC = (): ReactElement => { currentStep={currentStep} isOpen={showModalAddItem} onClose={() => setShowModalAddItem(false)} - handleAddItem={() => { - console.log('Item added'); - }} nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} @@ -195,9 +192,6 @@ export const Components: FC = (): ReactElement => { currentStep={currentStep} isOpen={showModalUpdateItem} onClose={() => setShowModalUpdateItem(false)} - handleUpdateItem={() => { - console.log('Item updated'); - }} nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} diff --git a/libs/service/src/schemas/gacha/index.ts b/libs/service/src/schemas/gacha/index.ts index 2e9065b..765037d 100644 --- a/libs/service/src/schemas/gacha/index.ts +++ b/libs/service/src/schemas/gacha/index.ts @@ -1,5 +1,31 @@ import { z } from 'zod'; +export const gachaItemSchema = 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'), + foto: z + .instanceof(File) + .optional() + .refine( + (file) => !file || file.size <= 5000000, // 5MB in bytes + 'Ukuran file maksimal 5MB' + ) + .refine( + (file) => !file || ['image/jpeg', 'image/png', 'image/webp'].includes(file.type), + 'Format file harus JPG, PNG, atau WEBP' + ) +}); + export const gachaRollItemSchema = z.object({ itemName: z .string({ diff --git a/libs/service/src/types/gacha/index.ts b/libs/service/src/types/gacha/index.ts index 3c0493d..14a1415 100644 --- a/libs/service/src/types/gacha/index.ts +++ b/libs/service/src/types/gacha/index.ts @@ -1,3 +1,9 @@ +export type TGachaItem = { + itemName: string; + quantity: number; + foto?: File; +}; + export type TGachaRollItem = { itemName: string; quantity: number; 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 e8e10b9..6f89836 100644 --- a/libs/ui/src/organisms/controlled-field/controlled-input-field.tsx +++ b/libs/ui/src/organisms/controlled-field/controlled-input-field.tsx @@ -17,16 +17,27 @@ export const ControlledInputField = ( const { field, fieldState } = useController(props); const handleChange = (e: React.ChangeEvent) => { - const value = - props.type === 'number' ? Number(e.target.value) : e.target.value; + let value; + + if (props.type === 'number') { + value = Number(e.target.value); + } else if (props.type === 'file') { + value = e.target.files?.[0]; + } else { + value = e.target.value; + } field.onChange(value); }; + const inputProps = + props.type === 'file' + ? { ...props, ...field, value: undefined } + : { ...props, ...field }; + return ( );