feat: update Dashboard page
- Perubahan tampilan dan modal pada halaman Dashboard - Tambah schema dan type untuk Gacha Item - Gunakan form dan Toast
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
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 {
|
interface IModalAddItem {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleAddItem: () => void;
|
handleAddItem?: () => Promise<boolean>;
|
||||||
currentStep?: number;
|
currentStep?: number;
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
@@ -46,91 +48,114 @@ interface IStepOneProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => (
|
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||||
<>
|
const { form, onSubmit } = useItem(nextStep);
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Item Gacha
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<InputField
|
|
||||||
label="Nama Hadiah"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nama Hadiah"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Chance Rate"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Chance Rate"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Foto Barang"
|
|
||||||
type="file"
|
|
||||||
placeholder=".jpg, .jpeg, atau .png"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
|
return (
|
||||||
Tambahkan Item
|
<>
|
||||||
</Button>
|
<Modal.Header>
|
||||||
</Modal.Content>
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
</>
|
Tambah Item Gacha
|
||||||
);
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Nama Hadiah"
|
||||||
|
name="itemName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Nama Hadiah"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Quantity"
|
||||||
|
name="quantity"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
placeholder="Masukkan Kuantitas Item"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Foto Barang"
|
||||||
|
type="file"
|
||||||
|
name="foto"
|
||||||
|
placeholder=".jpg, .jpeg, atau .png"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
Tambahkan Item
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IStepTwoProps {
|
interface IStepTwoProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleAddItem?: () => void;
|
handleAddItem?: () => Promise<boolean>;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => (
|
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||||
<>
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
onClose,
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
resetStep,
|
||||||
Tambah Item
|
handleAddItem,
|
||||||
</h2>
|
{
|
||||||
<p className="text-p3 text-neutral-400">
|
success: 'Item ditambahkan ke gacha item',
|
||||||
Apakah kamu yakin ingin
|
error: 'Item gagal ditambahkan ke gacha item',
|
||||||
<br /> menambahkan item ini?
|
}
|
||||||
</p>
|
);
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
return (
|
||||||
<Button
|
<>
|
||||||
variant="bordered"
|
<Modal.Header className="mb-0 text-center items-center">
|
||||||
size="lg"
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
className="w-full"
|
Tambah Item
|
||||||
onClick={() => {
|
</h2>
|
||||||
onClose();
|
<p className="text-p3 text-neutral-400">
|
||||||
resetStep();
|
Apakah kamu yakin ingin
|
||||||
}}
|
<br /> menambahkan item ini?
|
||||||
>
|
</p>
|
||||||
Batal
|
</Modal.Header>
|
||||||
</Button>
|
<Modal.Content className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="bordered"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={onCancel}
|
||||||
handleAddItem && handleAddItem();
|
>
|
||||||
onClose();
|
Batal
|
||||||
resetStep();
|
</Button>
|
||||||
}}
|
<Button
|
||||||
>
|
variant="primary"
|
||||||
Tambahkan
|
size="lg"
|
||||||
</Button>
|
className="w-full"
|
||||||
</Modal.Content>
|
onClick={onConfirm}
|
||||||
</>
|
>
|
||||||
);
|
Tambahkan
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ModalAddItem;
|
export default ModalAddItem;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
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 {
|
interface IModalEditItem {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleEditItem?: () => void;
|
handleEditItem?: () => Promise<boolean>;
|
||||||
currentStep?: number;
|
currentStep?: number;
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
@@ -46,91 +48,117 @@ interface IStepOneProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => (
|
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||||
<>
|
const initialValues = {
|
||||||
<Modal.Header>
|
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
quantity: 10,
|
||||||
Edit Item Gacha
|
};
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Silakan mengubah detail dari item yang diperlukan
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<InputField
|
|
||||||
label="Nama Hadiah"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nama Hadiah"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Chance Rate"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Chance Rate"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Foto Barang"
|
|
||||||
type="file"
|
|
||||||
placeholder=".jpg, .jpeg, atau .png"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
|
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||||
Perbarui Item
|
|
||||||
</Button>
|
return (
|
||||||
</Modal.Content>
|
<>
|
||||||
</>
|
<Modal.Header>
|
||||||
);
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Edit Item Gacha
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Silakan mengubah detail dari item yang diperlukan
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex flex-col gap-8">
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Nama Hadiah"
|
||||||
|
type="text"
|
||||||
|
name="itemName"
|
||||||
|
placeholder="Masukkan Nama Hadiah"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Quantity"
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
placeholder="Masukkan Kuantitas Item"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Foto Barang"
|
||||||
|
type="file"
|
||||||
|
name="foto"
|
||||||
|
placeholder=".jpg, .jpeg, atau .png"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
Perbarui Item
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IStepTwoProps {
|
interface IStepTwoProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleEditItem?: () => void;
|
handleEditItem?: () => Promise<boolean>;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => (
|
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
|
||||||
<>
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
onClose,
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
resetStep,
|
||||||
Update Item
|
handleEditItem,
|
||||||
</h2>
|
{
|
||||||
<p className="text-p3 text-neutral-400">
|
success: 'Perubahan item berhasil dilakukan',
|
||||||
Apakah kamu yakin dengan
|
error: 'Perubahan item gagal dilakukan',
|
||||||
<br /> perubahan yang dilakukan?
|
}
|
||||||
</p>
|
);
|
||||||
</Modal.Header>
|
return (
|
||||||
<Modal.Content className="flex gap-4">
|
<>
|
||||||
<Button
|
<Modal.Header className="mb-0 text-center items-center">
|
||||||
variant="bordered"
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
size="lg"
|
Update Item
|
||||||
className="w-full"
|
</h2>
|
||||||
onClick={() => {
|
<p className="text-p3 text-neutral-400">
|
||||||
onClose();
|
Apakah kamu yakin dengan
|
||||||
resetStep();
|
<br /> perubahan yang dilakukan?
|
||||||
}}
|
</p>
|
||||||
>
|
</Modal.Header>
|
||||||
Batal
|
<Modal.Content className="flex gap-4">
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="bordered"
|
||||||
variant="primary"
|
size="lg"
|
||||||
size="lg"
|
className="w-full"
|
||||||
className="w-full"
|
onClick={onCancel}
|
||||||
onClick={() => {
|
>
|
||||||
handleEditItem && handleEditItem();
|
Batal
|
||||||
onClose();
|
</Button>
|
||||||
resetStep();
|
<Button
|
||||||
}}
|
variant="primary"
|
||||||
>
|
size="lg"
|
||||||
Update
|
className="w-full"
|
||||||
</Button>
|
onClick={onConfirm}
|
||||||
</Modal.Content>
|
>
|
||||||
</>
|
Update
|
||||||
);
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ModalEditItem;
|
export default ModalEditItem;
|
||||||
|
|||||||
@@ -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<TGachaItem>({
|
||||||
|
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<boolean>,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -31,20 +31,17 @@ export const Components: FC = (): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||||
{/* Dashboard Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
||||||
<div className="w-full flex flex-col gap-[40px]">
|
<div className="w-full flex flex-col gap-[40px]">
|
||||||
{/* Summary Section */}
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
||||||
Summary
|
Summary
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Participants */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<UsergroupAddOutlined className="text-[20px]" />
|
<UsergroupAddOutlined className="text-[20px]" />
|
||||||
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Roll and Reroll */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<ReloadOutlined className="text-[20px]" />
|
<ReloadOutlined className="text-[20px]" />
|
||||||
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Redeem */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<UserSwitchOutlined className="text-[20px]" />
|
<UserSwitchOutlined className="text-[20px]" />
|
||||||
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Inactive Users */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||||
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Gacha Items Section */}
|
|
||||||
<section className="flex flex-col gap-8">
|
<section className="flex flex-col gap-8">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-p2 font-medium text-primary-500">
|
<h2 className="text-p2 font-medium text-primary-500">
|
||||||
@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gacha Items List */}
|
|
||||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className="bg-white max-h-[80px] overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col py-4 px-6 gap-4">
|
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-p3 text-primary-500 font-medium">
|
<h3 className="text-p3 text-primary-500 font-medium">
|
||||||
Lanyard IMPHNEN
|
Lanyard IMPHNEN
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1">
|
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||||
<span>Prize {item}</span>
|
<span>Prize {item}</span>
|
||||||
<span>Chance Rate: (0.1%)</span>
|
<span>Quantity: 10</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start gap-2">
|
<div className="flex justify-start gap-2">
|
||||||
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
|
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
||||||
<img
|
|
||||||
src="gacha-clip.webp"
|
|
||||||
alt="Lanyard IMPHNEN"
|
|
||||||
className="h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -174,9 +161,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalAddItem}
|
isOpen={showModalAddItem}
|
||||||
onClose={() => setShowModalAddItem(false)}
|
onClose={() => setShowModalAddItem(false)}
|
||||||
handleAddItem={() => {
|
|
||||||
console.log('Item added');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
@@ -187,9 +171,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalEditItem}
|
isOpen={showModalEditItem}
|
||||||
onClose={() => setShowModalEditItem(false)}
|
onClose={() => setShowModalEditItem(false)}
|
||||||
handleEditItem={() => {
|
|
||||||
console.log('Item edited');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
@@ -199,9 +180,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<ModalDeleteItem
|
<ModalDeleteItem
|
||||||
isOpen={showModalDeleteItem}
|
isOpen={showModalDeleteItem}
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
handleDeleteItem={() => {
|
|
||||||
console.log('Item deleted');
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -184,9 +184,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalAddItem}
|
isOpen={showModalAddItem}
|
||||||
onClose={() => setShowModalAddItem(false)}
|
onClose={() => setShowModalAddItem(false)}
|
||||||
handleAddItem={() => {
|
|
||||||
console.log('Item added');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
@@ -195,9 +192,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalUpdateItem}
|
isOpen={showModalUpdateItem}
|
||||||
onClose={() => setShowModalUpdateItem(false)}
|
onClose={() => setShowModalUpdateItem(false)}
|
||||||
handleUpdateItem={() => {
|
|
||||||
console.log('Item updated');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
import { z } from 'zod';
|
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({
|
export const gachaRollItemSchema = z.object({
|
||||||
itemName: z
|
itemName: z
|
||||||
.string({
|
.string({
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
export type TGachaItem = {
|
||||||
|
itemName: string;
|
||||||
|
quantity: number;
|
||||||
|
foto?: File;
|
||||||
|
};
|
||||||
|
|
||||||
export type TGachaRollItem = {
|
export type TGachaRollItem = {
|
||||||
itemName: string;
|
itemName: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
@@ -17,16 +17,27 @@ export const ControlledInputField = <T extends FieldValues>(
|
|||||||
const { field, fieldState } = useController<T>(props);
|
const { field, fieldState } = useController<T>(props);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value =
|
let value;
|
||||||
props.type === 'number' ? Number(e.target.value) : e.target.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);
|
field.onChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputProps =
|
||||||
|
props.type === 'file'
|
||||||
|
? { ...props, ...field, value: undefined }
|
||||||
|
: { ...props, ...field };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputField
|
<InputField
|
||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
{...props}
|
{...inputProps}
|
||||||
{...field}
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user