Merge pull request #12 from IMPHNEN/feat/backoffice-modal

Slicing the design of the Modal component in Backoffice
This commit is contained in:
Maulana Sodiqin
2025-04-01 21:10:53 +08:00
committed by GitHub
18 changed files with 1369 additions and 904 deletions
@@ -0,0 +1,162 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useState } from 'react';
interface IModalEditAccount {
isOpen: boolean;
onClose: () => void;
handleEditAccount?: () => void;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalEditAccount = ({
isOpen,
onClose,
currentStep,
nextStep,
prevStep,
resetStep,
handleEditAccount,
}: IModalEditAccount) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleEditAccount={handleEditAccount}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const [fullName, setFullName] = useState('Ahmad Wiyana');
const [email, setEmail] = useState('fullname23@gmail.com');
const [phoneNumber, setPhoneNumber] = useState('081904423804');
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Edit Data Akun
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputForm
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
size="lg"
className="w-full"
/>
<InputForm
label="Email"
type="text"
placeholder="Masukkan Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
size="lg"
className="w-full"
/>
<InputForm
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
size="lg"
className="w-full"
/>
<InputForm
label="Alamat"
type="text"
placeholder="Masukkan Alamat"
value={address}
onChange={(e) => setAddress(e.target.value)}
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
>
Perbarui Data
</Button>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleEditAccount?: () => void;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Data
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => {
onClose();
resetStep();
}}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => {
handleEditAccount && handleEditAccount();
onClose();
resetStep();
}}
>
Update
</Button>
</Modal.Content>
</>
);
export default ModalEditAccount;
+41 -14
View File
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { FC, ReactElement, useState } from 'react'; import { FC, Fragment, ReactElement, useState } from 'react';
import { import {
FilterOutlined, FilterOutlined,
SearchOutlined, SearchOutlined,
@@ -17,6 +17,8 @@ import {
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '../../hook/use-query-state';
interface Account { interface Account {
id: number; id: number;
@@ -35,6 +37,28 @@ const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
address: 'Jl. Pantai Cibaduyut Indah', address: 'Jl. Pantai Cibaduyut Indah',
})); }));
export const Components: FC = (): ReactElement => {
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const columns: ColumnDef<Account>[] = [ const columns: ColumnDef<Account>[] = [
{ {
id: 'select', id: 'select',
@@ -83,7 +107,7 @@ const columns: ColumnDef<Account>[] = [
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// handleEdit(row.id); setShowModalEditAccount(true);
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
@@ -93,15 +117,6 @@ const columns: ColumnDef<Account>[] = [
}, },
]; ];
export const Components: FC = (): ReactElement => {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const table = useReactTable({ const table = useReactTable({
data: mockData, data: mockData,
columns, columns,
@@ -119,12 +134,12 @@ export const Components: FC = (): ReactElement => {
}); });
return ( return (
<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">
{/* Header */} {/* 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">Data Akun</h1> <h1 className="text-p2 font-semibold">Data Akun</h1>
</header> </header>
{/* Account Table Section */} {/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md"> <section className="flex flex-col gap-6 p-8 bg-white rounded-md">
{/* Search and Filter */} {/* Search and Filter */}
@@ -138,7 +153,6 @@ export const Components: FC = (): ReactElement => {
<SearchOutlined /> <SearchOutlined />
</div> </div>
</div> </div>
<div className="relative"> <div className="relative">
<Button <Button
variant="primary" variant="primary"
@@ -157,11 +171,24 @@ export const Components: FC = (): ReactElement => {
)} )}
</div> </div>
</div> </div>
{/* Table */} {/* Table */}
<DataTable data={mockData} columns={columns} table={table} /> <DataTable data={mockData} columns={columns} table={table} />
</section> </section>
</main> </main>
{/* Modal Edit Account */}
<ModalEditAccount
currentStep={currentStep}
isOpen={showModalEditAccount}
onClose={() => setShowModalEditAccount(false)}
handleEditAccount={() => {
console.log('Account updated');
}}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
/>
</Fragment>
); );
}; };
@@ -0,0 +1,136 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalAddItem {
isOpen: boolean;
onClose: () => void;
handleAddItem: () => void;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalAddItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleAddItem,
}: IModalAddItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleAddItem={handleAddItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => (
<>
<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">
<InputForm
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputForm
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputForm
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}>
Tambahkan Item
</Button>
</Modal.Content>
</>
);
interface IStepTwoProps {
onClose: () => void;
handleAddItem?: () => void;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan item ini?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => {
onClose();
resetStep();
}}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => {
handleAddItem && handleAddItem();
onClose();
resetStep();
}}
>
Tambahkan
</Button>
</Modal.Content>
</>
);
export default ModalAddItem;
@@ -0,0 +1,62 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => void;
}
const ModalDeleteItem = ({
isOpen,
onClose,
handleDeleteItem,
}: IModalDeleteItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus item ini?
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
onClick={onClose}
>
Batal Hapus
</Button>
<Button
variant="danger"
size="lg"
className="w-full"
onClick={() => {
handleDeleteItem && handleDeleteItem();
}}
>
Hapus Item
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalDeleteItem;
@@ -0,0 +1,136 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalEditItem {
isOpen: boolean;
onClose: () => void;
handleEditItem?: () => void;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalEditItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleEditItem,
}: IModalEditItem) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
handleEditItem={handleEditItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => (
<>
<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">
<div className="flex flex-col gap-4">
<InputForm
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputForm
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputForm
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}>
Perbarui Item
</Button>
</Modal.Content>
</>
);
interface IStepTwoProps {
onClose: () => void;
handleEditItem?: () => void;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => {
onClose();
resetStep();
}}
>
Batal
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => {
handleEditItem && handleEditItem();
onClose();
resetStep();
}}
>
Update
</Button>
</Modal.Content>
</>
);
export default ModalEditItem;
+68 -3
View File
@@ -6,10 +6,30 @@ import {
UserSwitchOutlined, UserSwitchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, ReactElement } from 'react'; import { FC, Fragment, ReactElement, useState } from 'react';
import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '../../hook/use-query-state';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalEditItem, setShowModalEditItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
});
return ( return (
<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 */} {/* 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">
@@ -66,7 +86,9 @@ export const Components: FC = (): ReactElement => {
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3> <h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Inactive Users</p> <p className="text-label1 text-neutral-500">
Inactive Users
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -78,7 +100,12 @@ export const Components: FC = (): ReactElement => {
<h2 className="text-p2 font-medium text-primary-500"> <h2 className="text-p2 font-medium text-primary-500">
Gacha Items Gacha Items
</h2> </h2>
<Button variant="primary" size="sm" className="items-end gap-3"> <Button
variant="primary"
size="sm"
className="items-end gap-3"
onClick={() => setShowModalAddItem(true)}
>
<span>Tambah Item</span> <span>Tambah Item</span>
<PlusOutlined className="text-[16px]" /> <PlusOutlined className="text-[16px]" />
</Button> </Button>
@@ -106,6 +133,7 @@ export const Components: FC = (): ReactElement => {
variant="text" variant="text"
size="sm" size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500" className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => setShowModalEditItem(true)}
> >
Edit Edit
</Button> </Button>
@@ -113,6 +141,7 @@ export const Components: FC = (): ReactElement => {
variant="text" variant="text"
size="sm" size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700" className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => setShowModalDeleteItem(true)}
> >
Delete Delete
</Button> </Button>
@@ -139,6 +168,42 @@ export const Components: FC = (): ReactElement => {
/> />
</div> </div>
</main> </main>
{/* Modal Add Item */}
<ModalAddItem
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
handleAddItem={() => {
console.log('Item added');
}}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
/>
{/* Modal Edit Item */}
<ModalEditItem
currentStep={currentStep}
isOpen={showModalEditItem}
onClose={() => setShowModalEditItem(false)}
handleEditItem={() => {
console.log('Item edited');
}}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
/>
{/* Modal Delete Item */}
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={() => {
console.log('Item deleted');
}}
/>
</Fragment>
); );
}; };
@@ -0,0 +1,84 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalProcessDelivery {
isOpen: boolean;
onClose: () => void;
handleProcessDelivery?: () => void;
}
const ModalProcessDelivery = ({
isOpen,
onClose,
handleProcessDelivery,
}: IModalProcessDelivery) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Delivery Process
</h2>
<p className="text-p3 text-neutral-400">
Lakukan pengiriman hadiah gacha untuk pengguna di bawah ini, jika
sudah ubah status menjadi Delivered.
</p>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputForm
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
readOnly
/>
<InputForm
label="Item yang didapatkan"
type="text"
placeholder="Masukkan Nama Item"
value="Lanyard + ID Card"
size="lg"
className="w-full"
readOnly
/>
<InputForm
label="Alamat Pengiriman"
type="text"
placeholder="Masukkan Alamat Pengiriman"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
readOnly
/>
<InputForm
label="Status"
type="text"
placeholder="Isi Status Pengiriman"
value="Lanyard + ID Card"
size="lg"
className="w-full"
readOnly
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => handleProcessDelivery && handleProcessDelivery()}
>
Proses Pengiriman
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalProcessDelivery;
+50 -31
View File
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { FC, ReactElement, useState } from 'react'; import { FC, Fragment, ReactElement, useState } from 'react';
import { import {
FilterOutlined, FilterOutlined,
SearchOutlined, SearchOutlined,
@@ -17,13 +17,15 @@ import {
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import ModalProcessDelivery from './_components/modal-process-item';
type Status = 'valid' | 'invalid' | 'unchecked'; type OrderValid = 'valid' | 'invalid' | 'unchecked';
type Status = 'undelivered' | 'delivered';
interface Prize { interface Prize {
id: number; id: number;
name: string; name: string;
orderValid: Status; orderValid: OrderValid;
items: string; items: string;
address: string; address: string;
status: Status; status: Status;
@@ -46,16 +48,29 @@ const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
? 'invalid' ? 'invalid'
: i % 5 === 0 : i % 5 === 0
? 'unchecked' ? 'unchecked'
: 'valid') as Status, : 'valid') as OrderValid,
items: items[i % items.length], items: items[i % items.length],
address: 'Jl. Pantai Cibaduyut Indah', address: 'Jl. Pantai Cibaduyut Indah',
status: (i % 3 === 0 status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status,
? 'unchecked'
: i % 5 === 0
? 'invalid'
: 'valid') as Status,
})); }));
export const Components: FC = (): ReactElement => {
const [showModalProcessDelivery, setShowModalProcessDelivery] =
useState(false);
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const deliveryOptions = [
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
{ id: 'option1', value: 'delivered', label: 'Delivered' },
];
const columns: ColumnDef<Prize>[] = [ const columns: ColumnDef<Prize>[] = [
{ {
id: 'select', id: 'select',
@@ -89,12 +104,12 @@ const columns: ColumnDef<Prize>[] = [
accessorKey: 'orderValid', accessorKey: 'orderValid',
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.orderValid; const status = row.original.orderValid;
const statusColors: Record<Status, string> = { const statusColors: Record<OrderValid, string> = {
valid: 'bg-success-200 text-success-500', valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500', invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900', unchecked: 'bg-warning-200 text-warning-900',
}; };
const statusText: Record<Status, string> = { const statusText: Record<OrderValid, string> = {
valid: 'Valid', valid: 'Valid',
invalid: 'Invalid', invalid: 'Invalid',
unchecked: 'Unchecked', unchecked: 'Unchecked',
@@ -122,14 +137,12 @@ const columns: ColumnDef<Prize>[] = [
cell: ({ row }) => { cell: ({ row }) => {
const status = row.original.status; const status = row.original.status;
const statusColors: Record<Status, string> = { const statusColors: Record<Status, string> = {
valid: 'bg-success-200 text-success-500', delivered: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500', undelivered: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
}; };
const statusText: Record<Status, string> = { const statusText: Record<Status, string> = {
valid: 'Valid', delivered: 'Delivered',
invalid: 'Invalid', undelivered: 'Undelivered',
unchecked: 'Unchecked',
}; };
return ( return (
<div <div
@@ -148,7 +161,7 @@ const columns: ColumnDef<Prize>[] = [
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// handleUpdate(row.id); setShowModalProcessDelivery(true);
}} }}
className="flex items-center gap-2 w-full" className="flex items-center gap-2 w-full"
> >
@@ -158,15 +171,6 @@ const columns: ColumnDef<Prize>[] = [
}, },
]; ];
export const Components: FC = (): ReactElement => {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const table = useReactTable({ const table = useReactTable({
data: mockData, data: mockData,
columns, columns,
@@ -184,12 +188,12 @@ export const Components: FC = (): ReactElement => {
}); });
return ( return (
<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">
{/* Header */} {/* 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">Data Pengiriman Hadiah</h1> <h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
</header> </header>
{/* Account Table Section */} {/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md"> <section className="flex flex-col gap-6 p-8 bg-white rounded-md">
{/* Search and Filter */} {/* Search and Filter */}
@@ -203,7 +207,6 @@ export const Components: FC = (): ReactElement => {
<SearchOutlined /> <SearchOutlined />
</div> </div>
</div> </div>
<div className="relative"> <div className="relative">
<Button <Button
variant="primary" variant="primary"
@@ -216,16 +219,32 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
{showFilter && ( {showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg"> <div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} /> <Filter
options={deliveryOptions}
onClose={() => setShowFilter(false)}
onFilterChange={(value) => {
console.log('Selected filter:', value);
// Filter logic di sini
}}
/>
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* Table */} {/* Table */}
<DataTable data={mockData} columns={columns} table={table} /> <DataTable data={mockData} columns={columns} table={table} />
</section> </section>
</main> </main>
{/* Modal Process Delivery */}
<ModalProcessDelivery
isOpen={showModalProcessDelivery}
onClose={() => setShowModalProcessDelivery(false)}
handleProcessDelivery={() => {
console.log('Action ketika user menekan tombol Proses Pengiriman');
}}
/>
</Fragment>
); );
}; };
@@ -0,0 +1,61 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalValidate {
isOpen: boolean;
onClose: () => void;
handleValid?: () => void;
handleInvalid?: () => void;
}
const ModalValidate = ({
isOpen,
onClose,
handleValid,
handleInvalid,
}: IModalValidate) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Validasi Transaksi
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<InputForm
label="Nomor Transaksi"
type="text"
placeholder="Masukkan Nomor Transaksi"
value="2502133Y9AFVBO"
size="lg"
className="w-full"
/>
<div className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full border-danger-500 text-danger-500 hover:border-danger-700 hover:text-danger-700"
onClick={() => handleInvalid && handleInvalid()}
>
Tidak Valid
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => handleValid && handleValid()}
>
Valid
</Button>
</div>
</Modal.Content>
</Modal>
);
};
export default ModalValidate;
+40 -12
View File
@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { FC, ReactElement, useState } from 'react'; import { FC, Fragment, ReactElement, useState } from 'react';
import { import {
FilterOutlined, FilterOutlined,
SearchOutlined, SearchOutlined,
@@ -16,6 +16,7 @@ import {
useReactTable, useReactTable,
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import ModalValidate from './_components/modal-validate';
type TransactionStatus = 'valid' | 'invalid' | 'unchecked'; type TransactionStatus = 'valid' | 'invalid' | 'unchecked';
@@ -38,6 +39,23 @@ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
: 'valid') as TransactionStatus, : 'valid') as TransactionStatus,
})); }));
export const Components: FC = (): ReactElement => {
const [showModalValidate, setShowModalValidate] = useState(false);
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const validationOptions = [
{ id: 'option1', value: 'unchecked', label: 'Unchecked' },
{ id: 'option2', value: 'valid', label: 'Valid' },
{ id: 'option3', value: 'invalid', label: 'Invalid' },
];
const columns: ColumnDef<Transaction>[] = [ const columns: ColumnDef<Transaction>[] = [
{ {
id: 'select', id: 'select',
@@ -102,7 +120,7 @@ const columns: ColumnDef<Transaction>[] = [
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// handleUpdate(row.id); setShowModalValidate(true);
}} }}
className="flex items-center gap-2 w-full" className="flex items-center gap-2 w-full"
> >
@@ -112,15 +130,6 @@ const columns: ColumnDef<Transaction>[] = [
}, },
]; ];
export const Components: FC = (): ReactElement => {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const table = useReactTable({ const table = useReactTable({
data: mockTransactions, data: mockTransactions,
columns, columns,
@@ -138,6 +147,7 @@ export const Components: FC = (): ReactElement => {
}); });
return ( return (
<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">
{/* Header */} {/* 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">
@@ -170,7 +180,14 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
{showFilter && ( {showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg"> <div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} /> <Filter
options={validationOptions}
onClose={() => setShowFilter(false)}
onFilterChange={(value) => {
console.log('Selected filter:', value);
// Filter logic di sini
}}
/>
</div> </div>
)} )}
</div> </div>
@@ -180,6 +197,17 @@ export const Components: FC = (): ReactElement => {
<DataTable data={mockTransactions} columns={columns} table={table} /> <DataTable data={mockTransactions} columns={columns} table={table} />
</section> </section>
</main> </main>
<ModalValidate
isOpen={showModalValidate}
onClose={() => setShowModalValidate(false)}
handleValid={() => {
console.log('Action ketika user klik Valid');
}}
handleInvalid={() => {
console.log('Action ketika user klik Tidak Valid');
}}
/>
</Fragment>
); );
}; };
@@ -0,0 +1,71 @@
import { useCallback, useEffect, useState } from 'react';
interface UseQueryStateOptions {
defaultValue: number;
maxValue?: number;
minValue?: number;
}
export const useQueryState = (key: string, options: UseQueryStateOptions) => {
const { defaultValue, maxValue = Infinity, minValue = 1 } = options;
const getQueryParam = (param: string): string | null => {
if (typeof window === 'undefined') return null;
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get(param);
};
const setQueryParam = (param: string, value: string) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(param, value);
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
window.history.replaceState(null, '', newUrl);
};
const initialValue = () => {
const queryValue = getQueryParam(key);
const parsedValue = queryValue ? parseInt(queryValue, 10) : defaultValue;
return Math.max(minValue, Math.min(maxValue, parsedValue));
};
const [value, setValue] = useState<number>(initialValue);
useEffect(() => {
const handlePopState = () => {
const queryValue = getQueryParam(key);
const newValue = queryValue ? parseInt(queryValue, 10) : defaultValue;
setValue(Math.max(minValue, Math.min(maxValue, newValue)));
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [key, defaultValue, minValue, maxValue]);
const updateValue = useCallback(
(newValue: number) => {
const constrainedValue = Math.max(minValue, Math.min(maxValue, newValue));
setValue(constrainedValue);
setQueryParam(key, constrainedValue.toString());
},
[key, minValue, maxValue]
);
const nextStep = useCallback(() => {
updateValue(value + 1);
}, [value, updateValue]);
const prevStep = useCallback(() => {
updateValue(value - 1);
}, [value, updateValue]);
const resetStep = useCallback(() => {
updateValue(defaultValue);
}, [defaultValue, updateValue]);
return {
step: value,
nextStep,
prevStep,
resetStep,
};
};
+1 -1
View File
@@ -9,7 +9,7 @@ import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import
import { cn } from '@imphnen-frontend-service/utils'; import { cn } from '@imphnen-frontend-service/utils';
import { Button } from '../button'; import { Button } from '../button';
type TInputType = 'text' | 'email' | 'password'; type TInputType = 'text' | 'email' | 'password' | 'file';
type TInputSize = 'sm' | 'md' | 'lg'; type TInputSize = 'sm' | 'md' | 'lg';
type TInputProps = Omit< type TInputProps = Omit<
@@ -7,7 +7,7 @@ import {
import { Input } from '../../atoms'; import { Input } from '../../atoms';
import { cn } from '@imphnen-frontend-service/utils'; import { cn } from '@imphnen-frontend-service/utils';
type TInputType = 'text' | 'email' | 'password'; type TInputType = 'text' | 'email' | 'password' | 'file';
type TInputSize = 'sm' | 'md' | 'lg'; type TInputSize = 'sm' | 'md' | 'lg';
type TInputFormProps = Omit< type TInputFormProps = Omit<
+29 -16
View File
@@ -42,13 +42,31 @@ const Radio = ({
interface FilterProps { interface FilterProps {
onClose?: () => void; onClose?: () => void;
options: Array<{
id: string;
value: string;
label: string;
}>;
selectedValue?: string;
onFilterChange?: (value: string) => void;
title?: string;
} }
export const Filter = ({ onClose }: FilterProps) => { export const Filter = ({
const [selectedStatus, setSelectedStatus] = useState('delivered'); onClose,
options,
selectedValue,
onFilterChange,
title = 'Status',
}: FilterProps) => {
const [selectedStatus, setSelectedStatus] = useState(
selectedValue || options[0]?.value || ''
);
const handleStatusChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleStatusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedStatus(e.target.value); const newValue = e.target.value;
setSelectedStatus(newValue);
onFilterChange?.(newValue);
}; };
return ( return (
@@ -63,24 +81,19 @@ export const Filter = ({ onClose }: FilterProps) => {
</button> </button>
</div> </div>
<hr className="border-primary-200" /> <hr className="border-primary-200" />
<span className="font-semibold text-primary-500">Status</span> <span className="font-semibold text-primary-500">{title}</span>
<div className="flex flex-col gap-[10px]"> <div className="flex flex-col gap-[10px]">
{options.map((option) => (
<Radio <Radio
id="option1" key={option.id}
id={option.id}
name="status" name="status"
value="undelivered" value={option.value}
label="Undelivered" label={option.label}
checked={selectedStatus === 'undelivered'} checked={selectedStatus === option.value}
onChange={handleStatusChange}
/>
<Radio
id="option2"
name="status"
value="delivered"
label="Delivered"
checked={selectedStatus === 'delivered'}
onChange={handleStatusChange} onChange={handleStatusChange}
/> />
))}
</div> </div>
</div> </div>
); );
@@ -1 +0,0 @@
export * from './modal-backoffice';
@@ -1,25 +0,0 @@
import { ReactElement, ReactNode } from 'react';
interface ModalBackofficeProps {
children: ReactNode;
className?: string;
}
export const ModalBackoffice = ({
children,
className,
...rest
}: ModalBackofficeProps): ReactElement => {
return (
<div
className={`w-[400px] bg-primary-50 rounded-lg p-[40px] gap-[32px] ${
className || ''
}`}
{...rest}
>
{children}
</div>
);
};
export default ModalBackoffice;
@@ -1,373 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ModalBackoffice } from './modal-backoffice';
import { InputForm } from '../../molecules';
import { Button } from '../../atoms';
const meta: Meta<typeof ModalBackoffice> = {
title: 'Organisms/Modal Backoffice',
component: ModalBackoffice,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ModalBackoffice>;
export default meta;
type Story = StoryObj<typeof ModalBackoffice>;
export const Default: Story = {
args: {
// isOpen: true,
// onClose: () => console.log('Modal closed'),
// title: 'Sample Modal',
children: (
<>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Modal Title
</h2>
<p className="text-p3 text-neutral-400">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas repellat
rerum doloremque dolorem fugit architecto eos blanditiis ratione
facere? Esse dolores inventore deleniti. Dicta voluptatem dolore vel
quia amet dolorem?
</p>
<div className="flex flex-col gap-4"></div>
</>
),
},
};
export const TambahItemGacha: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<div>
<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>
</div>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputForm
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputForm
label="Foto Barang"
type="text"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Tambahkan Item
</Button>
</div>
),
},
};
export const TambahItem: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<div>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin ingin
<br /> menambahkan item ini?
</p>
</div>
<div className="flex gap-4">
<Button variant="bordered" size="lg" className="w-full">
Batal
</Button>
<Button variant="primary" size="lg" className="w-full">
Tambahkan
</Button>
</div>
</div>
),
},
};
export const EditItemGacha: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<div>
<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>
</div>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputForm
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputForm
label="Foto Barang"
type="text"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Perbarui Item
</Button>
</div>
),
},
};
export const UpdateItem: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<div>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</div>
<div className="flex gap-4">
<Button variant="bordered" size="lg" className="w-full">
Batal
</Button>
<Button variant="primary" size="lg" className="w-full">
Update
</Button>
</div>
</div>
),
},
};
export const DeleteItem: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div>
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Delete Item
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus item ini?
</p>
</div>
<div className="flex gap-4">
<Button variant="secondary" size="lg" className="w-full">
Batal Hapus
</Button>
<Button variant="danger" size="lg" className="w-full">
Hapus Item
</Button>
</div>
</div>
),
},
};
export const EditDataAkun: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Edit Data Akun
</h2>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
/>
<InputForm
label="Email"
type="text"
placeholder="Masukkan Email"
value="fullname23@gmail.com"
size="lg"
className="w-full"
/>
<InputForm
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value="081904423804"
size="lg"
className="w-full"
/>
<InputForm
label="Alamat"
type="text"
placeholder="Masukkan Alamat"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Perbarui Data
</Button>
</div>
),
},
};
export const UpdateDataAkun: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<div>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Data
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin dengan
<br /> perubahan yang dilakukan?
</p>
</div>
<div className="flex gap-4">
<Button variant="bordered" size="lg" className="w-full">
Batal
</Button>
<Button variant="primary" size="lg" className="w-full">
Update
</Button>
</div>
</div>
),
},
};
export const ValidasiTransaksi: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<h2 className="text-p1 font-semibold text-primary-500 ">
Validasi Transaksi
</h2>
<InputForm
label="Nomor Transaksi"
type="text"
placeholder="Masukkan Nomor Transaksi"
value="2502133Y9AFVBO"
size="lg"
className="w-full"
/>
<div className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full border-danger-500 text-danger-500 hover:border-danger-700 hover:text-danger-700"
>
Tidak Valid
</Button>
<Button variant="primary" size="lg" className="w-full">
Valid
</Button>
</div>
</div>
),
},
};
export const ProsesDelivery: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<div>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Delivery Process
</h2>
<p className="text-p3 text-neutral-400">
Lakukan pengiriman hadiah gacha untuk pengguna di bawah ini, jika
sudah ubah status menjadi Delivered.
</p>
</div>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
/>
<InputForm
label="Item yang didapatkan"
type="text"
placeholder="Masukkan Nama Item"
value="Lanyard + ID Card"
size="lg"
className="w-full"
/>
<InputForm
label="Alamat Pengiriman"
type="text"
placeholder="Masukkan Alamat Pengiriman"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
/>
<InputForm
label="Status"
type="text"
placeholder="Isi Status Pengiriman"
value="Lanyard + ID Card"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Tambahkan Item
</Button>
</div>
),
},
};
+1 -1
View File
@@ -8,7 +8,7 @@
"gacha:prod": "serve ./dist/apps/gacha", "gacha:prod": "serve ./dist/apps/gacha",
"backoffice:dev": "nx serve backoffice", "backoffice:dev": "nx serve backoffice",
"backoffice:build": "nx build backoffice", "backoffice:build": "nx build backoffice",
"backoffice:prod": "serve ./dist/apps/gacha", "backoffice:prod": "serve ./dist/apps/backoffice",
"dimentorin:dev": "nx serve dimentorin", "dimentorin:dev": "nx serve dimentorin",
"dimentorin:build": "nx build dimentorin", "dimentorin:build": "nx build dimentorin",
"dimentorin:prod": "serve ./dist/apps/dimentorin", "dimentorin:prod": "serve ./dist/apps/dimentorin",