Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
638b64b715 | ||
|
|
d61cb4a215 | ||
|
|
272db82c8c | ||
|
|
9108ddb742 | ||
|
|
628e168342 | ||
|
|
9aff9e2dc8 | ||
|
|
585ad482f2 | ||
|
|
988476b409 | ||
|
|
38e6d4643b | ||
|
|
79cb9d81fc | ||
|
|
64e702df5d | ||
|
|
280afd68ab | ||
|
|
9777b7bea9 | ||
|
|
6bad504d44 | ||
|
|
6b959a2f22 | ||
|
|
33c04707f8 | ||
|
|
f87dc4c782 | ||
|
|
b5428d8552 | ||
|
|
0a846b0512 | ||
|
|
3a950ea867 | ||
|
|
4693284b37 | ||
|
|
f8f796e071 | ||
|
|
fe1928e8ef | ||
|
|
9c7b8c1c63 | ||
|
|
2bd767d20b | ||
|
|
a6fea630a4 | ||
|
|
c05061becf | ||
|
|
a9cc728ade | ||
|
|
2602657201 | ||
|
|
c70e5706bc | ||
|
|
49ca2a9fb0 | ||
|
|
47ebd3ad73 | ||
|
|
d544e43abc | ||
|
|
d7a90f47d0 | ||
|
|
984a31c587 | ||
|
|
a575e534b5 | ||
|
|
989503f930 | ||
|
|
3b979119ed | ||
|
|
ecec423d30 | ||
|
|
3bbdba7015 | ||
|
|
f7a7acaf65 | ||
|
|
809eeee931 | ||
|
|
f39466936f | ||
|
|
5d418830b9 | ||
|
|
d58ad58ef8 | ||
|
|
b62c762cf9 | ||
|
|
4e665cca76 | ||
|
|
74c0ebc5dd | ||
|
|
eadd554a32 | ||
|
|
ecdaa4a82e | ||
|
|
747e75e684 | ||
|
|
76637dd051 | ||
|
|
3dda8149ba | ||
|
|
cc21735468 | ||
|
|
a6bc6de72e | ||
|
|
340bd8c4bf | ||
|
|
1594be9d46 | ||
|
|
7eecf2621d | ||
|
|
088a0fc41f | ||
|
|
9178c65c4a | ||
|
|
f93d741cef | ||
|
|
81fa73935f | ||
|
|
88c71ccdfa | ||
|
|
5b3b9e44fa | ||
|
|
ac2ff2ec92 | ||
|
|
79e0d39280 | ||
|
|
fd9113109c | ||
|
|
da7e1067d2 | ||
|
|
75779b0373 | ||
|
|
a3887519f9 | ||
|
|
2ec6ad5b5c | ||
|
|
2d541c71ba | ||
|
|
46f3bd6014 | ||
|
|
c374c97e66 | ||
|
|
a4ca53004f | ||
|
|
2d9473ea7c | ||
|
|
6bfa48bed8 | ||
|
|
43167fe7a1 | ||
|
|
39391e5522 | ||
|
|
ec509a7b43 | ||
|
|
a76bd89ec1 | ||
|
|
cafdb29024 | ||
|
|
d79d0d3d09 | ||
|
|
7606c5b4e2 | ||
|
|
bc9a0fd740 | ||
|
|
7f9a106fca | ||
|
|
0bcc8f7f6a | ||
|
|
31b7587571 | ||
|
|
82fb1ea2bc | ||
|
|
bd5e46e712 | ||
|
|
542469bc67 | ||
|
|
631684cb05 | ||
|
|
54f4572533 | ||
|
|
ca933f624a | ||
|
|
90ab974e03 | ||
|
|
daeeb09798 | ||
|
|
c8935b885e |
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=https://api.example.com
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## Issue
|
||||||
|
|
||||||
|
<!-- #GitHubIssueNumber / Issue Link / Describe the problem here (if the issue hasn't been defined) -->
|
||||||
|
|
||||||
|
## What did you do?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Describe what did you do in this code
|
||||||
|
- Example:
|
||||||
|
- [CORRECT] Add auth feature / Modify auth feature.
|
||||||
|
- [WRONG] Create file /controllers/auth.ts, /models/user.ts, modify /routes/web.ts
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Screenshot / Video
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Attach pictures of this work / video explanation about this work
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Note for Deployment
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Tell to DevOps what should he do to deploy this change
|
||||||
|
- example:
|
||||||
|
- Please run migration when deploy this
|
||||||
|
- Please upload these files into folder xxx
|
||||||
|
- Please create folder `xxx` and give permission `777`
|
||||||
|
- Please run this SQL after deploy ```sql INSERT INTO xxx (...) ```
|
||||||
|
- etc...
|
||||||
|
-->
|
||||||
@@ -45,3 +45,9 @@ vite.config.*.timestamp*
|
|||||||
vitest.config.*.timestamp*
|
vitest.config.*.timestamp*
|
||||||
|
|
||||||
storybook-static
|
storybook-static
|
||||||
|
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.prod
|
||||||
|
.env.develop
|
||||||
|
.env.staging
|
||||||
|
|||||||
@@ -1,101 +1,117 @@
|
|||||||
# ImphnenFrontendService
|
# IMPHNEN Frontend Service
|
||||||
|
|
||||||
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
<p align="center">
|
||||||
|
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||||
|
</p>
|
||||||
|
|
||||||
✨ Your new, shiny [Nx workspace](https://nx.dev) is ready ✨.
|
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
||||||
|
|
||||||
[Learn more about this workspace setup and its capabilities](https://nx.dev/getting-started/tutorials/react-monorepo-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
|
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>.
|
||||||
|
2. **Backoffice** - Application for internal management.
|
||||||
|
3. **Dimentorin** - Application for mentoring services.
|
||||||
|
|
||||||
## Run tasks
|
## How to install
|
||||||
|
|
||||||
To run the dev server for your app, use:
|
1. Clone this repository:
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git
|
||||||
|
cd imphnen-frontend-service
|
||||||
|
```
|
||||||
|
2. Install all dependencies:
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
## How to run
|
||||||
npx nx serve dimentorin
|
|
||||||
```
|
|
||||||
|
|
||||||
To create a production bundle:
|
### Development
|
||||||
|
|
||||||
```sh
|
Use the following commands to run in development mode:
|
||||||
npx nx build dimentorin
|
|
||||||
```
|
|
||||||
|
|
||||||
To see all available targets to run for a project, run:
|
- **Gacha**:
|
||||||
|
```sh
|
||||||
|
npm run gacha:dev
|
||||||
|
```
|
||||||
|
- **Backoffice**:
|
||||||
|
```sh
|
||||||
|
npm run backoffice:dev
|
||||||
|
```
|
||||||
|
- **Dimentorin**:
|
||||||
|
```sh
|
||||||
|
npm run dimentorin:dev
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
### Build
|
||||||
npx nx show project dimentorin
|
|
||||||
```
|
|
||||||
|
|
||||||
These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
|
Use the following commands to build the applications:
|
||||||
|
|
||||||
[More about running tasks in the docs »](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
- **Gacha**:
|
||||||
|
```sh
|
||||||
|
npm run gacha:build
|
||||||
|
```
|
||||||
|
- **Backoffice**:
|
||||||
|
```sh
|
||||||
|
npm run backoffice:build
|
||||||
|
```
|
||||||
|
- **Dimentorin**:
|
||||||
|
```sh
|
||||||
|
npm run dimentorin:build
|
||||||
|
```
|
||||||
|
|
||||||
## Add new projects
|
### Production
|
||||||
|
|
||||||
While you could add new projects to your workspace manually, you might want to leverage [Nx plugins](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) and their [code generation](https://nx.dev/features/generate-code?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) feature.
|
Use the following commands to run the applications in production mode:
|
||||||
|
|
||||||
Use the plugin's generator to create new projects.
|
- **Gacha**:
|
||||||
|
```sh
|
||||||
|
npm run gacha:prod
|
||||||
|
```
|
||||||
|
- **Backoffice**:
|
||||||
|
```sh
|
||||||
|
npm run backoffice:prod
|
||||||
|
```
|
||||||
|
- **Dimentorin**:
|
||||||
|
```sh
|
||||||
|
npm run dimentorin:prod
|
||||||
|
```
|
||||||
|
|
||||||
To generate a new application, use:
|
### Storybook
|
||||||
|
|
||||||
```sh
|
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
||||||
npx nx g @nx/react:app demo
|
|
||||||
```
|
|
||||||
|
|
||||||
To generate a new library, use:
|
- **Run Storybook**
|
||||||
|
|
||||||
```sh
|
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
||||||
npx nx g @nx/react:lib mylib
|
|
||||||
```
|
|
||||||
|
|
||||||
You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx list <plugin-name>` to learn about more specific capabilities of a particular plugin. Alternatively, [install Nx Console](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) to browse plugins and generators in your IDE.
|
```sh
|
||||||
|
npm run ui:storybook
|
||||||
|
```
|
||||||
|
|
||||||
[Learn more about Nx plugins »](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry »](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
## Set up CI!
|
- **Run Unit Test**
|
||||||
|
|
||||||
### Step 1
|
This command runs unit tests for the UI components to ensure they function as expected.
|
||||||
|
|
||||||
To connect to Nx Cloud, run the following command:
|
```sh
|
||||||
|
npm run ui:test
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
- **Build Components**
|
||||||
npx nx connect
|
|
||||||
```
|
|
||||||
|
|
||||||
Connecting to Nx Cloud ensures a [fast and scalable CI](https://nx.dev/ci/intro/why-nx-cloud?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) pipeline. It includes features such as:
|
This command generates a static build of Storybook, which can be deployed for sharing and documentation purposes.
|
||||||
|
|
||||||
- [Remote caching](https://nx.dev/ci/features/remote-cache?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
```sh
|
||||||
- [Task distribution across multiple machines](https://nx.dev/ci/features/distribute-task-execution?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
npm run ui:build
|
||||||
- [Automated e2e test splitting](https://nx.dev/ci/features/split-e2e-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
```
|
||||||
- [Task flakiness detection and rerunning](https://nx.dev/ci/features/flaky-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
### Step 2
|
## How to contribute
|
||||||
|
|
||||||
Use the following command to configure a CI workflow for your workspace:
|
1. Fork the repository and clone it locally.
|
||||||
|
2. Create a new branch for a new feature or fix:
|
||||||
|
```sh
|
||||||
|
git checkout -b feat/feature-name
|
||||||
|
```
|
||||||
|
3. Make changes, commit, and push to your forked repository.
|
||||||
|
4. Create a pull request to this repository `develop` branch.
|
||||||
|
|
||||||
```sh
|
If you encounter any issues or problems, feel free to create a new Issue.
|
||||||
npx nx g ci-workflow
|
|
||||||
```
|
|
||||||
|
|
||||||
[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
## Install Nx Console
|
|
||||||
|
|
||||||
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
|
|
||||||
|
|
||||||
[Install Nx Console »](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
## Useful links
|
|
||||||
|
|
||||||
Learn more:
|
|
||||||
|
|
||||||
- [Learn more about this workspace setup](https://nx.dev/getting-started/tutorials/react-monorepo-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
And join the Nx community:
|
|
||||||
- [Discord](https://go.nx.dev/community)
|
|
||||||
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
|
|
||||||
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
|
|
||||||
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
||||||
<link rel="stylesheet" href="/src/styles.css" />
|
<link rel="stylesheet" href="/src/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 511 KiB |
@@ -0,0 +1,31 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
authLoginSchema,
|
||||||
|
TLoginRequest,
|
||||||
|
usePostLogin,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const postLogin = usePostLogin();
|
||||||
|
const form = useForm<TLoginRequest>({
|
||||||
|
resolver: zodResolver(authLoginSchema),
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
postLogin.mutate(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
console.log('Success Login');
|
||||||
|
navigate('/dashboard');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FilterOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
RowSelectionState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import ModalEditAccount from './_components/modal-edit-account';
|
||||||
|
import { useQueryState } from '../../hook/use-query-state';
|
||||||
|
|
||||||
|
interface Account {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data for demonstration
|
||||||
|
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
||||||
|
email: 'fullname23@gmail.com',
|
||||||
|
phone: '081904423804',
|
||||||
|
address: 'Jl. Pantai Cibaduyut Indah',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
step: currentStep,
|
||||||
|
nextStep,
|
||||||
|
prevStep,
|
||||||
|
resetStep,
|
||||||
|
} = useQueryState('step', {
|
||||||
|
defaultValue: 1,
|
||||||
|
maxValue: 2,
|
||||||
|
minValue: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
|
||||||
|
const columns: ColumnDef<Account>[] = [
|
||||||
|
{
|
||||||
|
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({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Data Akun</h1>
|
||||||
|
</header>
|
||||||
|
{/* Account Table Section */}
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
{/* Search and Filter */}
|
||||||
|
<div className="flex justify-between items-center gap-8 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap, email"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<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>
|
||||||
|
{/* Table */}
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Modal Edit Account */}
|
||||||
|
<ModalEditAccount
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalEditAccount}
|
||||||
|
onClose={() => setShowModalEditAccount(false)}
|
||||||
|
handleEditAccount={() => {
|
||||||
|
console.log('Account updated');
|
||||||
|
}}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
UsergroupDeleteOutlined,
|
||||||
|
UserSwitchOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
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 => {
|
||||||
|
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 (
|
||||||
|
<Fragment>
|
||||||
|
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||||
|
{/* Dashboard Header */}
|
||||||
|
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
||||||
|
<div className="w-full flex flex-col gap-[40px]">
|
||||||
|
{/* Summary Section */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
||||||
|
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>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="items-end gap-3"
|
||||||
|
onClick={() => setShowModalAddItem(true)}
|
||||||
|
>
|
||||||
|
<span>Tambah Item</span>
|
||||||
|
<PlusOutlined className="text-[16px]" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gacha Items List */}
|
||||||
|
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||||
|
{[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>
|
||||||
|
|
||||||
|
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
|
||||||
|
<img
|
||||||
|
src="gacha-clip.webp"
|
||||||
|
alt="Lanyard IMPHNEN"
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right-side illustration */}
|
||||||
|
<img
|
||||||
|
src="gacha.webp"
|
||||||
|
alt=""
|
||||||
|
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Modal Add Item */}
|
||||||
|
<ModalAddItem
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalAddItem}
|
||||||
|
onClose={() => setShowModalAddItem(false)}
|
||||||
|
handleAddItem={() => {
|
||||||
|
console.log('Item added');
|
||||||
|
}}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal Edit Item */}
|
||||||
|
<ModalEditItem
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalEditItem}
|
||||||
|
onClose={() => setShowModalEditItem(false)}
|
||||||
|
handleEditItem={() => {
|
||||||
|
console.log('Item edited');
|
||||||
|
}}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal Delete Item */}
|
||||||
|
<ModalDeleteItem
|
||||||
|
isOpen={showModalDeleteItem}
|
||||||
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
|
handleDeleteItem={() => {
|
||||||
|
console.log('Item deleted');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<main className="bg-primary-50 min-h-screen">
|
<main className="bg-primary-50 min-h-screen">
|
||||||
<Navbar />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,44 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { useLogin } from './_hooks/use-login';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
return <>Hallo</>;
|
const { form, onSubmit } = useLogin();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<div className="bg-white border border-primary-200 shadow-lg p-[60px] text-center flex flex-col justify-items-stretch gap-8 rounded-2xl">
|
||||||
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
|
<h1 className="text-primary-500 text-p1 font-semibold">
|
||||||
|
Welcome to IMPHNEN Backoffice
|
||||||
|
</h1>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Email"
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Password"
|
||||||
|
placeholder="Masukkan Password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button type="submit" size="md" className="w-full">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Components;
|
export default Components;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FilterOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
AuditOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
RowSelectionState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import ModalProcessDelivery from './_components/modal-process-item';
|
||||||
|
|
||||||
|
type OrderValid = 'valid' | 'invalid' | 'unchecked';
|
||||||
|
type Status = 'undelivered' | 'delivered';
|
||||||
|
|
||||||
|
interface Prize {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
orderValid: OrderValid;
|
||||||
|
items: string;
|
||||||
|
address: string;
|
||||||
|
status: Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
'Sertifikat + Laminating',
|
||||||
|
'Lanyard + ID Card',
|
||||||
|
'Pin',
|
||||||
|
'Sticker Isi 3',
|
||||||
|
'Sticker Isi 5',
|
||||||
|
'Gelang Karet',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for transactions
|
||||||
|
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: 'Nama Lengkap',
|
||||||
|
orderValid: (i % 3 === 0
|
||||||
|
? 'invalid'
|
||||||
|
: i % 5 === 0
|
||||||
|
? 'unchecked'
|
||||||
|
: 'valid') as OrderValid,
|
||||||
|
items: items[i % items.length],
|
||||||
|
address: 'Jl. Pantai Cibaduyut Indah',
|
||||||
|
status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [showModalProcessDelivery, setShowModalProcessDelivery] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
|
||||||
|
const deliveryOptions = [
|
||||||
|
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
|
||||||
|
{ id: 'option1', value: 'delivered', label: 'Delivered' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns: ColumnDef<Prize>[] = [
|
||||||
|
{
|
||||||
|
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({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
|
||||||
|
</header>
|
||||||
|
{/* Account Table Section */}
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
{/* Search and Filter */}
|
||||||
|
<div className="flex justify-between items-center gap-8 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<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>
|
||||||
|
{/* Table */}
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Modal Process Delivery */}
|
||||||
|
<ModalProcessDelivery
|
||||||
|
isOpen={showModalProcessDelivery}
|
||||||
|
onClose={() => setShowModalProcessDelivery(false)}
|
||||||
|
handleProcessDelivery={() => {
|
||||||
|
console.log('Action ketika user menekan tombol Proses Pengiriman');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FilterOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
AuditOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
RowSelectionState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import ModalValidate from './_components/modal-validate';
|
||||||
|
|
||||||
|
type TransactionStatus = 'valid' | 'invalid' | 'unchecked';
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
transactionNumber: string;
|
||||||
|
status: TransactionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data for transactions
|
||||||
|
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
||||||
|
transactionNumber: '25D2133Y9AFYBD',
|
||||||
|
status: (i % 3 === 0
|
||||||
|
? 'invalid'
|
||||||
|
: i % 5 === 0
|
||||||
|
? 'unchecked'
|
||||||
|
: 'valid') as TransactionStatus,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [showModalValidate, setShowModalValidate] = useState(false);
|
||||||
|
|
||||||
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
|
||||||
|
const validationOptions = [
|
||||||
|
{ id: 'option1', value: 'unchecked', label: 'Unchecked' },
|
||||||
|
{ id: 'option2', value: 'valid', label: 'Valid' },
|
||||||
|
{ id: 'option3', value: 'invalid', label: 'Invalid' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns: ColumnDef<Transaction>[] = [
|
||||||
|
{
|
||||||
|
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({
|
||||||
|
data: mockTransactions,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockTransactions.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Account Table Section */}
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
{/* Search and Filter */}
|
||||||
|
<div className="flex justify-between items-center gap-8 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<DataTable data={mockTransactions} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<ModalValidate
|
||||||
|
isOpen={showModalValidate}
|
||||||
|
onClose={() => setShowModalValidate(false)}
|
||||||
|
handleValid={() => {
|
||||||
|
console.log('Action ketika user klik Valid');
|
||||||
|
}}
|
||||||
|
handleInvalid={() => {
|
||||||
|
console.log('Action ketika user klik Tidak Valid');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -17,59 +17,55 @@
|
|||||||
|
|
||||||
--color-neutral-50: #f6f6f6;
|
--color-neutral-50: #f6f6f6;
|
||||||
--color-neutral-100: #e7e7e7;
|
--color-neutral-100: #e7e7e7;
|
||||||
--color-neutral-200: #d0d0d0;
|
--color-neutral-200: #d1d1d1;
|
||||||
--color-neutral-300: #b7b7b7;
|
--color-neutral-300: #b0b0b0;
|
||||||
--color-neutral-400: #9f9f9f;
|
--color-neutral-400: #888888;
|
||||||
--color-neutral-500: #7e7e7e;
|
--color-neutral-500: #6d6d6d;
|
||||||
--color-neutral-600: #5d5d5d;
|
--color-neutral-600: #5d5d5d;
|
||||||
--color-neutral-700: #4a4a4a;
|
--color-neutral-700: #4f4f4f;
|
||||||
--color-neutral-800: #3a3a3a;
|
--color-neutral-800: #454545;
|
||||||
--color-neutral-900: #2f2f2f;
|
--color-neutral-900: #3d3d3d;
|
||||||
--color-neutral-950: #2b2b2b;
|
--color-neutral-950: #2b2b2b;
|
||||||
|
|
||||||
--color-success-50: #e7f2ee;
|
--color-success-100: #e0fbd8;
|
||||||
--color-success-100: #cde6dd;
|
--color-success-200: #bcf8b0;
|
||||||
--color-success-200: #9bccbc;
|
--color-success-300: #8eea85;
|
||||||
--color-success-300: #78bdab;
|
--color-success-400: #63d564;
|
||||||
--color-success-400: #4ca88f;
|
--color-success-500: #35ba43;
|
||||||
--color-success-500: #349078;
|
--color-success-600: #269f3e;
|
||||||
--color-success-600: #2f7c66;
|
--color-success-700: #1a8439;
|
||||||
--color-success-700: #26624f;
|
--color-success-800: #106b32;
|
||||||
--color-success-800: #1e4a3b;
|
--color-success-900: #0b592f;
|
||||||
--color-success-900: #1b513b;
|
|
||||||
|
|
||||||
--color-info-50: #e7f4f5;
|
--color-info-100: #ccfcfe;
|
||||||
--color-info-100: #cce9ea;
|
--color-info-200: #9bf3fd;
|
||||||
--color-info-200: #99d3d5;
|
--color-info-300: #67e3fb;
|
||||||
--color-info-300: #78c4c7;
|
--color-info-400: #42cdf8;
|
||||||
--color-info-400: #3aa9ad;
|
--color-info-500: #04acf3;
|
||||||
--color-info-500: #279599;
|
--color-info-600: #0185d0;
|
||||||
--color-info-600: #207d82;
|
--color-info-700: #0264af;
|
||||||
--color-info-700: #1a666b;
|
--color-info-800: #01478d;
|
||||||
--color-info-800: #124c52;
|
--color-info-900: #003375;
|
||||||
--color-info-900: #093d43;
|
|
||||||
|
|
||||||
--color-warning-50: #fffce6;
|
--color-warning-100: #fffcd3;
|
||||||
--color-warning-100: #fff8cc;
|
--color-warning-200: #fffaa9;
|
||||||
--color-warning-200: #fef39b;
|
--color-warning-300: #fff67d;
|
||||||
--color-warning-300: #fde967;
|
--color-warning-400: #fff25d;
|
||||||
--color-warning-400: #fbd93d;
|
--color-warning-500: #ffed27;
|
||||||
--color-warning-500: #f3c215;
|
--color-warning-600: #dbc91d;
|
||||||
--color-warning-600: #d7a20f;
|
--color-warning-700: #b7a714;
|
||||||
--color-warning-700: #aa7e0c;
|
--color-warning-800: #93850b;
|
||||||
--color-warning-800: #805c07;
|
--color-warning-900: #7a6d07;
|
||||||
--color-warning-900: #665c00;
|
|
||||||
|
|
||||||
--color-danger-50: #ffe7e7;
|
--color-danger-100: #ffe8da;
|
||||||
--color-danger-100: #ffcece;
|
--color-danger-200: #ffcbb3;
|
||||||
--color-danger-200: #ff9f9f;
|
--color-danger-300: #ffaa8d;
|
||||||
--color-danger-300: #ff6e6e;
|
--color-danger-400: #ff8870;
|
||||||
--color-danger-400: #fa4646;
|
--color-danger-500: #ff5242;
|
||||||
--color-danger-500: #ef1f1f;
|
--color-danger-600: #da3030;
|
||||||
--color-danger-600: #d11515;
|
--color-danger-700: #b7212d;
|
||||||
--color-danger-700: #a51111;
|
--color-danger-800: #93152a;
|
||||||
--color-danger-800: #7f0c0c;
|
--color-danger-900: #7a0c27;
|
||||||
--color-danger-900: #5d2d2d;
|
|
||||||
|
|
||||||
--radius-none: 0px;
|
--radius-none: 0px;
|
||||||
--radius-sm: 2px;
|
--radius-sm: 2px;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ export default defineConfig(() => ({
|
|||||||
root: __dirname,
|
root: __dirname,
|
||||||
cacheDir: '../../node_modules/.vite/apps/backoffice',
|
cacheDir: '../../node_modules/.vite/apps/backoffice',
|
||||||
server: {
|
server: {
|
||||||
port: 4200,
|
port: 3000,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
port: 4300,
|
port: 3001,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
},
|
},
|
||||||
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
|||||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 8.6 MiB |
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { ForgotStep, OtpForm } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<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'>
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
|
||||||
|
<ForgotStep step={2} />
|
||||||
|
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Forgot Password</h3>
|
||||||
|
<h5 className='mt-2 text-xl font-medium text-primary-500'>Yeay~! Pesan dari dunia lain (a.k.a email-mu) sudah tiba!</h5>
|
||||||
|
<OtpForm />
|
||||||
|
<Button>Summon Password Baru!</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { ForgotStep } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<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'>
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
|
||||||
|
<ForgotStep step={1} />
|
||||||
|
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Forgot Password</h3>
|
||||||
|
<h5 className='mt-2 text-xl font-medium text-primary-500'>Masukkan email-mu, dan biarkan kami memanggil password-mu kembali dari dunia lain!</h5>
|
||||||
|
<h6 className='text-gray-600 mt-8'>Email</h6>
|
||||||
|
<Input type='email' size='lg' placeholder='Contoh: yourname@mail.com'></Input>
|
||||||
|
<Button variant='primary' className='mt-7'>Kirim Kode OTP ^^</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { ForgotStep } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<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'>
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
|
||||||
|
<ForgotStep step={3} />
|
||||||
|
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Forgot Password</h3>
|
||||||
|
<h5 className='mt-2 text-md font-medium text-primary-500'>Sekarang, buatlah password baru yang lebih kuat, seperti jurus andalanmu!<span role="img" aria-label='emoji'>🔥⚡</span></h5>
|
||||||
|
<div className='my-4'>
|
||||||
|
<h6 className='text-gray-700'>Password Baru</h6>
|
||||||
|
<Input className='w-full' type="password" placeholder='Buat password sekokoh armor legendary!'/>
|
||||||
|
<h6 className='text-gray-700 mt-5'>Ulang Password Baru</h6>
|
||||||
|
<Input className='w-full' type="password" placeholder='Pastikan Cocok! Jangan sampai ada typo, Senpai~!'/>
|
||||||
|
</div>
|
||||||
|
<div className='mb-4 text-gray-700 flex flex-col gap-3 text-sm'>
|
||||||
|
<h6><span role='img' aria-label='emoji'>💡</span> Tips dari kami:</h6>
|
||||||
|
<h6><span role='img' aria-label='emoji'>✅ </span> Buat kombinasi huruf besar, kecil, angka, dan simbol untuk kekuatan maksimal!</h6>
|
||||||
|
<h6><span role='img' aria-label='emoji'>✅</span> Pastikan kamu ingat password-mu atau simpan di tempat aman~</h6>
|
||||||
|
<h6>Masukkan password baru dan bersiaplah untuk kembali bertualang!</h6>
|
||||||
|
</div>
|
||||||
|
<Button className='mt-5'>Summon Password Baru!</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const error = searchParams.get('error'); // Ambil nilai ?error=
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<LoginBanner />
|
||||||
|
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
|
||||||
|
<div className="w-[404px]">
|
||||||
|
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
||||||
|
Hallo Minna-san
|
||||||
|
</h2>
|
||||||
|
<h5 className="text-xl font-medium text-primary-500 text-center">
|
||||||
|
Welcome to Dimentorin by IMPHNEN
|
||||||
|
</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>
|
||||||
|
<Button className="w-full">Enter Isekai</Button>
|
||||||
|
<div className="flex my-3 gap-3 justify-center">
|
||||||
|
<h5>Belum Punya akun ?</h5>
|
||||||
|
<a href="/auth/register" className="text-primary-500 font-medium">
|
||||||
|
Daftar Disini
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
|
||||||
|
<span className="px-3 text-blue-400">Or</span>
|
||||||
|
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full my-3 text-gray-500 gap-2"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<p>Log In With Google</p>
|
||||||
|
<img
|
||||||
|
src="/image/33978ce5bed2da9bf9d73acad802182a.webp"
|
||||||
|
alt="Google Icon"
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<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'>
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center'>
|
||||||
|
<div className='w-[404px]'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import {
|
||||||
|
InputField,
|
||||||
|
Modal,
|
||||||
|
Stepper,
|
||||||
|
} from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import { Fragment } from 'react/jsx-runtime';
|
||||||
|
interface IModalFormForgotPasswordProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFormForgotPassword = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: IModalFormForgotPasswordProps) => {
|
||||||
|
const {
|
||||||
|
step: currentStep,
|
||||||
|
nextStep,
|
||||||
|
prevStep,
|
||||||
|
resetStep,
|
||||||
|
} = useQueryState('step', {
|
||||||
|
defaultValue: 1,
|
||||||
|
maxValue: 3,
|
||||||
|
minValue: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Modal.Header className="space-y-4">
|
||||||
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
|
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||||
|
Forgot Password
|
||||||
|
</h1>
|
||||||
|
<Stepper currentStep={currentStep} totalSteps={3} />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="space-y-6">
|
||||||
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<StepTwo nextStep={nextStep} prevStep={prevStep} />
|
||||||
|
)}
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<StepThree onClose={onClose} resetStep={resetStep} />
|
||||||
|
)}
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepOneProps {
|
||||||
|
nextStep: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
label="Email"
|
||||||
|
placeholder="Masukkan Email yang Terdaftar"
|
||||||
|
type="email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<Button variant="bordered" size="md" className="w-full" onClick={onClose}>
|
||||||
|
Back To Login
|
||||||
|
</Button>
|
||||||
|
<Button size="md" className="w-full" onClick={nextStep}>
|
||||||
|
Kirim OTP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
||||||
|
<Fragment>
|
||||||
|
<InputField
|
||||||
|
label="Kode OTP"
|
||||||
|
placeholder="Masukkan Kode OTP"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant="bordered"
|
||||||
|
className="w-full"
|
||||||
|
onClick={prevStep}
|
||||||
|
>
|
||||||
|
Change Email
|
||||||
|
</Button>
|
||||||
|
<Button size="md" className="w-full" onClick={nextStep}>
|
||||||
|
Reset Password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IStepThreeProps {
|
||||||
|
onClose: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepThree = ({ onClose, resetStep }: IStepThreeProps) => (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
label="Password Baru"
|
||||||
|
placeholder="Masukkan Password Baru"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Ulang Password"
|
||||||
|
placeholder="Masukkan Ulang Password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
console.log('Password reset submitted');
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Buat Password Baru
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ModalFormForgotPassword;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { useLogin } from '../../_hooks/use-login';
|
||||||
|
|
||||||
|
interface IModalFormLogin {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onForgotPassword: () => void;
|
||||||
|
setIsOpenRegisterModal: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFormLogin = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onForgotPassword,
|
||||||
|
setIsOpenRegisterModal,
|
||||||
|
}: IModalFormLogin) => {
|
||||||
|
const { form, onSubmit } = useLogin();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<Modal.Header className="space-y-4">
|
||||||
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
|
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||||
|
Login
|
||||||
|
</h1>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form className="space-y-4" onSubmit={onSubmit}>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Email"
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Password"
|
||||||
|
placeholder="Masukkan Password"
|
||||||
|
type="password"
|
||||||
|
name="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>
|
||||||
|
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
||||||
|
<p className="text-neutral-500">Belum punya akun?</p>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
className="text-primary-500 hover:text-primary-600 m-0 p-0"
|
||||||
|
onClick={() => setIsOpenRegisterModal(true)}
|
||||||
|
>
|
||||||
|
Daftar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalFormLogin;
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import { useRegister } from '../../_hooks/use-register';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
|
import { TRegisterRequest } from '@imphnen-frontend-service/service';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface IModalFormRegisterProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||||
|
const { form, onSubmit, isStepOneValid } = useRegister();
|
||||||
|
|
||||||
|
const {
|
||||||
|
step: currentStep,
|
||||||
|
nextStep,
|
||||||
|
prevStep,
|
||||||
|
resetStep,
|
||||||
|
} = useQueryState('registerStep', {
|
||||||
|
defaultValue: 1,
|
||||||
|
maxValue: 2,
|
||||||
|
minValue: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isStepOneValid && currentStep === 2) {
|
||||||
|
prevStep();
|
||||||
|
}
|
||||||
|
}, [isStepOneValid, currentStep, prevStep]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Modal.Header className="space-y-4">
|
||||||
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
|
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||||
|
Register
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-neutral-500 text-lg text-center">
|
||||||
|
{currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'}
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<StepOne
|
||||||
|
form={form}
|
||||||
|
nextStep={nextStep}
|
||||||
|
isStepOneValid={isStepOneValid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />}
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepOneProps {
|
||||||
|
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||||
|
nextStep: () => void;
|
||||||
|
isStepOneValid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="fullname"
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Email"
|
||||||
|
placeholder="Masukkan Email Anda"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Masukkan Password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="confirm_password"
|
||||||
|
label="Ulangi Password"
|
||||||
|
placeholder="Masukkan Ulang Password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
|
onClick={nextStep}
|
||||||
|
disabled={!isStepOneValid}
|
||||||
|
>
|
||||||
|
Selanjutnya
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||||
|
prevStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ form, prevStep }: IStepTwoProps) => (
|
||||||
|
<>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="phone_number"
|
||||||
|
label="Nomor Telepon"
|
||||||
|
placeholder="Masukkan Nomor Telepon Aktif"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="referral_code"
|
||||||
|
label="Kode Referral"
|
||||||
|
placeholder="Masukkan Kode Referral"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="referred_by"
|
||||||
|
label="Referral By"
|
||||||
|
placeholder="Masukkan Referral By"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="student_type"
|
||||||
|
label="Tipe Pelajar"
|
||||||
|
placeholder="Masukkan Tipe Pelajar"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<Button size="md" variant="text" className="w-[40%]" onClick={prevStep}>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button size="md" className="w-full" type="submit">
|
||||||
|
Gacha Sekarang
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ModalFormRegister;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
authRegisterSchema,
|
||||||
|
stepOneRegisterSchema,
|
||||||
|
TRegisterRequest,
|
||||||
|
usePostRegister,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useRegister = () => {
|
||||||
|
const postRegister = usePostRegister();
|
||||||
|
const form = useForm<TRegisterRequest>({
|
||||||
|
resolver: zodResolver(authRegisterSchema),
|
||||||
|
mode: 'all',
|
||||||
|
defaultValues: {
|
||||||
|
fullname: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
phone_number: '',
|
||||||
|
referral_code: '',
|
||||||
|
referred_by: '',
|
||||||
|
student_type: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [stepValid, setStepValid] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = form.watch(() => {
|
||||||
|
const { fullname, email, password, confirm_password } = form.getValues();
|
||||||
|
const valid = stepOneRegisterSchema.safeParse({
|
||||||
|
fullname,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirm_password,
|
||||||
|
}).success;
|
||||||
|
setStepValid(valid);
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
postRegister.mutate(data, {
|
||||||
|
onSuccess: (data) => toast.success(data.message),
|
||||||
|
onError: (error) => toast.error(error.message),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
isStepOneValid: stepValid,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||