Compare commits

..
Author SHA1 Message Date
egagofur 280afd68ab fix: template github 2025-04-03 19:51:31 +07:00
egagofur 9777b7bea9 feat: add toast library 2025-04-03 19:40:19 +07:00
egagofur 6bad504d44 feat: change wording validation 2025-04-03 19:40:04 +07:00
egagofur 6b959a2f22 feat: change from dummy button to navbar button 2025-04-03 19:39:53 +07:00
Maulana Sodiqin f8f796e071 chore: fix global error build 2025-04-03 12:31:16 +07:00
Maulana SodiqinandGitHub 9c7b8c1c63 chore: kesalahan pemula 2025-04-03 12:27:22 +07:00
Maulana SodiqinandGitHub 2bd767d20b chore: kesalahan pemula 2025-04-03 12:26:45 +07:00
Maulana Sodiqin a6fea630a4 feat: login example 2025-04-03 12:21:10 +07:00
Maulana Sodiqin c05061becf feat: login example 2025-04-03 12:21:08 +07:00
Maulana SodiqinandGitHub c70e5706bc Merge pull request #13 from IMPHNEN/feat/register-form
Feat/register form
2025-04-01 21:16:59 +08:00
Maulana SodiqinandGitHub 49ca2a9fb0 Merge pull request #12 from IMPHNEN/feat/backoffice-modal
Slicing the design of the Modal component in Backoffice
2025-04-01 21:10:53 +08:00
Hafid Nur d7a90f47d0 feat: dynamic radio button for Filter component 2025-03-31 22:46:09 +07:00
Hafid Nur 984a31c587 fix: create custom hook and stepper to make multi-step modal functional
- Tambah custom hook 'use-query-state' dan implementasi stepper pada modal dengan tahapan lebih dari 1
- Hapus component ModalBackoffice karena sudah menggunakan component umum Modal
2025-03-31 22:36:37 +07:00
Hafid Nur a575e534b5 feat: slicing modal for backoffice 'data akun' page 2025-03-31 21:53:18 +07:00
Hafid Nur 989503f930 feat: slicing modal for backoffice 'validasi transaksi' page
- Tambah fungsi untuk handle aksi ketika button pada modal ditekan
2025-03-31 21:40:00 +07:00
Hafid Nur 3b979119ed feat: slicing modal for backoffice 'data pengiriman hadiah' page 2025-03-31 17:32:30 +07:00
Hafid Nur ecec423d30 feat: slicing modal for backoffice dashboard page 2025-03-31 01:33:18 +07:00
Hafid Nur 3bbdba7015 fix: add type=file on Input & InputForm components
Juga fix script untuk serve backoffice
2025-03-31 01:13:21 +07:00
Hafid Nur f7a7acaf65 feat(backoffice): add modal delete gacha item 2025-03-31 00:30:05 +07:00
59 changed files with 2446 additions and 1741 deletions
+6
View File
@@ -45,3 +45,9 @@ vite.config.*.timestamp*
vitest.config.*.timestamp* vitest.config.*.timestamp*
storybook-static storybook-static
.env
.env.prod
.env.develop
.env.staging
@@ -0,0 +1,162 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, 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">
<InputField
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Email"
type="text"
placeholder="Masukkan Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
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;
+126 -99
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,65 +37,20 @@ const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
address: 'Jl. Pantai Cibaduyut Indah', address: 'Jl. Pantai Cibaduyut Indah',
})); }));
const columns: ColumnDef<Account>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
header: 'No',
accessorKey: 'id',
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
},
{
header: 'Email',
accessorKey: 'email',
},
{
header: 'Nomor Telp',
accessorKey: 'phone',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
// handleEdit(row.id);
}}
className="flex items-center gap-2"
>
<EditOutlined /> Edit
</Button>
),
},
];
export const Components: FC = (): ReactElement => { 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>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
@@ -102,6 +59,64 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false);
const columns: ColumnDef<Account>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
header: 'No',
accessorKey: 'id',
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
},
{
header: 'Email',
accessorKey: 'email',
},
{
header: 'Nomor Telp',
accessorKey: 'phone',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowModalEditAccount(true);
}}
className="flex items-center gap-2"
>
<EditOutlined /> Edit
</Button>
),
},
];
const table = useReactTable({ const table = useReactTable({
data: mockData, data: mockData,
columns, columns,
@@ -119,49 +134,61 @@ export const Components: FC = (): ReactElement => {
}); });
return ( return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <Fragment>
{/* Header */} <main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4"> {/* Header */}
<h1 className="text-p2 font-semibold">Data Akun</h1> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
</header> <h1 className="text-p2 font-semibold">Data Akun</h1>
</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 */}
<div className="flex justify-between items-center gap-8 mb-2"> <div className="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
placeholder="Cari berdasarkan nama lengkap, email" placeholder="Cari berdasarkan nama lengkap, email"
className="pl-12 w-full max-h-full" className="pl-12 w-full max-h-full"
/> />
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]"> <div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined /> <SearchOutlined />
</div>
</div>
<div className="relative">
<Button
variant="primary"
size="md"
className="flex items-center gap-3"
disabled
onClick={() => setShowFilter(!showFilter)}
>
<FilterOutlined />
Filters
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} />
</div>
)}
</div> </div>
</div> </div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
<div className="relative"> {/* Modal Edit Account */}
<Button <ModalEditAccount
variant="primary" currentStep={currentStep}
size="md" isOpen={showModalEditAccount}
className="flex items-center gap-3" onClose={() => setShowModalEditAccount(false)}
disabled handleEditAccount={() => {
onClick={() => setShowFilter(!showFilter)} console.log('Account updated');
> }}
<FilterOutlined /> nextStep={nextStep}
Filters prevStep={prevStep}
</Button> resetStep={resetStep}
{showFilter && ( />
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg"> </Fragment>
<Filter onClose={() => setShowFilter(false)} />
</div>
)}
</div>
</div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
); );
}; };
@@ -0,0 +1,136 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, 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">
<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}>
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 { InputField, 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">
<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}>
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;
+185 -120
View File
@@ -6,139 +6,204 @@ 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 (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <Fragment>
{/* Dashboard Header */} <main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4"> {/* Dashboard Header */}
<h1 className="text-p2 font-semibold">Dashboard</h1> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
</header> <h1 className="text-p2 font-semibold">Dashboard</h1>
</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 */} {/* 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>
<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="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<UsergroupAddOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Participants</p>
</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="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<ReloadOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">
Roll and Reroll
</p>
</div>
</div>
{/* Redeem */}
<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">
<UserSwitchOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Redeem</p>
</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="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<UsergroupDeleteOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Inactive Users</p>
</div>
</div>
</div>
</section>
{/* Gacha Items Section */}
<section className="flex flex-col gap-8">
<div className="flex justify-between items-center">
<h2 className="text-p2 font-medium text-primary-500">
Gacha Items
</h2> </h2>
<Button variant="primary" size="sm" className="items-end gap-3"> <div className="grid grid-cols-2 gap-4">
<span>Tambah Item</span> {/* Participants */}
<PlusOutlined className="text-[16px]" /> <div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
</Button> <div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
</div> <UsergroupAddOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Participants</p>
</div>
</div>
{/* Gacha Items List */} {/* Roll and Reroll */}
<div className="flex flex-col gap-4 max-h-140 overflow-auto"> <div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
{[1, 2, 3, 4, 5, 6].map((item) => ( <div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<div <ReloadOutlined className="text-[20px]" />
key={item} </div>
className="bg-white max-h-[80px] overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100" <div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">
Roll and Reroll
</p>
</div>
</div>
{/* Redeem */}
<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">
<UserSwitchOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Redeem</p>
</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="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<UsergroupDeleteOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">
Inactive Users
</p>
</div>
</div>
</div>
</section>
{/* Gacha Items Section */}
<section className="flex flex-col gap-8">
<div className="flex justify-between items-center">
<h2 className="text-p2 font-medium text-primary-500">
Gacha Items
</h2>
<Button
variant="primary"
size="sm"
className="items-end gap-3"
onClick={() => setShowModalAddItem(true)}
> >
<div className="flex flex-col py-4 px-6 gap-4"> <span>Tambah Item</span>
<div> <PlusOutlined className="text-[16px]" />
<h3 className="text-p3 text-primary-500 font-medium"> </Button>
Lanyard IMPHNEN </div>
</h3>
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1"> {/* Gacha Items List */}
<span>Prize {item}</span> <div className="flex flex-col gap-4 max-h-140 overflow-auto">
<span>Chance Rate: (0.1%)</span> {[1, 2, 3, 4, 5, 6].map((item) => (
<div
key={item}
className="bg-white max-h-[80px] 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>
<h3 className="text-p3 text-primary-500 font-medium">
Lanyard IMPHNEN
</h3>
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1">
<span>Prize {item}</span>
<span>Chance Rate: (0.1%)</span>
</div>
</div>
<div className="flex justify-start gap-2">
<Button
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => setShowModalEditItem(true)}
>
Edit
</Button>
<Button
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => setShowModalDeleteItem(true)}
>
Delete
</Button>
</div> </div>
</div> </div>
<div className="flex justify-start gap-2">
<Button {/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
variant="text" <img
size="sm" src="gacha-clip.webp"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500" alt="Lanyard IMPHNEN"
> className="h-full"
Edit />
</Button>
<Button
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
>
Delete
</Button>
</div>
</div> </div>
))}
</div>
</section>
</div>
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */} {/* Right-side illustration */}
<img <img
src="gacha-clip.webp" src="gacha.webp"
alt="Lanyard IMPHNEN" alt=""
className="h-full" className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
/> />
</div>
))}
</div>
</section>
</div> </div>
</main>
{/* Right-side illustration */} {/* Modal Add Item */}
<img <ModalAddItem
src="gacha.webp" currentStep={currentStep}
alt="" isOpen={showModalAddItem}
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover" onClose={() => setShowModalAddItem(false)}
/> handleAddItem={() => {
</div> console.log('Item added');
</main> }}
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>
); );
}; };
+3 -3
View File
@@ -1,7 +1,7 @@
import { FC, ReactElement } from 'react'; import { FC, ReactElement } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm } from '@imphnen-frontend-service/ui/molecules'; import { InputField } from '@imphnen-frontend-service/ui/molecules';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -13,14 +13,14 @@ export const Components: FC = (): ReactElement => {
<h1 className="text-primary-500 text-p1 font-semibold"> <h1 className="text-primary-500 text-p1 font-semibold">
Welcome to IMPHNEN Backoffice Welcome to IMPHNEN Backoffice
</h1> </h1>
<InputForm <InputField
label="Email" label="Email"
placeholder="Masukkan Email" placeholder="Masukkan Email"
type="email" type="email"
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<InputForm <InputField
label="Password" label="Password"
placeholder="Masukkan password" placeholder="Masukkan password"
type="password" type="password"
@@ -0,0 +1,84 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, 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">
<InputField
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
readOnly
/>
<InputField
label="Item yang didapatkan"
type="text"
placeholder="Masukkan Nama Item"
value="Lanyard + ID Card"
size="lg"
className="w-full"
readOnly
/>
<InputField
label="Alamat Pengiriman"
type="text"
placeholder="Masukkan Alamat Pengiriman"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
readOnly
/>
<InputField
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;
+169 -150
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,119 +48,16 @@ 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,
})); }));
const columns: ColumnDef<Prize>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
header: 'No',
accessorKey: 'id',
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
},
{
header: 'Order Valid?',
accessorKey: 'orderValid',
cell: ({ row }) => {
const status = row.original.orderValid;
const statusColors: Record<Status, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<Status, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Items',
accessorKey: 'items',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<Status, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<Status, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
// handleUpdate(row.id);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Process
</Button>
),
},
];
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [showModalProcessDelivery, setShowModalProcessDelivery] =
useState(false);
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
@@ -167,6 +66,111 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false);
const deliveryOptions = [
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
{ id: 'option1', value: 'delivered', label: 'Delivered' },
];
const columns: ColumnDef<Prize>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
header: 'No',
accessorKey: 'id',
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
},
{
header: 'Order Valid?',
accessorKey: 'orderValid',
cell: ({ row }) => {
const status = row.original.orderValid;
const statusColors: Record<OrderValid, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<OrderValid, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Items',
accessorKey: 'items',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<Status, string> = {
delivered: 'bg-success-200 text-success-500',
undelivered: 'bg-danger-200 text-danger-500',
};
const statusText: Record<Status, string> = {
delivered: 'Delivered',
undelivered: 'Undelivered',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowModalProcessDelivery(true);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Process
</Button>
),
},
];
const table = useReactTable({ const table = useReactTable({
data: mockData, data: mockData,
columns, columns,
@@ -184,48 +188,63 @@ export const Components: FC = (): ReactElement => {
}); });
return ( return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <Fragment>
{/* Header */} <main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4"> {/* Header */}
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
</header> <h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
</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 */}
<div className="flex justify-between items-center gap-8 mb-2"> <div className="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee" placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
className="pl-12 w-full max-h-full" className="pl-12 w-full max-h-full"
/> />
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]"> <div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined /> <SearchOutlined />
</div>
</div>
<div className="relative">
<Button
variant="primary"
size="md"
className="flex items-center gap-3"
onClick={() => setShowFilter(!showFilter)}
>
<FilterOutlined />
Filters
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter
options={deliveryOptions}
onClose={() => setShowFilter(false)}
onFilterChange={(value) => {
console.log('Selected filter:', value);
// Filter logic di sini
}}
/>
</div>
)}
</div> </div>
</div> </div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
<div className="relative"> {/* Modal Process Delivery */}
<Button <ModalProcessDelivery
variant="primary" isOpen={showModalProcessDelivery}
size="md" onClose={() => setShowModalProcessDelivery(false)}
className="flex items-center gap-3" handleProcessDelivery={() => {
onClick={() => setShowFilter(!showFilter)} console.log('Action ketika user menekan tombol Proses Pengiriman');
> }}
<FilterOutlined /> />
Filters </Fragment>
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} />
</div>
)}
</div>
</div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
); );
}; };
@@ -0,0 +1,61 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, 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">
<InputField
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;
+141 -113
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,81 +39,9 @@ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
: 'valid') as TransactionStatus, : 'valid') as TransactionStatus,
})); }));
const columns: ColumnDef<Transaction>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
header: 'No',
accessorKey: 'id',
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
},
{
header: 'Nomor Transaksi',
accessorKey: 'transactionNumber',
},
{
header: 'Order Valid?',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<TransactionStatus, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<TransactionStatus, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
// handleUpdate(row.id);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Update
</Button>
),
},
];
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [showModalValidate, setShowModalValidate] = useState(false);
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 9, pageSize: 9,
@@ -121,6 +50,86 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false); 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>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
header: 'No',
accessorKey: 'id',
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
},
{
header: 'Nomor Transaksi',
accessorKey: 'transactionNumber',
},
{
header: 'Order Valid?',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<TransactionStatus, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<TransactionStatus, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowModalValidate(true);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Update
</Button>
),
},
];
const table = useReactTable({ const table = useReactTable({
data: mockTransactions, data: mockTransactions,
columns, columns,
@@ -138,48 +147,67 @@ export const Components: FC = (): ReactElement => {
}); });
return ( return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <Fragment>
{/* Header */} <main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4"> {/* Header */}
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
</header> <h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
</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 */}
<div className="flex justify-between items-center gap-8 mb-2"> <div className="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee" placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
className="pl-12 w-full max-h-full" className="pl-12 w-full max-h-full"
/> />
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]"> <div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined /> <SearchOutlined />
</div>
</div>
<div className="relative">
<Button
variant="primary"
size="md"
className="flex items-center gap-3"
onClick={() => setShowFilter(!showFilter)}
>
<FilterOutlined />
Filters
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter
options={validationOptions}
onClose={() => setShowFilter(false)}
onFilterChange={(value) => {
console.log('Selected filter:', value);
// Filter logic di sini
}}
/>
</div>
)}
</div> </div>
</div> </div>
<div className="relative"> {/* Table */}
<Button <DataTable data={mockTransactions} columns={columns} table={table} />
variant="primary" </section>
size="md" </main>
className="flex items-center gap-3" <ModalValidate
onClick={() => setShowFilter(!showFilter)} isOpen={showModalValidate}
> onClose={() => setShowModalValidate(false)}
<FilterOutlined /> handleValid={() => {
Filters console.log('Action ketika user klik Valid');
</Button> }}
{showFilter && ( handleInvalid={() => {
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg"> console.log('Action ketika user klik Tidak Valid');
<Filter onClose={() => setShowFilter(false)} /> }}
</div> />
)} </Fragment>
</div>
</div>
{/* Table */}
<DataTable data={mockTransactions} columns={columns} table={table} />
</section>
</main>
); );
}; };
+46 -18
View File
@@ -1,39 +1,67 @@
import { FC, ReactElement } from 'react'; import { FC, ReactElement } from 'react';
import { LoginBanner } from "@imphnen-frontend-service/ui/organisms" import { LoginBanner } from '@imphnen-frontend-service/ui/organisms';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { InputForm } from '@imphnen-frontend-service/ui/molecules'; import { InputField } from '@imphnen-frontend-service/ui/molecules';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const error = searchParams.get("error"); // Ambil nilai ?error= const error = searchParams.get('error'); // Ambil nilai ?error=
return ( return (
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'> <div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
<div className='bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6'> <div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
<LoginBanner /> <LoginBanner />
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center'> <div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
<div className='w-[404px]'> <div className="w-[404px]">
<h2 className='text-4xl font-semibold text-primary-500 text-center mb-2'>Hallo Minna-san</h2> <h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
<h5 className='text-xl font-medium text-primary-500 text-center'>Welcome to Dimentorin by IMPHNEN</h5> Hallo Minna-san
<InputForm label='Email' error={error ? error : undefined} size='lg' className='w-full' placeholder='Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)' /> </h2>
<InputForm label="password" error={error ? error : undefined} className='w-full' size='lg' type='password' placeholder='Masukkan password rahasiamu!' /> <h5 className="text-xl font-medium text-primary-500 text-center">
<div className='flex justify-end my-5'> Welcome to Dimentorin by IMPHNEN
<a href="/auth/forgot" className='text-primary-500 font-medium'>Lupa Password ?</a> </h5>
<InputField
label="Email"
error={error ? error : undefined}
size="lg"
className="w-full"
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
/>
<InputField
label="password"
error={error ? error : undefined}
className="w-full"
size="lg"
type="password"
placeholder="Masukkan password rahasiamu!"
/>
<div className="flex justify-end my-5">
<a href="/auth/forgot" className="text-primary-500 font-medium">
Lupa Password ?
</a>
</div> </div>
<Button className='w-full'>Enter Isekai</Button> <Button className="w-full">Enter Isekai</Button>
<div className='flex my-3 gap-3 justify-center'> <div className="flex my-3 gap-3 justify-center">
<h5>Belum Punya akun ?</h5> <h5>Belum Punya akun ?</h5>
<a href="/auth/register" className='text-primary-500 font-medium'>Daftar Disini</a> <a href="/auth/register" className="text-primary-500 font-medium">
Daftar Disini
</a>
</div> </div>
<div className="flex items-center w-full"> <div className="flex items-center w-full">
<div className="flex-grow border-t border-blue-400 opacity-50"></div> <div className="flex-grow border-t border-blue-400 opacity-50"></div>
<span className="px-3 text-blue-400">Or</span> <span className="px-3 text-blue-400">Or</span>
<div className="flex-grow border-t border-blue-400 opacity-50"></div> <div className="flex-grow border-t border-blue-400 opacity-50"></div>
</div> </div>
<Button className='w-full my-3 text-gray-500 gap-2' variant='secondary'> <Button
className="w-full my-3 text-gray-500 gap-2"
variant="secondary"
>
<p>Log In With Google</p> <p>Log In With Google</p>
<img src="/image/33978ce5bed2da9bf9d73acad802182a.webp" alt="Google Icon" width={24}/> <img
src="/image/33978ce5bed2da9bf9d73acad802182a.webp"
alt="Google Icon"
width={24}
/>
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,11 +1,11 @@
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { import {
InputForm, InputField,
Modal, Modal,
Stepper, Stepper,
} from '@imphnen-frontend-service/ui/molecules'; } from '@imphnen-frontend-service/ui/molecules';
import { useQueryState } from '../../../hooks/use-query-state'; import { useQueryState } from '@imphnen-frontend-service/utils';
import { Fragment } from 'react/jsx-runtime';
interface IModalFormForgotPasswordProps { interface IModalFormForgotPasswordProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
@@ -62,7 +62,7 @@ interface IStepOneProps {
const StepOne = ({ nextStep, onClose }: IStepOneProps) => ( const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
<> <>
<InputForm <InputField
label="Email" label="Email"
placeholder="Masukkan Email yang Terdaftar" placeholder="Masukkan Email yang Terdaftar"
type="email" type="email"
@@ -86,9 +86,8 @@ interface IStepTwoProps {
} }
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => ( const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
<> <Fragment>
{/* TODO: Change component using OTP Input */} <InputField
<InputForm
label="Kode OTP" label="Kode OTP"
placeholder="Masukkan Kode OTP" placeholder="Masukkan Kode OTP"
type="text" type="text"
@@ -108,7 +107,7 @@ const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
Reset Password Reset Password
</Button> </Button>
</div> </div>
</> </Fragment>
); );
interface IStepThreeProps { interface IStepThreeProps {
@@ -118,14 +117,14 @@ interface IStepThreeProps {
const StepThree = ({ onClose, resetStep }: IStepThreeProps) => ( const StepThree = ({ onClose, resetStep }: IStepThreeProps) => (
<> <>
<InputForm <InputField
label="Password Baru" label="Password Baru"
placeholder="Masukkan Password Baru" placeholder="Masukkan Password Baru"
type="password" type="password"
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<InputForm <InputField
label="Ulang Password" label="Ulang Password"
placeholder="Masukkan Ulang Password" placeholder="Masukkan Ulang Password"
type="password" type="password"
@@ -1,18 +1,23 @@
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { Link } from 'react-router-dom'; import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { useLogin } from '../../_hooks/use-login';
interface IModalFormLogin { interface IModalFormLogin {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onForgotPassword: () => void; onForgotPassword: () => void;
setIsOpenRegisterModal: (value: boolean) => void;
} }
const ModalFormLogin = ({ const ModalFormLogin = ({
isOpen, isOpen,
onClose, onClose,
onForgotPassword, onForgotPassword,
setIsOpenRegisterModal,
}: IModalFormLogin) => { }: IModalFormLogin) => {
const { form, onSubmit } = useLogin();
return ( return (
<Modal <Modal
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7" className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
@@ -25,38 +30,45 @@ const ModalFormLogin = ({
Login Login
</h1> </h1>
</Modal.Header> </Modal.Header>
<Modal.Content className="space-y-4"> <Modal.Content>
<InputForm <form className="space-y-4" onSubmit={onSubmit}>
label="Email" <ControlledInputField
placeholder="Masukkan Email" control={form.control}
type="email" label="Email"
size="lg" placeholder="Masukkan Email"
className="w-full" type="email"
/> name="email"
<InputForm size="lg"
label="Password" className="w-full"
placeholder="Masukkan Password" />
type="password" <ControlledInputField
size="lg" control={form.control}
className="w-full" label="Password"
/> placeholder="Masukkan Password"
<h1 className="text-end text-primary-500 text-xl font-medium"> type="password"
<Button variant="text" onClick={onForgotPassword}> name="password"
Lupa Password? size="lg"
className="w-full"
/>
<h1 className="text-end text-primary-500 text-xl font-medium">
<Button variant="text" onClick={onForgotPassword}>
Lupa Password?
</Button>
</h1>
<Button type="submit" size="md" className="w-full">
Login
</Button> </Button>
</h1> <div className="flex justify-center gap-2 pt-2.5 font-medium">
<Button size="md" className="w-full"> <p className="text-neutral-500">Belum punya akun?</p>
Login <Button
</Button> variant="text"
<div className="flex justify-center gap-2 pt-2.5 font-medium"> className="text-primary-500 hover:text-primary-600 m-0 p-0"
<p className="text-neutral-500">Belum punya akun?</p> onClick={() => setIsOpenRegisterModal(true)}
<Link >
className="text-primary-500 hover:text-primary-600" Daftar
to="/register" </Button>
> </div>
Daftar </form>
</Link>
</div>
</Modal.Content> </Modal.Content>
</Modal> </Modal>
); );
@@ -1,6 +1,6 @@
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useQueryState } from '../../../hooks/use-query-state'; import { useQueryState } from '@imphnen-frontend-service/utils';
interface IModalFormRegisterProps { interface IModalFormRegisterProps {
isOpen: boolean; isOpen: boolean;
@@ -54,28 +54,28 @@ interface IStepOneProps {
const StepOne = ({ nextStep, onClose }: IStepOneProps) => ( const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
<> <>
<InputForm <InputField
label="Nama Lengkap" label="Nama Lengkap"
placeholder="Masukkan Nama Lengkap" placeholder="Masukkan Nama Lengkap"
type="text" type="text"
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<InputForm <InputField
label="Email" label="Email"
placeholder="Masukkan Email Anda" placeholder="Masukkan Email Anda"
type="email" type="email"
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<InputForm <InputField
label="Password" label="Password"
placeholder="Masukkan Password" placeholder="Masukkan Password"
type="password" type="password"
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<InputForm <InputField
label="Ulangi Password" label="Ulangi Password"
placeholder="Masukkan Ulang Password" placeholder="Masukkan Ulang Password"
type="password" type="password"
@@ -95,14 +95,14 @@ interface IStepTwoProps {
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => ( const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
<> <>
<InputForm <InputField
label="Nomor Telepon" label="Nomor Telepon"
placeholder="Masukkan Nomor Telepon Aktif" placeholder="Masukkan Nomor Telepon Aktif"
type="text" type="text"
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<InputForm <InputField
label="Alamat Pengiriman" label="Alamat Pengiriman"
placeholder="Masukkan Alamat Pengiriman" placeholder="Masukkan Alamat Pengiriman"
type="text" type="text"
@@ -0,0 +1,29 @@
import { cn } from '@imphnen-frontend-service/utils';
import { FC, ReactElement } from 'react';
type TGachaItem = {
src: string;
label: string;
className?: string;
};
export const GachaItem: FC<TGachaItem> = ({
src,
label,
className,
}): ReactElement => {
return (
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full overflow-hidden">
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
<img
src={src}
alt="Banner"
width={255}
height={255}
className={cn('h-[150px] md:h-[255px] object-contain', className)}
/>
</div>
<p className="font-semibold text-center md:text-p2">{label}</p>
</div>
);
};
+26
View File
@@ -0,0 +1,26 @@
import { useForm } from 'react-hook-form';
import {
authLoginSchema,
TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema),
mode: 'all',
});
const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
onSuccess: () => console.log('Success Login'),
});
});
return {
form,
onSubmit,
};
};
+9 -7
View File
@@ -1,14 +1,16 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { Navbar } from '@imphnen-frontend-service/ui/organisms'; import { Navbar } from '@imphnen-frontend-service/ui/organisms';
import { Outlet } from 'react-router-dom';
import { FC, ReactElement } from 'react';
import { ModalLoginProvider } from '@imphnen-frontend-service/utils';
export const AppLayout: FC = (): ReactElement => { export const AppLayout: FC = (): ReactElement => {
return ( return (
<main className="bg-primary-50 min-h-screen"> <ModalLoginProvider>
<Navbar /> <main className="bg-primary-50 min-h-screen">
<Outlet /> <Navbar />
</main> <Outlet />
</main>
</ModalLoginProvider>
); );
}; };
export default AppLayout; export default AppLayout;
+16 -77
View File
@@ -1,20 +1,15 @@
import { ArrowDownOutlined } from '@ant-design/icons'; import { ArrowDownOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { cn } from '@imphnen-frontend-service/utils'; import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
import ModalFormForgotPassword from './_components/form/modal-form-forgot-password'; import ModalFormForgotPassword from './_components/form/modal-form-forgot-password';
import ModalFormLogin from './_components/form/modal-form-login'; import ModalFormLogin from './_components/form/modal-form-login';
import ModalFormRegister from './_components/form/modal-form-register'; import ModalFormRegister from './_components/form/modal-form-register';
import { GachaItem } from './_components/item/gacha-item';
interface GachaItemProps { import { useModalLogin } from '@imphnen-frontend-service/utils';
src: string;
label: string;
className?: string;
}
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const { showModalLogin, setShowModalLogin } = useModalLogin();
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false); const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
const [showModalLogin, setShowModalLogin] = useState(false);
const [showModalRegister, setShowModalRegister] = useState(false); const [showModalRegister, setShowModalRegister] = useState(false);
const scrollToRoulette = () => { const scrollToRoulette = () => {
@@ -24,49 +19,6 @@ export const Components: FC = (): ReactElement => {
} }
}; };
useEffect(() => {
const gachaPlaySection = document.getElementById('gacha-play');
if (gachaPlaySection) {
let currentIndex = 0;
const items = gachaPlaySection.children;
const totalItems = items.length;
const scrollItems = () => {
if (currentIndex >= totalItems) {
currentIndex = 0;
}
items[currentIndex].scrollIntoView({
behavior: 'smooth',
inline: 'center',
});
currentIndex++;
};
const intervalId = setInterval(scrollItems, 2800);
return () => clearInterval(intervalId);
}
}, []);
const GachaItem: FC<GachaItemProps> = ({
src,
label,
className,
}): ReactElement => {
return (
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full">
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
<img
src={src}
alt=""
className={cn('h-[150px] md:h-[255px] object-contain', className)}
/>
</div>
<p className="font-semibold text-center md:text-p2">{label}</p>
</div>
);
};
const handleForgotPasswordClick = () => { const handleForgotPasswordClick = () => {
setShowModalLogin(false); setShowModalLogin(false);
setShowModalForgotPassword(true); setShowModalForgotPassword(true);
@@ -74,7 +26,6 @@ export const Components: FC = (): ReactElement => {
return ( return (
<Fragment> <Fragment>
{/* Landing Page */}
<section <section
id="landing-page" id="landing-page"
className="my-12 md:my-24 grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center max-w-[1280px] items-center justify-center mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto" className="my-12 md:my-24 grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center max-w-[1280px] items-center justify-center mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto"
@@ -137,22 +88,28 @@ export const Components: FC = (): ReactElement => {
~ 175k ~ ~ 175k ~
</div> </div>
</div> </div>
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative"> <div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative overflow-hidden">
<div className="relative h-[130px] md:h-[230px]"> <div className="relative h-[130px] md:h-[230px]">
<img <img
width={400}
height={400}
src="/merch/2.png" src="/merch/2.png"
alt="Merch 2" alt="Merch 2"
className="relative top-[5px] md:top-[8px] left-0 md:left-[5px] z-10 w-[165px] md:w-[277px]" className="relative top-[5px] md:top-[8px] left-0 md:left-[5px] z-10 w-[165px] md:w-[277px]"
/> />
<img <img
width={400}
height={400}
className="absolute top-0 -left-[5px] md:-left-[8px] z-0 min-w-[174px] md:min-w-[302px]" className="absolute top-0 -left-[5px] md:-left-[8px] z-0 min-w-[174px] md:min-w-[302px]"
src="/merch/Vector-2.svg" src="/merch/Vector-2.svg"
alt="" alt="Merch 3"
/> />
<img <img
width={400}
height={400}
className="absolute hidden lg:block -bottom-[100px] -left-[55px]" className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
src="/landing-arrow-2.svg" src="/landing-arrow-2.svg"
alt="" alt="Merch 1"
/> />
</div> </div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded"> <div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
@@ -163,7 +120,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</div> </div>
</section> </section>
{/* Roulette Page */}
<section <section
id="roulette" id="roulette"
className="mt-20 pb-50 lg:py-90 mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto lg:max-w-[1280px] grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center gap-y-16 md:gap-y-28" className="mt-20 pb-50 lg:py-90 mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto lg:max-w-[1280px] grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center gap-y-16 md:gap-y-28"
@@ -191,10 +148,7 @@ export const Components: FC = (): ReactElement => {
</ol> </ol>
</div> </div>
<div <div className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-4 md:gap-8 overflow-x-hidden">
id="roulette-spin"
className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-4 md:gap-8 overflow-x-hidden"
>
<div className="bg-white text-primary-500 font-medium text-p3 md:text-h3 shadow py-2 px-4 md:py-4 md:px-8 max-w-fit rounded-md md:rounded-lg"> <div className="bg-white text-primary-500 font-medium text-p3 md:text-h3 shadow py-2 px-4 md:py-4 md:px-8 max-w-fit rounded-md md:rounded-lg">
Here Take Your Prize Here Take Your Prize
</div> </div>
@@ -223,31 +177,17 @@ export const Components: FC = (): ReactElement => {
Spin Now Spin Now
</Button> </Button>
</div> </div>
{/* TODO: Change using real button trigger */}
<div className="flex gap-4 col-span-4 md:col-span-8 lg:col-span-6 justify-center items-center">
<Button onClick={() => setShowModalLogin(true)}>
Open Modal Login
</Button>
<Button onClick={() => setShowModalForgotPassword(true)}>
Open Modal Forgot Password
</Button>
<Button onClick={() => setShowModalRegister(true)}>
Open Modal Register
</Button>
</div>
</section> </section>
{/* Diffuser & Cloud */}
<div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div> <div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div>
{/* Login Modal */}
<ModalFormLogin <ModalFormLogin
isOpen={showModalLogin} isOpen={showModalLogin}
onClose={() => setShowModalLogin(false)} onClose={() => setShowModalLogin(false)}
onForgotPassword={handleForgotPasswordClick} onForgotPassword={handleForgotPasswordClick}
setIsOpenRegisterModal={setShowModalRegister}
key="login" key="login"
/> />
{/* Register Modal */}
<ModalFormRegister <ModalFormRegister
isOpen={showModalRegister} isOpen={showModalRegister}
onClose={() => { onClose={() => {
@@ -256,7 +196,6 @@ export const Components: FC = (): ReactElement => {
key="register" key="register"
/> />
{/* Forgot Password Modal */}
<ModalFormForgotPassword <ModalFormForgotPassword
isOpen={showModalForgotPassword} isOpen={showModalForgotPassword}
onClose={() => { onClose={() => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { api } from '@imphnen-frontend-service/utils'; import { api } from '../';
import { import {
TLoginRequest, TLoginRequest,
TLoginResponse, TLoginResponse,
+8
View File
@@ -1,3 +1,11 @@
import axios, { AxiosRequestConfig } from 'axios';
export * from './auth'; export * from './auth';
export * from './gacha'; export * from './gacha';
export * from './users'; export * from './users';
const config: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_URL,
};
export const api = axios.create(config);
+7
View File
@@ -6,6 +6,8 @@ import {
TRegisterRequest, TRegisterRequest,
TVerifyEmailRequest, TVerifyEmailRequest,
} from '../../types/auth'; } from '../../types/auth';
import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
import { TResponseError, TResponseMessage } from '../../types/common'; import { TResponseError, TResponseMessage } from '../../types/common';
export const usePostLogin = (): UseMutationResult< export const usePostLogin = (): UseMutationResult<
@@ -17,6 +19,11 @@ export const usePostLogin = (): UseMutationResult<
return useMutation({ return useMutation({
mutationKey: ['post-login'], mutationKey: ['post-login'],
mutationFn: async (payload) => await postLogin(payload), mutationFn: async (payload) => await postLogin(payload),
onSuccess: (res) => {
SessionUser.set(res.data.user);
SessionToken.set(res.data.token);
window.location.reload();
},
}); });
}; };
+1
View File
@@ -1,3 +1,4 @@
export * from './api'; export * from './api';
export * from './hooks'; export * from './hooks';
export * from './types'; export * from './types';
export * from './schemas';
+17
View File
@@ -0,0 +1,17 @@
import { z } from 'zod';
export const authLoginSchema = z.object({
email: z
.string({
required_error: 'Email tidak boleh kosong',
invalid_type_error: 'Email harus berupa string',
})
.min(1, 'Email tidak boleh kosong')
.email('Email harus valid'),
password: z
.string({
required_error: 'Password tidak boleh kosong',
invalid_type_error: 'Password harus berupa string',
})
.min(1, 'Password tidak boleh kosong'),
});
+1
View File
@@ -0,0 +1 @@
export * from './auth';
+3 -5
View File
@@ -1,3 +1,5 @@
import { TUserItem } from '../users';
export type TLoginRequest = { export type TLoginRequest = {
email: string; email: string;
password: string; password: string;
@@ -9,11 +11,7 @@ export type TLoginResponse = {
access_token: string; access_token: string;
refresh_token: string; refresh_token: string;
}; };
user: { user: TUserItem;
fullname: string;
email: string;
is_active: boolean;
};
}; };
}; };
+2
View File
@@ -1,3 +1,5 @@
export * from './auth'; export * from './auth';
export * from './gacha'; export * from './gacha';
export * from './users'; export * from './users';
export * from './roles';
export * from './permissions';
@@ -0,0 +1,6 @@
export type TPermissionItem = {
id: string;
name: string;
created_at: string;
updated_at: string;
};
+9
View File
@@ -0,0 +1,9 @@
import { TPermissionItem } from '../permissions';
export type TRoleItem = {
id: string;
name: string;
created_at: string;
updated_at: string;
permissions: TPermissionItem[];
};
+19 -1
View File
@@ -1 +1,19 @@
export {}; import { TRoleItem } from '../roles';
export type TUserItem = {
id: string;
avatar: string;
birthdate: string;
email: string;
fullname: string;
gender: string;
identity_number: string;
is_active: boolean;
is_profile_completed: boolean;
phone_number: string;
referral_code: string;
referred_by: string;
religion: string;
student_type: string;
role: TRoleItem;
};
+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<
+1 -1
View File
@@ -1,6 +1,6 @@
export * from './forgot-step'; export * from './forgot-step';
export * from './otp-form'; export * from './otp-form';
export * from './input-form'; export * from './input-field';
export * from './pagination'; export * from './pagination';
export * from './modal/modal'; export * from './modal/modal';
export * from './stepper'; export * from './stepper';
@@ -0,0 +1 @@
export * from './input-field';
@@ -1,9 +1,9 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import InputForm from './input-form'; import { InputField } from './input-field';
describe('InputForm Component', () => { describe('InputField Component', () => {
it('renders correctly with disabled prop', () => { it('renders correctly with disabled prop', () => {
render(<InputForm label="Test Label" disabled={true} />); render(<InputField label="Test Label" disabled={true} />);
const input = screen.getByLabelText('Test Label'); const input = screen.getByLabelText('Test Label');
expect(input).toBeDisabled(); expect(input).toBeDisabled();
@@ -11,7 +11,7 @@ describe('InputForm Component', () => {
}); });
it('renders correctly without disabled prop', () => { it('renders correctly without disabled prop', () => {
render(<InputForm label="Test Label" disabled={false} />); render(<InputField label="Test Label" disabled={false} />);
const input = screen.getByLabelText('Test Label'); const input = screen.getByLabelText('Test Label');
expect(input).not.toBeDisabled(); expect(input).not.toBeDisabled();
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { InputForm } from './input-form'; import { InputField } from './input-field';
const meta = { const meta = {
title: 'Molecules/Input Form', title: 'Molecules/Input Form',
component: InputForm, component: InputField,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
docs: { docs: {
@@ -27,7 +27,7 @@ Cek dan inspect element pada story With HtmlFor untuk melihat hasilnya.
}, },
}, },
tags: ['autodocs'], tags: ['autodocs'],
} satisfies Meta<typeof InputForm>; } satisfies Meta<typeof InputField>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
@@ -7,10 +7,9 @@ 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'; export type TInputType = 'text' | 'email' | 'password' | 'file';
type TInputSize = 'sm' | 'md' | 'lg'; export type TInputSize = 'sm' | 'md' | 'lg';
export type TInputFieldProps = Omit<
type TInputFormProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type' 'size' | 'type'
> & { > & {
@@ -19,7 +18,6 @@ type TInputFormProps = Omit<
size?: TInputSize; size?: TInputSize;
error?: string; error?: string;
disabled?: boolean; disabled?: boolean;
helperText?: string; helperText?: string;
htmlFor?: string; htmlFor?: string;
}; };
@@ -39,7 +37,7 @@ const sizeClasses: Record<TInputSize, { label: string; helperText: string }> = {
}, },
}; };
export const InputForm: FC<TInputFormProps> = ({ export const InputField: FC<TInputFieldProps> = ({
label, label,
placeholder, placeholder,
type = 'text', type = 'text',
@@ -77,7 +75,7 @@ export const InputForm: FC<TInputFormProps> = ({
{...rest} {...rest}
/> />
{error ? ( {error ? (
<p className="text-danger-500 text-xs mt-1">{error}</p> <p className="text-danger-500 text-xl mt-1">{error}</p>
) : ( ) : (
helperText && ( helperText && (
<p className={cn('text-cs mt-1', sizeClasses[size].helperText)}> <p className={cn('text-cs mt-1', sizeClasses[size].helperText)}>
@@ -88,5 +86,3 @@ export const InputForm: FC<TInputFormProps> = ({
</div> </div>
); );
}; };
export default InputForm;
@@ -1 +0,0 @@
export * from './input-form';
@@ -0,0 +1,21 @@
import {
InputField,
TInputFieldProps,
} from '@imphnen-frontend-service/ui/molecules';
import {
FieldValues,
useController,
UseControllerProps,
} from 'react-hook-form';
export type TControlledInputFieldProps<T extends FieldValues> =
UseControllerProps<T> & TInputFieldProps;
export const ControlledInputField = <T extends FieldValues>(
props: TControlledInputFieldProps<T>
) => {
const { field, fieldState } = useController<T>(props);
return (
<InputField error={fieldState.error?.message} {...{ ...props, ...field }} />
);
};
@@ -0,0 +1 @@
export * from './controlled-input-field';
+33 -20
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]">
<Radio {options.map((option) => (
id="option1" <Radio
name="status" key={option.id}
value="undelivered" id={option.id}
label="Undelivered" name="status"
checked={selectedStatus === 'undelivered'} value={option.value}
onChange={handleStatusChange} label={option.label}
/> checked={selectedStatus === option.value}
<Radio onChange={handleStatusChange}
id="option2" />
name="status" ))}
value="delivered"
label="Delivered"
checked={selectedStatus === 'delivered'}
onChange={handleStatusChange}
/>
</div> </div>
</div> </div>
); );
+6 -5
View File
@@ -1,6 +1,7 @@
export * from './navbar'; export * from './navbar';
export * from "./modals-gacha"; export * from './modals-gacha';
export * from "./auth-banner"; export * from './auth-banner';
export * from './backoffice-sidebar' export * from './backoffice-sidebar';
export * from './datatable' export * from './datatable';
export * from './filter' export * from './filter';
export * from './controlled-field';
@@ -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>
),
},
};
+2 -2
View File
@@ -163,7 +163,7 @@ describe('Navbar', () => {
} }
}); });
it('has correct ARIA role for navigation', () => { it('has correct ARIA role for nav', () => {
const { container }: RenderResult = render( const { container }: RenderResult = render(
<BrowserRouter> <BrowserRouter>
<Navbar /> <Navbar />
@@ -171,6 +171,6 @@ describe('Navbar', () => {
); );
const header: HTMLElement | null = container.querySelector('header'); const header: HTMLElement | null = container.querySelector('header');
expect(header).toHaveAttribute('role', 'navigation'); expect(header).toHaveAttribute('role', 'nav');
}); });
}); });
+34 -20
View File
@@ -1,16 +1,19 @@
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, ReactElement, useState } from 'react'; import { FC, ReactElement, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '../../atoms/button';
import { useModalLogin, useSession } from '@imphnen-frontend-service/utils';
export const Navbar: FC = (): ReactElement => { export const Navbar: FC = (): ReactElement => {
const [isDropdownOpen, setDropdownOpen] = useState(false); const { session, signOut, isAuthenticated } = useSession();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { setShowModalLogin } = useModalLogin();
return ( return (
<div className="bg-primary-50 w-full px-[32px] pt-[32px] md:px-[60px] md:pt-[60px] lg:px-[80px] sticky top-0 z-50"> <div className="bg-primary-50 w-full px-[32px] pt-[32px] md:px-[60px] md:pt-[60px] lg:px-[80px] sticky top-0 z-50">
<header <header
className="bg-white shadow-lg rounded-lg min-h-[47px] max-h-[47px] md:min-h-[60px] md:max-h-[60px] lg:min-h-[71px] lg:max-h-[71px] flex justify-between w-full max-w-[1280px] xl:mx-auto" className="bg-white shadow-lg rounded-lg min-h-[47px] max-h-[47px] md:min-h-[60px] md:max-h-[60px] lg:min-h-[71px] lg:max-h-[71px] flex justify-between w-full max-w-[1280px] xl:mx-auto"
role="navigation" role="nav"
> >
<div className="flex w-full items-center justify-between p-4 md:px-[32px] md:py-[10px]"> <div className="flex w-full items-center justify-between p-4 md:px-[32px] md:py-[10px]">
<div className="flex items-center"> <div className="flex items-center">
@@ -40,20 +43,27 @@ export const Navbar: FC = (): ReactElement => {
<Link to="#">Merch Gacha</Link> <Link to="#">Merch Gacha</Link>
</Button> </Button>
</li> </li>
<li> {!isAuthenticated ? (
<Button <li>
size="md" <Button onClick={() => setShowModalLogin(true)}>Login</Button>
className="lg:text-[19px] lg:max-h-[44px] text-neutral-50 hover:text-neutral-200 transition-colors" </li>
> ) : (
<Link to="/login">Login</Link> <li className="flex gap-x-4">
</Button> <span className="text-lg">{session.user?.fullname}</span>
</li> <div
onClick={signOut}
className="text-lg text-red-500 font-bold"
>
Logout
</div>
</li>
)}
</ul> </ul>
<button <button
className={`md:hidden duration-200 ${ className={`md:hidden duration-200 ${
isDropdownOpen ? 'transform rotate-90' : '' isDropdownOpen ? 'transform rotate-90' : ''
}`} }`}
onClick={() => setDropdownOpen(!isDropdownOpen)} onClick={() => setIsDropdownOpen(!isDropdownOpen)}
> >
<MenuOutlined style={{ color: '#1a8ce6' }} /> <MenuOutlined style={{ color: '#1a8ce6' }} />
</button> </button>
@@ -77,14 +87,18 @@ export const Navbar: FC = (): ReactElement => {
Merch Gacha Merch Gacha
</Link> </Link>
</li> </li>
<li> {!isAuthenticated ? (
<Link <li>
to="#" <Button
className="block text-gray-600 transition-colors px-4 py-2 text-center font-semibold" onClick={() => setShowModalLogin(true)}
> className="block w-full text-gray-100 transition-colors px-4 py-2 text-center font-semibold"
Login >
</Link> Login
</li> </Button>
</li>
) : (
<li>{session.user?.fullname}</li>
)}
</ul> </ul>
</div> </div>
)} )}
-7
View File
@@ -1,7 +0,0 @@
import axios, { AxiosRequestConfig } from 'axios';
const config: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_URL,
};
export const api = axios.create(config);
-1
View File
@@ -1 +0,0 @@
export * from './api';
+3
View File
@@ -0,0 +1,3 @@
export * from './use-query-state';
export * from './use-session';
export * from './use-modal-login';
+28
View File
@@ -0,0 +1,28 @@
import { createContext, ReactNode, useContext, useState } from 'react';
interface ModalLoginContextType {
showModalLogin: boolean;
setShowModalLogin: (value: boolean) => void;
}
const ModalLoginContext = createContext<ModalLoginContextType | undefined>(
undefined
);
export const ModalLoginProvider = ({ children }: { children: ReactNode }) => {
const [showModalLogin, setShowModalLogin] = useState(false);
return (
<ModalLoginContext.Provider value={{ showModalLogin, setShowModalLogin }}>
{children}
</ModalLoginContext.Provider>
);
};
export const useModalLogin = () => {
const context = useContext(ModalLoginContext);
if (!context) {
throw new Error('useModalLogin must be used within an ModalLoginProvider');
}
return context;
};
+71
View File
@@ -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,
};
};
+22
View File
@@ -0,0 +1,22 @@
import { SessionToken, SessionUser } from '../local-storage';
export const useSession = () => {
const session = {
user: SessionUser.get(),
token: SessionToken.get(),
};
const isAuthenticated = !!session.token?.access_token;
const signOut = () => {
SessionUser.remove();
SessionToken.remove();
window.location.reload();
};
return {
isAuthenticated,
session,
signOut,
};
};
+2 -1
View File
@@ -1,4 +1,5 @@
export * from './react-query'; export * from './react-query';
export * from './react-router'; export * from './react-router';
export * from './tailwind-merge'; export * from './tailwind-merge';
export * from './axios'; export * from './hooks';
export * from './local-storage';
+60
View File
@@ -0,0 +1,60 @@
export type TPermissionItem = {
id: string;
name: string;
created_at: string;
updated_at: string;
};
type TRoleItem = {
id: string;
name: string;
created_at: string;
updated_at: string;
permissions: TPermissionItem[];
};
type TUserItem = {
id: string;
avatar: string;
birthdate: string;
email: string;
fullname: string;
gender: string;
identity_number: string;
is_active: boolean;
is_profile_completed: boolean;
phone_number: string;
referral_code: string;
referred_by: string;
religion: string;
student_type: string;
role: TRoleItem;
};
export const SessionUser = {
set: (val: TUserItem) => localStorage.setItem('users', JSON.stringify(val)),
get: (): TUserItem | undefined => {
const users = localStorage.getItem('users');
return users ? JSON.parse(users) : undefined;
},
remove: () => localStorage.removeItem('users'),
};
export const SessionToken = {
set: (val: { access_token: string; refresh_token: string }) => {
localStorage.setItem('access_token', val.access_token);
localStorage.setItem('refresh_token', val.refresh_token);
},
get: ():
| { access_token?: string | null; refresh_token?: string | null }
| undefined => {
return {
access_token: localStorage.getItem('access_token'),
refresh_token: localStorage.getItem('refresh_token'),
};
},
remove: () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
},
};
+602 -616
View File
File diff suppressed because it is too large Load Diff
+10 -3
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",
@@ -19,14 +19,20 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@hookform/resolvers": "^5.0.1",
"@tanstack/react-query": "^5.67.3", "@tanstack/react-query": "^5.67.3",
"@tanstack/react-store": "^0.7.0",
"@tanstack/react-table": "^8.21.2", "@tanstack/react-table": "^8.21.2",
"axios": "^1.8.3", "axios": "^1.8.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"js-cookie": "^3.0.5",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.55.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",
"tailwind-merge": "^3.0.2" "sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
"zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.5", "@babel/core": "^7.14.5",
@@ -48,7 +54,7 @@
"@storybook/test-runner": "^0.19.0", "@storybook/test-runner": "^0.19.0",
"@storybook/testing-library": "^0.2.2", "@storybook/testing-library": "^0.2.2",
"@swc-node/register": "~1.9.1", "@swc-node/register": "~1.9.1",
"@swc/cli": "~0.3.12", "@swc/cli": "^0.6.0",
"@swc/core": "~1.5.7", "@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11", "@swc/helpers": "~0.5.11",
"@tailwindcss/postcss": "^4.0.13", "@tailwindcss/postcss": "^4.0.13",
@@ -56,6 +62,7 @@
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/js-cookie": "^3.0.6",
"@types/node": "18.16.9", "@types/node": "18.16.9",
"@types/react": "19.0.0", "@types/react": "19.0.0",
"@types/react-dom": "19.0.0", "@types/react-dom": "19.0.0",