Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46f1e43114 | ||
|
|
84c60dcffa | ||
|
|
f432085eb8 | ||
|
|
a4e543d30e | ||
|
|
69b32f7a46 | ||
|
|
37f926a74d | ||
|
|
88e8649b2a | ||
|
|
031256ec4e | ||
|
|
d96df63660 | ||
|
|
8fb3e9a37f | ||
|
|
2280bd6085 | ||
|
|
d39960f446 | ||
|
|
1c1c66fc0f | ||
|
|
60f0e98476 | ||
|
|
472e02e0b0 | ||
|
|
8d71173897 | ||
|
|
d2292a7fa0 | ||
|
|
b655de92c0 | ||
|
|
761eb0963a | ||
|
|
fc3c275fea | ||
|
|
3527458bc9 | ||
|
|
a7a7abddd2 | ||
|
|
06c71e8dcd | ||
|
|
50b9380add | ||
|
|
9bd326d7ad | ||
|
|
432a2d8d9e | ||
|
|
a9ec7e947f | ||
|
|
a867891001 | ||
|
|
6850313fe3 | ||
|
|
c3c203b555 | ||
|
|
341b4eef98 | ||
|
|
d03deb2d91 | ||
|
|
5f97352031 | ||
|
|
3b71a97d16 | ||
|
|
6e940d749a | ||
|
|
e971d33a05 | ||
|
|
0296a891eb | ||
|
|
47c8a25574 | ||
|
|
671a4f596b | ||
|
|
8c8e2c646f | ||
|
|
035f7fa8cd | ||
|
|
0a4921f2ef | ||
|
|
dbaaddab2e | ||
|
|
6c640f1cd4 | ||
|
|
e6f1b80cd3 | ||
|
|
5ca4caa0ba | ||
|
|
5e70f36a7e | ||
|
|
d009adcd70 | ||
|
|
92842c29bb | ||
|
|
684f299194 | ||
|
|
d5607b456b | ||
|
|
bb9c2f30a8 | ||
|
|
6819c24c6a | ||
|
|
f05fd5331d | ||
|
|
b1d9a56dda | ||
|
|
9f25941a4a | ||
|
|
aaf5b4262d | ||
|
|
0f46244d2b | ||
|
|
dd03b3da85 | ||
|
|
9a0fee65a5 | ||
|
|
376e646c10 | ||
|
|
f8cd57a1cf | ||
|
|
5618a8e99b | ||
|
|
598bd22b5a | ||
|
|
246adb57a1 | ||
|
|
75839271ad | ||
|
|
6e78b49731 | ||
|
|
3aa552afd6 | ||
|
|
b389235971 | ||
|
|
86290cc820 | ||
|
|
41712be56a | ||
|
|
240e684b53 | ||
|
|
b1a53df4a9 | ||
|
|
0709c3a2bd | ||
|
|
f2e1cead83 | ||
|
|
1b795f2dfc | ||
|
|
ce115f63e4 | ||
|
|
c907c696f5 | ||
|
|
de76020709 | ||
|
|
1e391a2932 | ||
|
|
a7c67cb77f | ||
|
|
dafdfba490 | ||
|
|
a741fbb8d4 | ||
|
|
0e3dc101d5 | ||
|
|
e805e5fd41 | ||
|
|
5bdf8f5a4d | ||
|
|
07f1fb0208 | ||
|
|
3958ae1098 | ||
|
|
885d156833 | ||
|
|
2746af0b3f | ||
|
|
745a2d2873 | ||
|
|
8887e3b0a9 | ||
|
|
638b64b715 | ||
|
|
d61cb4a215 | ||
|
|
272db82c8c |
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=https://api.example.com
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Test and Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['develop']
|
||||||
|
pull_request:
|
||||||
|
branches: ['develop']
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 22.14.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
echo "CMS_SECRET=${{ secrets.CMS_SECRET }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_REGION=${{ secrets.CMS_STORAGE_REGION }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_ENDPOINT=${{ secrets.CMS_STORAGE_ENDPOINT }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_ACCESS_KEY_ID=${{ secrets.CMS_STORAGE_ACCESS_KEY_ID }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_SECRET_ACCESS_KEY=${{ secrets.CMS_STORAGE_SECRET_ACCESS_KEY }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_POSTGRES_URL=${{ secrets.CMS_POSTGRES_URL }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Nx Build
|
||||||
|
run: npx nx run-many --target=build --all
|
||||||
+11
@@ -51,3 +51,14 @@ storybook-static
|
|||||||
.env.prod
|
.env.prod
|
||||||
.env.develop
|
.env.develop
|
||||||
.env.staging
|
.env.staging
|
||||||
|
|
||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|||||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"nrwl.angular-console",
|
"nrwl.angular-console",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint"
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-playwright.playwright"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
||||||
|
|
||||||
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>.
|
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Gacha Website</a>.
|
||||||
2. **Backoffice** - Application for internal management.
|
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
||||||
3. **Dimentorin** - Application for mentoring services.
|
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
||||||
|
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
@@ -24,6 +25,15 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
|
|||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
|
### Setup Environment Variables
|
||||||
|
|
||||||
|
Before running the applications, you need to set up the environment variables. You can do this by copying the `.env.example` file to `.env` and modifying the values according to your needs.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd apps/{appname}
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
Use the following commands to run in development mode:
|
Use the following commands to run in development mode:
|
||||||
@@ -40,6 +50,10 @@ Use the following commands to run in development mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:dev
|
npm run dimentorin:dev
|
||||||
```
|
```
|
||||||
|
- **Landing Page**:
|
||||||
|
```sh
|
||||||
|
npm run landing:dev
|
||||||
|
```
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
@@ -57,6 +71,10 @@ Use the following commands to build the applications:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:build
|
npm run dimentorin:build
|
||||||
```
|
```
|
||||||
|
- **Landing Page**:
|
||||||
|
```sh
|
||||||
|
npm run landing:build
|
||||||
|
```
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
@@ -74,6 +92,10 @@ Use the following commands to run the applications in production mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:prod
|
npm run dimentorin:prod
|
||||||
```
|
```
|
||||||
|
- **Landing Page**:
|
||||||
|
```sh
|
||||||
|
npm run landing:prod
|
||||||
|
```
|
||||||
|
|
||||||
### Storybook
|
### Storybook
|
||||||
|
|
||||||
@@ -87,7 +109,6 @@ This repository uses Storybook to develop, test, and document UI components in a
|
|||||||
npm run ui:storybook
|
npm run ui:storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- **Run Unit Test**
|
- **Run Unit Test**
|
||||||
|
|
||||||
This command runs unit tests for the UI components to ensure they function as expected.
|
This command runs unit tests for the UI components to ensure they function as expected.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
+2
-2
@@ -18,7 +18,7 @@ import {
|
|||||||
RowSelectionState,
|
RowSelectionState,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import ModalEditAccount from './_components/modal-edit-account';
|
import ModalEditAccount from './_components/modal-edit-account';
|
||||||
import { useQueryState } from '../../hook/use-query-state';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -166,7 +166,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</Button>
|
</Button>
|
||||||
{showFilter && (
|
{showFilter && (
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
||||||
<Filter onClose={() => setShowFilter(false)} />
|
<Filter onClose={() => setShowFilter(false)} options={[]} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
interface IModalAddItem {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleAddItem?: () => Promise<boolean>;
|
||||||
|
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) => {
|
||||||
|
const { form, onSubmit } = useItem(nextStep);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Nama Hadiah"
|
||||||
|
name="itemName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Nama Hadiah"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Quantity"
|
||||||
|
name="quantity"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
placeholder="Masukkan Kuantitas Item"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Foto Barang"
|
||||||
|
type="file"
|
||||||
|
name="foto"
|
||||||
|
placeholder=".jpg, .jpeg, atau .png"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
Tambahkan Item
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
handleAddItem?: () => Promise<boolean>;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||||
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleAddItem,
|
||||||
|
{
|
||||||
|
success: 'Item ditambahkan ke gacha item',
|
||||||
|
error: 'Item gagal ditambahkan ke gacha item',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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={onCancel}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Tambahkan
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalAddItem;
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
interface IModalEditItem {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleEditItem?: () => Promise<boolean>;
|
||||||
|
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) => {
|
||||||
|
const initialValues = {
|
||||||
|
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||||
|
quantity: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Edit Item Gacha
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Silakan mengubah detail dari item yang diperlukan
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex flex-col gap-8">
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Nama Hadiah"
|
||||||
|
type="text"
|
||||||
|
name="itemName"
|
||||||
|
placeholder="Masukkan Nama Hadiah"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Quantity"
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
placeholder="Masukkan Kuantitas Item"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Foto Barang"
|
||||||
|
type="file"
|
||||||
|
name="foto"
|
||||||
|
placeholder=".jpg, .jpeg, atau .png"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
Perbarui Item
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
handleEditItem?: () => Promise<boolean>;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
|
||||||
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleEditItem,
|
||||||
|
{
|
||||||
|
success: 'Perubahan item berhasil dilakukan',
|
||||||
|
error: 'Perubahan item gagal dilakukan',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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={onCancel}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalEditItem;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import {
|
||||||
|
gachaItemSchema,
|
||||||
|
TGachaItem
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const useItem = (
|
||||||
|
nextStep: () => void,
|
||||||
|
initialValues?: TGachaItem
|
||||||
|
) => {
|
||||||
|
const form = useForm<TGachaItem>({
|
||||||
|
resolver: zodResolver(gachaItemSchema),
|
||||||
|
mode: 'all',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
console.log('Form data:', data);
|
||||||
|
nextStep();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfirmItem = (
|
||||||
|
onClose: () => void,
|
||||||
|
resetStep: () => void,
|
||||||
|
actionFunction?: () => Promise<boolean>,
|
||||||
|
messages?: {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
// const result = await actionFunction?.();
|
||||||
|
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||||
|
toast.success(messages?.success);
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(messages?.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
};
|
||||||
|
};
|
||||||
+6
-28
@@ -10,7 +10,7 @@ import { FC, Fragment, ReactElement, useState } from 'react';
|
|||||||
import ModalAddItem from './_components/modal-add-item';
|
import ModalAddItem from './_components/modal-add-item';
|
||||||
import ModalEditItem from './_components/modal-edit-item';
|
import ModalEditItem from './_components/modal-edit-item';
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
import ModalDeleteItem from './_components/modal-delete-item';
|
||||||
import { useQueryState } from '../../hook/use-query-state';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
@@ -31,20 +31,17 @@ export const Components: FC = (): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||||
{/* Dashboard Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
||||||
<div className="w-full flex flex-col gap-[40px]">
|
<div className="w-full flex flex-col gap-[40px]">
|
||||||
{/* Summary Section */}
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
||||||
Summary
|
Summary
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Participants */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<UsergroupAddOutlined className="text-[20px]" />
|
<UsergroupAddOutlined className="text-[20px]" />
|
||||||
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Roll and Reroll */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<ReloadOutlined className="text-[20px]" />
|
<ReloadOutlined className="text-[20px]" />
|
||||||
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Redeem */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<UserSwitchOutlined className="text-[20px]" />
|
<UserSwitchOutlined className="text-[20px]" />
|
||||||
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Inactive Users */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||||
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Gacha Items Section */}
|
|
||||||
<section className="flex flex-col gap-8">
|
<section className="flex flex-col gap-8">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-p2 font-medium text-primary-500">
|
<h2 className="text-p2 font-medium text-primary-500">
|
||||||
@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gacha Items List */}
|
|
||||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className="bg-white max-h-[80px] overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col py-4 px-6 gap-4">
|
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-p3 text-primary-500 font-medium">
|
<h3 className="text-p3 text-primary-500 font-medium">
|
||||||
Lanyard IMPHNEN
|
Lanyard IMPHNEN
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1">
|
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||||
<span>Prize {item}</span>
|
<span>Prize {item}</span>
|
||||||
<span>Chance Rate: (0.1%)</span>
|
<span>Quantity: 10</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start gap-2">
|
<div className="flex justify-start gap-2">
|
||||||
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
|
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
||||||
<img
|
|
||||||
src="gacha-clip.webp"
|
|
||||||
alt="Lanyard IMPHNEN"
|
|
||||||
className="h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -174,9 +161,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalAddItem}
|
isOpen={showModalAddItem}
|
||||||
onClose={() => setShowModalAddItem(false)}
|
onClose={() => setShowModalAddItem(false)}
|
||||||
handleAddItem={() => {
|
|
||||||
console.log('Item added');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
@@ -187,9 +171,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalEditItem}
|
isOpen={showModalEditItem}
|
||||||
onClose={() => setShowModalEditItem(false)}
|
onClose={() => setShowModalEditItem(false)}
|
||||||
handleEditItem={() => {
|
|
||||||
console.log('Item edited');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
@@ -199,9 +180,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<ModalDeleteItem
|
<ModalDeleteItem
|
||||||
isOpen={showModalDeleteItem}
|
isOpen={showModalDeleteItem}
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
handleDeleteItem={() => {
|
|
||||||
console.log('Item deleted');
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
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 { useItem, useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalAddItem {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleAddItem?: () => Promise<boolean>;
|
||||||
|
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) => {
|
||||||
|
const { form, onSubmit } = useItem(nextStep);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Tambah Item Roll Gacha
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Lengkapi detal di bawah ini, untuk menambahkan item gacha
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Pilih Item"
|
||||||
|
name="itemName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Pilih item yang dimasukkan ke roll"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Quantity"
|
||||||
|
name="quantity"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
placeholder="Masukkan Kuantitas Item"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Chance Rate"
|
||||||
|
name="chanceRate"
|
||||||
|
type="number"
|
||||||
|
value={0.1}
|
||||||
|
min={0.1}
|
||||||
|
step={0.1}
|
||||||
|
max={1}
|
||||||
|
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||||
|
Tambahkan Item
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
handleAddItem?: () => Promise<boolean>;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||||
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleAddItem,
|
||||||
|
{
|
||||||
|
success: 'Item ditambahkan ke roll gacha',
|
||||||
|
error: 'Item gagal ditambahkan ke roll gacha',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header className="mb-0 text-center items-center">
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Tambah ke Roll Gacha
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Apakah kamu yakin ingin
|
||||||
|
<br /> menambahkan item ini ke roll gacha?
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
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,168 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
interface IModalUpdateItem {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleUpdateItem?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalUpdateItem = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
currentStep,
|
||||||
|
nextStep,
|
||||||
|
resetStep,
|
||||||
|
handleUpdateItem,
|
||||||
|
}: IModalUpdateItem) => {
|
||||||
|
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}
|
||||||
|
handleUpdateItem={handleUpdateItem}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepOneProps {
|
||||||
|
nextStep: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||||
|
const initialValues = {
|
||||||
|
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||||
|
quantity: 10,
|
||||||
|
chanceRate: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Update Item Roll Gacha
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Pilih Item"
|
||||||
|
name="itemName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Pilih item yang dimasukkan ke roll"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Quantity"
|
||||||
|
name="quantity"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
placeholder="Masukkan Kuantitas Item"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Chance Rate"
|
||||||
|
name="chanceRate"
|
||||||
|
type="number"
|
||||||
|
value={0.1}
|
||||||
|
min={0.1}
|
||||||
|
step={0.1}
|
||||||
|
max={1}
|
||||||
|
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
Perbarui Item
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
handleUpdateItem?: () => Promise<boolean>;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ onClose, handleUpdateItem, resetStep }: IStepTwoProps) => {
|
||||||
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleUpdateItem,
|
||||||
|
{
|
||||||
|
success: 'Perubahan item roll berhasil dilakukan',
|
||||||
|
error: 'Perubahan item roll gagal dilakukan',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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={onCancel}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalUpdateItem;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import {
|
||||||
|
gachaRollItemSchema,
|
||||||
|
TGachaRollItem
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const useItem = (
|
||||||
|
nextStep: () => void,
|
||||||
|
initialValues?: TGachaRollItem
|
||||||
|
) => {
|
||||||
|
const form = useForm<TGachaRollItem>({
|
||||||
|
resolver: zodResolver(gachaRollItemSchema),
|
||||||
|
mode: 'all',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
console.log('Form data:', data);
|
||||||
|
nextStep();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfirmItem = (
|
||||||
|
onClose: () => void,
|
||||||
|
resetStep: () => void,
|
||||||
|
actionFunction?: () => Promise<boolean>,
|
||||||
|
messages?: {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
// const result = await actionFunction?.();
|
||||||
|
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||||
|
toast.success(messages?.success);
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(messages?.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
RowSelectionState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import ModalAddItem from './_components/modal-add-item';
|
||||||
|
import ModalUpdateItem from './_components/modal-update-item';
|
||||||
|
import ModalDeleteItem from './_components/modal-delete-item';
|
||||||
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
interface GachaItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
chanceRate: number;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: 'Hoodie IMPHNEN Official 2025',
|
||||||
|
chanceRate: 0.1,
|
||||||
|
quantity: 10,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
|
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||||
|
const [showModalDeleteItem, setShowModalDeleteItem] = 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 columns: ColumnDef<GachaItem>[] = [
|
||||||
|
{
|
||||||
|
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 Item',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Chance Rate',
|
||||||
|
accessorKey: 'chanceRate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Quantity',
|
||||||
|
accessorKey: 'quantity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
cell: () => (
|
||||||
|
<div className="flex gap-[8px]">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModalUpdateItem(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<EditOutlined /> Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModalDeleteItem(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<DeleteOutlined /> Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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 className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Gacha Roll</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-8 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama item"
|
||||||
|
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 gap-3 text-nowrap"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModalAddItem(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
Tambah Item
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ModalAddItem
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalAddItem}
|
||||||
|
onClose={() => setShowModalAddItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
<ModalUpdateItem
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalUpdateItem}
|
||||||
|
onClose={() => setShowModalUpdateItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
<ModalDeleteItem
|
||||||
|
isOpen={showModalDeleteItem}
|
||||||
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
|
handleDeleteItem={() => {
|
||||||
|
console.log('Item deleted');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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 { useItem, useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalAddPermission {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleAddItem?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalAddPermission = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
currentStep,
|
||||||
|
nextStep,
|
||||||
|
resetStep,
|
||||||
|
handleAddItem,
|
||||||
|
}: IModalAddPermission) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 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) => {
|
||||||
|
const { form, onSubmit } = useItem(nextStep);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500">
|
||||||
|
Tambah Permissions
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Permission"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||||
|
Tambah Permission
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
handleAddItem?: () => Promise<boolean>;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||||
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleAddItem,
|
||||||
|
{
|
||||||
|
success: 'Data permissions berhasil ditambahkan',
|
||||||
|
error: 'Data permissions gagal ditambahkan',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header className="mb-7 text-center items-center">
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Tambah Permissions
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-center text-neutral-400">
|
||||||
|
Apakah kamu yakin ingin
|
||||||
|
<br /> menambahkan permission ini?
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex mb-0 gap-4">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Tambahkan
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalAddPermission;
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalDeletePermission {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleDelete?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalDeletePermission = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleDelete,
|
||||||
|
}: IModalDeletePermission) => {
|
||||||
|
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
||||||
|
success: 'Data permissions berhasil dihapus',
|
||||||
|
error: 'Data permissions gagal dihapus',
|
||||||
|
});
|
||||||
|
|
||||||
|
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?"
|
||||||
|
width={148}
|
||||||
|
className="self-center"
|
||||||
|
/>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||||
|
Delete Permissions
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini
|
||||||
|
mungkin akan mempengaruhi fungsional sistem
|
||||||
|
</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="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full bg-danger-500 hover:bg-danger-600"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Hapus Item
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalDeletePermission;
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalUpdatePermission {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleUpdate?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalUpdatePermission = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleUpdate,
|
||||||
|
}: IModalUpdatePermission) => {
|
||||||
|
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
||||||
|
success: 'Perubahan permissions berhasil dilakukan',
|
||||||
|
error: 'Perubahan permissions gagal dilakukan',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
disableEscapeKeyDown={true}
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500">
|
||||||
|
Update Permissions
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex flex-col gap-8">
|
||||||
|
<InputField
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Permission"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Update Permission
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalUpdatePermission;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
// import {
|
||||||
|
// gachaRollItemSchema,
|
||||||
|
// TGachaRollItem
|
||||||
|
// } from '@imphnen-frontend-service/service';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const useItem = (
|
||||||
|
nextStep: () => void,
|
||||||
|
initialValues?: any
|
||||||
|
) => {
|
||||||
|
const form = useForm<any>({
|
||||||
|
// resolver: zodResolver(),
|
||||||
|
mode: 'all',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
console.log('Form data:', data);
|
||||||
|
nextStep();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfirmItem = (
|
||||||
|
onClose: () => void,
|
||||||
|
resetStep: () => void,
|
||||||
|
actionFunction?: () => Promise<boolean>,
|
||||||
|
messages?: {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
// const result = await actionFunction?.();
|
||||||
|
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||||
|
toast.success(messages?.success);
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(messages?.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
useReactTable,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import ModalAddPermission from './_components/modal-add-permission';
|
||||||
|
import ModalUpdatePermission from './_components/modal-update-permission';
|
||||||
|
import ModalDeletePermission from './_components/modal-delete-permission';
|
||||||
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: Permission[] = [
|
||||||
|
{ id: 1, name: 'Read' },
|
||||||
|
{ id: 2, name: 'Create' },
|
||||||
|
{ id: 3, name: 'Update' },
|
||||||
|
{ id: 4, name: 'Delete' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
|
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||||
|
const [showModalDeleteItem, setShowModalDeleteItem] = 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 columns: ColumnDef<Permission>[] = [
|
||||||
|
{
|
||||||
|
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: 'Name',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
cell: () => (
|
||||||
|
<div className="flex gap-[8px]">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModalUpdateItem(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<EditOutlined /> Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModalDeleteItem(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<DeleteOutlined /> Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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 className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Permissions</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-8 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama permissions"
|
||||||
|
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 gap-3 text-nowrap"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModalAddItem(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
Tambah Permissionss
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
data={mockData}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={9}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ModalAddPermission
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalAddItem}
|
||||||
|
onClose={() => setShowModalAddItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
<ModalUpdatePermission
|
||||||
|
isOpen={showModalUpdateItem}
|
||||||
|
onClose={() => setShowModalUpdateItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
<ModalDeletePermission
|
||||||
|
isOpen={showModalDeleteItem}
|
||||||
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
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 { useItem, useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalAddRole {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleAdd?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalAddRole = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
currentStep,
|
||||||
|
nextStep,
|
||||||
|
resetStep,
|
||||||
|
handleAdd,
|
||||||
|
}: IModalAddRole) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
}}
|
||||||
|
disableEscapeKeyDown={true}
|
||||||
|
>
|
||||||
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<StepTwo
|
||||||
|
onClose={onClose}
|
||||||
|
handleAdd={handleAdd}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepOneProps {
|
||||||
|
nextStep: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||||
|
const { form, onSubmit } = useItem(nextStep);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Tambah Roles
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
label="Nama Role"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Nama Role"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 items-start overflow-auto">
|
||||||
|
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
||||||
|
Permissions
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-x-6 overflow-x-scroll">
|
||||||
|
{[
|
||||||
|
'Gacha Items',
|
||||||
|
'Gacha Roll',
|
||||||
|
'Roll',
|
||||||
|
'Users',
|
||||||
|
'Gacha Claim',
|
||||||
|
].map((title) => (
|
||||||
|
<div
|
||||||
|
key={title}
|
||||||
|
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
||||||
|
>
|
||||||
|
<span className="text-nowrap text-label1">{title}</span>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`${title}-all`}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`${title}-all`} className="text-nowrap">
|
||||||
|
Check All
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr className="border-blue-200" />
|
||||||
|
<div className="flex flex-col items-start gap-4 mb-4">
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-read`} />
|
||||||
|
<label htmlFor={`${title}-read`}>Read</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-create`} />
|
||||||
|
<label htmlFor={`${title}-create`}>Create</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-update`} />
|
||||||
|
<label htmlFor={`${title}-update`}>Update</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-delete`} />
|
||||||
|
<label htmlFor={`${title}-delete`}>Delete</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||||
|
Tambah Role
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IStepTwoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
handleAdd?: () => Promise<boolean>;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
|
||||||
|
const { onConfirm, onCancel } = useConfirmItem(
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleAdd,
|
||||||
|
{
|
||||||
|
success: 'Data role berhasil ditambahkan',
|
||||||
|
error: 'Data role gagal ditambahkan',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal.Header className="mb-10 text-center items-center">
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Tambah Roles
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-center text-neutral-400">
|
||||||
|
Apakah kamu yakin ingin
|
||||||
|
<br /> menambahkan role ini?
|
||||||
|
</p>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex mb-0 gap-4">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Tambahkan
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalAddRole;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalDeletePermission {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleDelete?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalDeletePermission = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleDelete,
|
||||||
|
}: IModalDeletePermission) => {
|
||||||
|
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
||||||
|
success: 'Data roles berhasil dihapus',
|
||||||
|
error: 'Data roles gagal dihapus',
|
||||||
|
});
|
||||||
|
|
||||||
|
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?"
|
||||||
|
width={148}
|
||||||
|
className="self-center"
|
||||||
|
/>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||||
|
Delete Roles
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Apakah kamu yakin untuk menghapus role ini? Menghapus data ini
|
||||||
|
mungkin akan mempengaruhi fungsional sistem
|
||||||
|
</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="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full bg-danger-500 hover:bg-danger-600"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Hapus Role
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalDeletePermission;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useConfirmItem } from '../_hook/use-item';
|
||||||
|
|
||||||
|
interface IModalUpdatePermission {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleUpdate?: () => Promise<boolean>;
|
||||||
|
currentStep?: number;
|
||||||
|
nextStep: () => void;
|
||||||
|
prevStep: () => void;
|
||||||
|
resetStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalUpdatePermission = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
resetStep,
|
||||||
|
handleUpdate,
|
||||||
|
}: IModalUpdatePermission) => {
|
||||||
|
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
||||||
|
success: 'Perubahan roles berhasil dilakukan',
|
||||||
|
error: 'Perubahan roles gagal dilakukan',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
disableEscapeKeyDown={true}
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Update Roles
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<InputField
|
||||||
|
label="Nama Role"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Nama Role"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 items-start overflow-auto">
|
||||||
|
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
||||||
|
Permissions
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-x-6 overflow-x-scroll">
|
||||||
|
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
|
||||||
|
(title) => (
|
||||||
|
<div
|
||||||
|
key={title}
|
||||||
|
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
||||||
|
>
|
||||||
|
<span className="text-nowrap text-label1">{title}</span>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`${title}-all`}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`${title}-all`} className="text-nowrap">
|
||||||
|
Check All
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr className="border-blue-200" />
|
||||||
|
<div className="flex flex-col items-start gap-4 mb-4">
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-read`} />
|
||||||
|
<label htmlFor={`${title}-read`}>Read</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-create`} />
|
||||||
|
<label htmlFor={`${title}-create`}>Create</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-update`} />
|
||||||
|
<label htmlFor={`${title}-update`}>Update</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[8px] items-center">
|
||||||
|
<input type="checkbox" id={`${title}-delete`} />
|
||||||
|
<label htmlFor={`${title}-delete`}>Delete</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Update Role
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalUpdatePermission;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
// import {
|
||||||
|
// gachaRollItemSchema,
|
||||||
|
// TGachaRollItem
|
||||||
|
// } from '@imphnen-frontend-service/service';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const useItem = (
|
||||||
|
nextStep: () => void,
|
||||||
|
initialValues?: any
|
||||||
|
) => {
|
||||||
|
const form = useForm<any>({
|
||||||
|
// resolver: zodResolver(),
|
||||||
|
mode: 'all',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
console.log('Form data:', data);
|
||||||
|
nextStep();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfirmItem = (
|
||||||
|
onClose: () => void,
|
||||||
|
resetStep: () => void,
|
||||||
|
actionFunction?: () => Promise<boolean>,
|
||||||
|
messages?: {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
// const result = await actionFunction?.();
|
||||||
|
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||||
|
toast.success(messages?.success);
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(messages?.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
useReactTable,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import ModalAddRole from './_components/modal-add-role';
|
||||||
|
import ModalUpdateRole from './_components/modal-update-role';
|
||||||
|
import ModalDeleteRole from './_components/modal-delete-role';
|
||||||
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: Role[] = [
|
||||||
|
{ id: 1, name: 'Admin' },
|
||||||
|
{ id: 2, name: 'Admin Pembayaran' },
|
||||||
|
{ id: 3, name: 'Staff' },
|
||||||
|
{ id: 4, name: 'Staff Aktivasi User' },
|
||||||
|
{ id: 5, name: 'User' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
|
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||||
|
const [showModalDeleteItem, setShowModalDeleteItem] = 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 columns: ColumnDef<Role>[] = [
|
||||||
|
{
|
||||||
|
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: 'ID',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Roles Name',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
cell: () => (
|
||||||
|
<div className="flex gap-[8px]">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModalUpdateItem(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<EditOutlined /> Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModalDeleteItem(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<DeleteOutlined /> Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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 className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||||
|
<h1 className="text-p2 font-semibold">Roles</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-8 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama roles"
|
||||||
|
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 gap-3 text-nowrap"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModalAddItem(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
Tambah Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
data={mockData}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={9}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ModalAddRole
|
||||||
|
currentStep={currentStep}
|
||||||
|
isOpen={showModalAddItem}
|
||||||
|
onClose={() => setShowModalAddItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
<ModalUpdateRole
|
||||||
|
isOpen={showModalUpdateItem}
|
||||||
|
onClose={() => setShowModalUpdateItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
<ModalDeleteRole
|
||||||
|
isOpen={showModalDeleteItem}
|
||||||
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
resetStep={resetStep}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
+4
-12
@@ -2,27 +2,19 @@ import { useForm } from 'react-hook-form';
|
|||||||
import {
|
import {
|
||||||
authLoginSchema,
|
authLoginSchema,
|
||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
usePostLogin,
|
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useSession } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const postLogin = usePostLogin();
|
|
||||||
const form = useForm<TLoginRequest>({
|
const form = useForm<TLoginRequest>({
|
||||||
resolver: zodResolver(authLoginSchema),
|
resolver: zodResolver(authLoginSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const { signIn } = useSession();
|
||||||
postLogin.mutate(data, {
|
|
||||||
onSuccess: () => {
|
const onSubmit = form.handleSubmit((data) => signIn(data));
|
||||||
console.log('Success Login');
|
|
||||||
navigate('/dashboard');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
+10
-1
@@ -32,7 +32,16 @@ export const Components: FC = (): ReactElement => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" size="md" className="w-full">
|
<Button
|
||||||
|
disabled={
|
||||||
|
form.formState.isSubmitting ||
|
||||||
|
form.formState.isValidating ||
|
||||||
|
!form.formState.isValid
|
||||||
|
}
|
||||||
|
type="submit"
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
convertPagesToRoute,
|
convertPagesToRoute,
|
||||||
QueryProvider,
|
QueryProvider,
|
||||||
} from '@imphnen-frontend-service/utils';
|
} from '@imphnen-frontend-service/utils';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||||
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
|||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
|
<Toaster position="top-right" />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@@ -1,11 +1,108 @@
|
|||||||
import { LoaderFunctionArgs } from 'react-router-dom';
|
import {
|
||||||
|
PERMISSIONS,
|
||||||
|
SessionToken,
|
||||||
|
SessionUser,
|
||||||
|
} from '@imphnen-frontend-service/utils';
|
||||||
|
import { LoaderFunctionArgs, redirect } from 'react-router';
|
||||||
|
|
||||||
|
const mappingPublicRoutes = [
|
||||||
|
'/auth/login',
|
||||||
|
'/auth/forgot',
|
||||||
|
'/auth/new-password',
|
||||||
|
];
|
||||||
|
|
||||||
|
const mappingRoutePermissions = [
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
permissions: [PERMISSIONS.USERS.READ_LIST],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/create',
|
||||||
|
permissions: [PERMISSIONS.USERS.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/update',
|
||||||
|
permissions: [PERMISSIONS.USERS.UPDATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/detail',
|
||||||
|
permissions: [PERMISSIONS.USERS.READ_DETAIL],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles',
|
||||||
|
permissions: [PERMISSIONS.ROLES.READ_LIST],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles/create',
|
||||||
|
permissions: [PERMISSIONS.ROLES.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles/update',
|
||||||
|
permissions: [PERMISSIONS.ROLES.UPDATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles/detail',
|
||||||
|
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions/create',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions/update',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions/detail',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//TODO : Fix this later
|
||||||
|
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||||
|
// const fallback = mappingRoutePermissions.find((route) =>
|
||||||
|
// route.permissions.some((perm) => userPermissions.includes(perm))
|
||||||
|
// );
|
||||||
|
// return redirect(fallback?.path ?? '/auth/login');
|
||||||
|
// };
|
||||||
|
|
||||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
const session = SessionUser.get();
|
||||||
|
const session_token = SessionToken.get();
|
||||||
|
const token = session_token?.token?.access_token;
|
||||||
|
const userPermissions =
|
||||||
|
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||||
|
|
||||||
if (pathname) return null;
|
if (mappingPublicRoutes.includes(pathname)) {
|
||||||
|
if (token) return redirect('/dashboard');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) return redirect('/auth/login');
|
||||||
|
|
||||||
|
const matchedRoute = mappingRoutePermissions.find(
|
||||||
|
(route) => route.path === pathname
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchedRoute) {
|
||||||
|
const hasPermission =
|
||||||
|
!matchedRoute.permissions ||
|
||||||
|
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
return '/dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
authLoginSchema,
|
||||||
|
TLoginRequest,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useSession } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const form = useForm<TLoginRequest>({
|
||||||
|
resolver: zodResolver(authLoginSchema),
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { signIn, isLoading } = useSession();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => signIn(data));
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
TVerifyOtpRequest,
|
||||||
|
verifyEmailSchema,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useOtp } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
export const useOtpHook = () => {
|
||||||
|
const form = useForm<TVerifyOtpRequest>({
|
||||||
|
resolver: zodResolver(verifyEmailSchema),
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { otp, isLoading } = useOtp();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => otp(data));
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
authRegisterSchema,
|
||||||
|
TRegisterRequest,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useRegister } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
export const useRegisterHook = () => {
|
||||||
|
const form = useForm<TRegisterRequest>({
|
||||||
|
resolver: zodResolver(authRegisterSchema),
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, isLoading } = useRegister();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => register(data));
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,46 +1,50 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
import { ControlledInputField, LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useLogin } from '../../_hooks/use-login';
|
||||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [searchParams] = useSearchParams();
|
const { form, onSubmit, isLoading } = useLogin();
|
||||||
const error = searchParams.get('error'); // Ambil nilai ?error=
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
<div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
<div className="bg-white xl:min-w-[1120px] xl:min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
<LoginBanner />
|
<LoginBanner />
|
||||||
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
|
<div className="xl:border-2 xl:border-primary-500/50 md:w-[596px] rounded-lg md:py-[70px] md:px-[96px] flex justify-center">
|
||||||
<div className="w-[404px]">
|
<div className="md:w-[404px]">
|
||||||
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
||||||
Hallo Minna-san
|
Hallo Minna-san
|
||||||
</h2>
|
</h2>
|
||||||
<h5 className="text-xl font-medium text-primary-500 text-center">
|
<h5 className="text-xl font-medium text-primary-500 text-center mb-5">
|
||||||
Welcome to Dimentorin by IMPHNEN
|
Welcome to Dimentorin by IMPHNEN
|
||||||
</h5>
|
</h5>
|
||||||
<InputField
|
<form onSubmit={onSubmit} className="flex flex-col gap-2">
|
||||||
label="Email"
|
<ControlledInputField
|
||||||
error={error ? error : undefined}
|
control={form.control}
|
||||||
size="lg"
|
label="Email"
|
||||||
className="w-full"
|
size="lg"
|
||||||
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
className="w-full mb-2"
|
||||||
/>
|
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||||
<InputField
|
name={'email'}
|
||||||
label="password"
|
/>
|
||||||
error={error ? error : undefined}
|
<ControlledInputField
|
||||||
className="w-full"
|
control={form.control}
|
||||||
size="lg"
|
label="Password"
|
||||||
type="password"
|
className="w-full"
|
||||||
placeholder="Masukkan password rahasiamu!"
|
size="lg"
|
||||||
/>
|
type="password"
|
||||||
<div className="flex justify-end my-5">
|
placeholder="Masukkan password rahasiamu!"
|
||||||
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
name={'password'}
|
||||||
Lupa Password ?
|
/>
|
||||||
</a>
|
<div className="flex justify-end my-5">
|
||||||
</div>
|
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
||||||
<Button className="w-full">Enter Isekai</Button>
|
Lupa Password ?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" type='submit' disabled={(!form.formState.isValid || isLoading)}>Enter Isekai</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div className="flex my-3 gap-3 justify-center">
|
<div className="flex my-3 gap-3 justify-center">
|
||||||
<h5>Belum Punya akun ?</h5>
|
<h5>Belum Punya akun ?</h5>
|
||||||
<a href="/auth/register" className="text-primary-500 font-medium">
|
<a href="/auth/register" className="text-primary-500 font-medium">
|
||||||
@@ -63,6 +67,20 @@ export const Components: FC = (): ReactElement => {
|
|||||||
width={24}
|
width={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='xl:hidden w-full gap-3'
|
||||||
|
variant='secondary'
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
Kembali Ke Homepage
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='xl:hidden w-full gap-3'
|
||||||
|
variant='secondary'
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
Kembali Ke Homepage
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,37 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { useOtpHook } from '../../../_hooks/use-otp';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const { form, onSubmit, isLoading } = useOtpHook()
|
||||||
|
const [ searchParams ] = useSearchParams()
|
||||||
|
|
||||||
|
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'>
|
||||||
|
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Verifikasi Email</h3>
|
||||||
|
<h5 className='mt-2 text-xl font-medium text-primary-500'>Yeay~! Pesan dari dunia lain sudah dikirimkan ke {searchParams.get("email")}</h5>
|
||||||
|
<form onSubmit={onSubmit} className='mt-7'>
|
||||||
|
<ControlledInputField
|
||||||
|
label="OTP"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="XXXXXX"
|
||||||
|
max={6}
|
||||||
|
name={'otp'}
|
||||||
|
maxLength={6}
|
||||||
|
control={form.control}
|
||||||
|
/>
|
||||||
|
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -1,14 +1,108 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
import { useRegisterHook } from '../../_hooks/use-register';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const { form, onSubmit, isLoading } = useRegisterHook();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
<div className='bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6'>
|
<div className="bg-white xl:min-w-[1130px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
<RegisterResetBanner />
|
<RegisterResetBanner />
|
||||||
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center'>
|
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||||
<div className='w-[404px]'>
|
<div className="xl:w-[714px]">
|
||||||
|
<Button
|
||||||
|
className='xl:hidden gap-3'
|
||||||
|
variant='secondary'
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||||
|
Register
|
||||||
|
</h3>
|
||||||
|
<h5 className="text-primary-500 font-medium">
|
||||||
|
Yosha~! Saatnya Bergabung dengan Dimentorin
|
||||||
|
</h5>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className='my-7'>
|
||||||
|
<ControlledInputField
|
||||||
|
label="Full name"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
name={'fullname'}
|
||||||
|
control={form.control}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-flow-row-dense my-7 lg:grid-cols-2 gap-2">
|
||||||
|
<ControlledInputField
|
||||||
|
label="Email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Contoh : yourname@mail.com"
|
||||||
|
type='email'
|
||||||
|
control={form.control}
|
||||||
|
name={'email'}
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
label="No Hp"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Contoh : 088877665544"
|
||||||
|
control={form.control}
|
||||||
|
name={'phone_number'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ControlledInputField
|
||||||
|
label="Password"
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
type="password"
|
||||||
|
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||||
|
control={form.control}
|
||||||
|
name={'password'}
|
||||||
|
/>
|
||||||
|
<h6 className="text-sm text-gray-500 mt-1">
|
||||||
|
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||||
|
</h6>
|
||||||
|
<h6 className="text-sm text-gray-500 mt-2">
|
||||||
|
Tips membuat password yang OP:
|
||||||
|
</h6>
|
||||||
|
<h6 className="text-sm text-gray-500">
|
||||||
|
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||||
|
<span role="img" aria-label="emoji">
|
||||||
|
⚡
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
</h6>
|
||||||
|
<h6 className="text-sm text-gray-500">
|
||||||
|
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||||
|
ultimate!
|
||||||
|
<span role="img" aria-label="emoji">
|
||||||
|
🔥
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<h6 className="text-sm text-gray-500 mb-2">
|
||||||
|
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||||
|
musuh!
|
||||||
|
<span role="img" aria-label="emoji">
|
||||||
|
🚨
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<ControlledInputField
|
||||||
|
label="Repeat Password"
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
type="password"
|
||||||
|
placeholder="Ulangi password-mu, Senpai~!"
|
||||||
|
control={form.control}
|
||||||
|
name={"confirm_password"}
|
||||||
|
/>
|
||||||
|
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,63 @@
|
|||||||
|
import { FC, ReactElement, useEffect } from 'react';
|
||||||
|
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.location.href = "/auth/login"
|
||||||
|
}, 10000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
|
<div className="bg-white xl:min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className="border-2 border-primary-500/50 xl:w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||||
|
<div className="xl:w-[704px] flex flex-col justify-center items-center">
|
||||||
|
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||||
|
Register Successfull
|
||||||
|
</h2>
|
||||||
|
<h4 className="mt-3 text-primary-500 font-medium text-xl">
|
||||||
|
Yosha~! Saatnya Bergabung dengan Dimentorin!
|
||||||
|
</h4>
|
||||||
|
<svg
|
||||||
|
width="198"
|
||||||
|
height="198"
|
||||||
|
viewBox="0 0 198 198"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="mt-10"
|
||||||
|
>
|
||||||
|
<circle cx="99" cy="99" r="99" fill="#BCF8B0" />
|
||||||
|
<rect
|
||||||
|
x="31.1152"
|
||||||
|
y="32.9141"
|
||||||
|
width="133.971"
|
||||||
|
height="133.971"
|
||||||
|
rx="66.9857"
|
||||||
|
fill="#35BA43"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M127.935 75.8846H122.722C121.991 75.8846 121.297 76.2202 120.85 76.7945L90.0997 115.748L75.355 97.0657C75.1319 96.7825 74.8476 96.5535 74.5234 96.3959C74.1992 96.2384 73.8435 96.1563 73.483 96.1558H68.2697C67.77 96.1558 67.4941 96.7301 67.7999 97.1179L88.2277 122.998C89.1824 124.206 91.0171 124.206 91.9792 122.998L128.405 76.8392C128.711 76.4589 128.435 75.8846 127.935 75.8846Z"
|
||||||
|
fill="#E0FBD8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h4 className="mt-10 text-primary-500 font-medium text-xl">
|
||||||
|
Kamu akan memasuki isekai dalam 10 dtk
|
||||||
|
</h4>
|
||||||
|
<a href="/auth/login">
|
||||||
|
<Button className="gap-3 mt-10" size="lg">
|
||||||
|
Masuk Isekai
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
convertPagesToRoute,
|
convertPagesToRoute,
|
||||||
QueryProvider,
|
QueryProvider,
|
||||||
} from '@imphnen-frontend-service/utils';
|
} from '@imphnen-frontend-service/utils';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||||
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
|||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
|
<Toaster position="top-right" />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@@ -1,11 +1,113 @@
|
|||||||
import { LoaderFunctionArgs } from 'react-router-dom';
|
import {
|
||||||
|
PERMISSIONS,
|
||||||
|
SessionToken,
|
||||||
|
SessionUser,
|
||||||
|
} from '@imphnen-frontend-service/utils';
|
||||||
|
import { LoaderFunctionArgs, redirect } from 'react-router';
|
||||||
|
|
||||||
|
const mappingPublicRoutes = [
|
||||||
|
'/',
|
||||||
|
'/auth/login',
|
||||||
|
'/auth/forgot',
|
||||||
|
'/auth/forgot/otp',
|
||||||
|
'/auth/register',
|
||||||
|
'/auth/register/otp',
|
||||||
|
'/auth/register/success',
|
||||||
|
'/auth/new-password',
|
||||||
|
];
|
||||||
|
|
||||||
|
const mappingRoutePermissions = [
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
permissions: [PERMISSIONS.USERS.READ_LIST],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/create',
|
||||||
|
permissions: [PERMISSIONS.USERS.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/update',
|
||||||
|
permissions: [PERMISSIONS.USERS.UPDATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/detail',
|
||||||
|
permissions: [PERMISSIONS.USERS.READ_DETAIL],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles',
|
||||||
|
permissions: [PERMISSIONS.ROLES.READ_LIST],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles/create',
|
||||||
|
permissions: [PERMISSIONS.ROLES.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles/update',
|
||||||
|
permissions: [PERMISSIONS.ROLES.UPDATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/roles/detail',
|
||||||
|
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions/create',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions/update',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/permissions/detail',
|
||||||
|
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//TODO : Fix this later
|
||||||
|
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||||
|
// const fallback = mappingRoutePermissions.find((route) =>
|
||||||
|
// route.permissions.some((perm) => userPermissions.includes(perm))
|
||||||
|
// );
|
||||||
|
// return redirect(fallback?.path ?? '/auth/login');
|
||||||
|
// };
|
||||||
|
|
||||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
const session = SessionUser.get();
|
||||||
|
const session_token = SessionToken.get();
|
||||||
|
const token = session_token?.token?.access_token;
|
||||||
|
const userPermissions =
|
||||||
|
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||||
|
|
||||||
if (pathname) return null;
|
if (mappingPublicRoutes.includes(pathname)) {
|
||||||
|
if (token) return redirect('/dashboard');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) return redirect('/auth/login');
|
||||||
|
|
||||||
|
const matchedRoute = mappingRoutePermissions.find(
|
||||||
|
(route) => route.path === pathname
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchedRoute) {
|
||||||
|
const hasPermission =
|
||||||
|
!matchedRoute.permissions ||
|
||||||
|
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
return '/dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
@@ -2,6 +2,7 @@ import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { useLogin } from '../../_hooks/use-login';
|
import { useLogin } from '../../_hooks/use-login';
|
||||||
|
import ModalFormVerifyEmail from './modal-form-verify-email';
|
||||||
|
|
||||||
interface IModalFormLogin {
|
interface IModalFormLogin {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -16,61 +17,87 @@ const ModalFormLogin = ({
|
|||||||
onForgotPassword,
|
onForgotPassword,
|
||||||
setIsOpenRegisterModal,
|
setIsOpenRegisterModal,
|
||||||
}: IModalFormLogin) => {
|
}: IModalFormLogin) => {
|
||||||
const { form, onSubmit } = useLogin();
|
const {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
isLoading,
|
||||||
|
showVerifyModal,
|
||||||
|
verifyForm,
|
||||||
|
onVerifySubmit,
|
||||||
|
closeVerifyModal,
|
||||||
|
isVerifying,
|
||||||
|
emailToVerify,
|
||||||
|
} = useLogin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<>
|
||||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
<Modal
|
||||||
isOpen={isOpen}
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
onClose={onClose}
|
isOpen={isOpen && !showVerifyModal}
|
||||||
>
|
onClose={onClose}
|
||||||
<Modal.Header className="space-y-4">
|
>
|
||||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
<Modal.Header className="space-y-4">
|
||||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
Login
|
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||||
</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
|
Login
|
||||||
</Button>
|
</h1>
|
||||||
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
</Modal.Header>
|
||||||
<p className="text-neutral-500">Belum punya akun?</p>
|
<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" type="button" onClick={onForgotPassword}>
|
||||||
|
Lupa Password?
|
||||||
|
</Button>
|
||||||
|
</h1>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
type="submit"
|
||||||
className="text-primary-500 hover:text-primary-600 m-0 p-0"
|
size="md"
|
||||||
onClick={() => setIsOpenRegisterModal(true)}
|
className="w-full"
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Daftar
|
{isLoading ? 'Loading...' : 'Login'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
||||||
</form>
|
<p className="text-neutral-500">Belum punya akun?</p>
|
||||||
</Modal.Content>
|
<Button
|
||||||
</Modal>
|
variant="text"
|
||||||
|
className="text-primary-500 hover:text-primary-600 m-0 p-0"
|
||||||
|
onClick={() => setIsOpenRegisterModal(true)}
|
||||||
|
>
|
||||||
|
Daftar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ModalFormVerifyEmail
|
||||||
|
isOpen={showVerifyModal}
|
||||||
|
onClose={closeVerifyModal}
|
||||||
|
verifyForm={verifyForm}
|
||||||
|
onVerifySubmit={onVerifySubmit}
|
||||||
|
isVerifying={isVerifying}
|
||||||
|
email={emailToVerify}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
import { useRegister } from '../../_hooks/use-register';
|
import { useRegister } from '../../_hooks/use-register';
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { UseFormReturn } from 'react-hook-form';
|
import ModalFormVerifyEmail from './modal-form-verify-email';
|
||||||
import { TRegisterRequest } from '@imphnen-frontend-service/service';
|
|
||||||
|
|
||||||
interface IModalFormRegisterProps {
|
interface IModalFormRegisterProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -12,151 +10,100 @@ interface IModalFormRegisterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||||
const { form, onSubmit } = useRegister();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
step: currentStep,
|
form,
|
||||||
nextStep,
|
verifyForm,
|
||||||
prevStep,
|
onSubmit,
|
||||||
resetStep,
|
onVerifySubmit,
|
||||||
} = useQueryState('registerStep', {
|
showVerifyModal,
|
||||||
defaultValue: 1,
|
closeVerifyModal,
|
||||||
maxValue: 2,
|
isVerifying,
|
||||||
minValue: 1,
|
isRegistering,
|
||||||
});
|
registeredEmail,
|
||||||
|
} = useRegister();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<>
|
||||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
<Modal
|
||||||
isOpen={isOpen}
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
onClose={() => {
|
isOpen={isOpen && !showVerifyModal}
|
||||||
onClose();
|
onClose={onClose}
|
||||||
resetStep();
|
>
|
||||||
}}
|
<Modal.Header className="space-y-4">
|
||||||
>
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
<Modal.Header className="space-y-4">
|
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
Register
|
||||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
</h1>
|
||||||
Register
|
</Modal.Header>
|
||||||
</h1>
|
<Modal.Content>
|
||||||
<h2 className="text-neutral-500 text-lg text-center">
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
{currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'}
|
<ControlledInputField
|
||||||
</h2>
|
control={form.control}
|
||||||
</Modal.Header>
|
name="fullname"
|
||||||
<Modal.Content>
|
label="Nama Lengkap"
|
||||||
<form onSubmit={onSubmit} className="space-y-6">
|
placeholder="Masukkan Nama Lengkap"
|
||||||
{currentStep === 1 && (
|
type="text"
|
||||||
<StepOne form={form} nextStep={nextStep} onClose={onClose} />
|
size="lg"
|
||||||
)}
|
className="w-full"
|
||||||
{currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />}
|
/>
|
||||||
</form>
|
<ControlledInputField
|
||||||
</Modal.Content>
|
control={form.control}
|
||||||
</Modal>
|
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"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
control={form.control}
|
||||||
|
name="phone_number"
|
||||||
|
label="Nomor Telepon"
|
||||||
|
placeholder="Masukkan Nomor Telepon Aktif"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
disabled={isRegistering || !form.formState.isValid}
|
||||||
|
>
|
||||||
|
{isRegistering ? 'Mendaftar...' : 'Register'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ModalFormVerifyEmail
|
||||||
|
isOpen={showVerifyModal}
|
||||||
|
onClose={closeVerifyModal}
|
||||||
|
verifyForm={verifyForm}
|
||||||
|
onVerifySubmit={onVerifySubmit}
|
||||||
|
isVerifying={isVerifying}
|
||||||
|
email={registeredEmail}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ form, nextStep, onClose }: IStepOneProps) => (
|
|
||||||
<>
|
|
||||||
<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}>
|
|
||||||
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;
|
export default ModalFormRegister;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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 { UseFormReturn } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface IModalFormVerifyEmailProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
verifyForm: UseFormReturn<{ otp: string }, any>;
|
||||||
|
onVerifySubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
isVerifying: boolean;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFormVerifyEmail = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
verifyForm,
|
||||||
|
onVerifySubmit,
|
||||||
|
isVerifying,
|
||||||
|
email,
|
||||||
|
}: IModalFormVerifyEmailProps) => {
|
||||||
|
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">
|
||||||
|
Verifikasi Email
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-neutral-500 text-lg text-center">
|
||||||
|
Masukkan kode OTP yang telah dikirim ke {email ? email : 'email Anda'}
|
||||||
|
</h2>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<form onSubmit={onVerifySubmit} className="space-y-6">
|
||||||
|
<ControlledInputField
|
||||||
|
control={verifyForm.control}
|
||||||
|
name="otp"
|
||||||
|
label="Kode OTP"
|
||||||
|
placeholder="Masukkan Kode OTP"
|
||||||
|
type="text"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
disabled={isVerifying}
|
||||||
|
>
|
||||||
|
{isVerifying ? 'Memverifikasi...' : 'Verifikasi'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalFormVerifyEmail;
|
||||||
@@ -5,22 +5,74 @@ import {
|
|||||||
usePostLogin,
|
usePostLogin,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useAuthStore } from '@imphnen-frontend-service/utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useVerifyEmail } from './use-verify-email';
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const postLogin = usePostLogin();
|
|
||||||
const form = useForm<TLoginRequest>({
|
const form = useForm<TLoginRequest>({
|
||||||
resolver: zodResolver(authLoginSchema),
|
resolver: zodResolver(authLoginSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutate, isPending } = usePostLogin();
|
||||||
|
const { setLoading, setSession, clearSession } = useAuthStore();
|
||||||
|
|
||||||
|
const {
|
||||||
|
openVerifyModal,
|
||||||
|
showVerifyModal,
|
||||||
|
verifyForm,
|
||||||
|
onVerifySubmit,
|
||||||
|
closeVerifyModal,
|
||||||
|
isVerifying,
|
||||||
|
emailToVerify,
|
||||||
|
} = useVerifyEmail();
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
postLogin.mutate(data, {
|
setLoading(true);
|
||||||
onSuccess: () => console.log('Success Login'),
|
mutate(data, {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
toast.success('Login sukses');
|
||||||
|
|
||||||
|
if (response.data.token && response.data.user) {
|
||||||
|
setSession(response.data);
|
||||||
|
} else {
|
||||||
|
const { token, ...userData } = response.data;
|
||||||
|
|
||||||
|
const loginData = {
|
||||||
|
token,
|
||||||
|
userData,
|
||||||
|
};
|
||||||
|
setSession(loginData);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(0);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
const errorMessage = error?.response?.data?.message;
|
||||||
|
|
||||||
|
if (errorMessage === 'Account not active, please verify your email') {
|
||||||
|
toast.error('Akun belum aktif, silakan verifikasi email Anda');
|
||||||
|
openVerifyModal(data.email);
|
||||||
|
} else {
|
||||||
|
toast.error(errorMessage ?? 'Terjadi Kesalahan yang tidak diketahui');
|
||||||
|
clearSession();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
isLoading: isPending,
|
||||||
|
showVerifyModal,
|
||||||
|
verifyForm,
|
||||||
|
onVerifySubmit,
|
||||||
|
closeVerifyModal,
|
||||||
|
isVerifying,
|
||||||
|
emailToVerify,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,23 +6,51 @@ import {
|
|||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useVerifyEmail } from './use-verify-email';
|
||||||
|
|
||||||
export const useRegister = () => {
|
export const useRegister = () => {
|
||||||
const postRegister = usePostRegister();
|
const postRegister = usePostRegister();
|
||||||
|
const {
|
||||||
|
verifyForm,
|
||||||
|
onVerifySubmit,
|
||||||
|
showVerifyModal,
|
||||||
|
openVerifyModal,
|
||||||
|
closeVerifyModal,
|
||||||
|
isVerifying,
|
||||||
|
emailToVerify,
|
||||||
|
} = useVerifyEmail();
|
||||||
|
|
||||||
const form = useForm<TRegisterRequest>({
|
const form = useForm<TRegisterRequest>({
|
||||||
resolver: zodResolver(authRegisterSchema),
|
resolver: zodResolver(authRegisterSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
|
defaultValues: {
|
||||||
|
fullname: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
phone_number: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
postRegister.mutate(data, {
|
postRegister.mutate(data, {
|
||||||
onSuccess: (data) => toast.success(data.message),
|
onSuccess: () => {
|
||||||
onError: (error) => toast.error(error.message),
|
toast.success('Registrasi sukses. Silakan verifikasi email Anda.');
|
||||||
|
openVerifyModal(data.email);
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(error.message || 'Registrasi gagal'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
|
verifyForm,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onVerifySubmit,
|
||||||
|
showVerifyModal,
|
||||||
|
closeVerifyModal,
|
||||||
|
isVerifying,
|
||||||
|
isRegistering: postRegister.isPending,
|
||||||
|
registeredEmail: emailToVerify,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
TVerifyEmailRequest,
|
||||||
|
usePostVerifyEmail,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const useVerifyEmail = () => {
|
||||||
|
const [showVerifyModal, setShowVerifyModal] = useState(false);
|
||||||
|
const [emailToVerify, setEmailToVerify] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const postVerifyEmail = usePostVerifyEmail();
|
||||||
|
|
||||||
|
const verifyForm = useForm<{ otp: string }>({
|
||||||
|
mode: 'all',
|
||||||
|
defaultValues: {
|
||||||
|
otp: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onVerifySubmit = verifyForm.handleSubmit((data) => {
|
||||||
|
const otpNumber = data.otp;
|
||||||
|
|
||||||
|
const verifyData: TVerifyEmailRequest = {
|
||||||
|
email: emailToVerify,
|
||||||
|
otp: otpNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
postVerifyEmail.mutate(verifyData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Verifikasi email sukses');
|
||||||
|
|
||||||
|
navigate(0);
|
||||||
|
},
|
||||||
|
onError: (error) =>
|
||||||
|
toast.error(error.message || 'Verifikasi email gagal'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const openVerifyModal = (email: string) => {
|
||||||
|
setEmailToVerify(email);
|
||||||
|
setShowVerifyModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeVerifyModal = () => {
|
||||||
|
setShowVerifyModal(false);
|
||||||
|
verifyForm.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
verifyForm,
|
||||||
|
onVerifySubmit,
|
||||||
|
showVerifyModal,
|
||||||
|
openVerifyModal,
|
||||||
|
closeVerifyModal,
|
||||||
|
isVerifying: postVerifyEmail.isPending,
|
||||||
|
emailToVerify,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -66,11 +66,15 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<div className="order-1 lg:order-none flex flex-col col-span-4 justify-center items-center mb-8 relative">
|
<div className="order-1 lg:order-none flex flex-col col-span-4 justify-center items-center mb-8 relative">
|
||||||
<div className="relative h-[150px] md:h-[230px]">
|
<div className="relative h-[150px] md:h-[230px]">
|
||||||
<img
|
<img
|
||||||
|
width={400}
|
||||||
|
height={400}
|
||||||
src="/merch/1.png"
|
src="/merch/1.png"
|
||||||
alt="Merch 1"
|
alt="Merch 1"
|
||||||
className="relative top-[3px] md:top-[6px] left-[4px] md:left-[7px] z-10 w-[165px] md:w-[266px]"
|
className="relative top-[3px] md:top-[6px] left-[4px] md:left-[7px] z-10 w-[165px] md:w-[266px]"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
|
width={400}
|
||||||
|
height={400}
|
||||||
className="absolute top-0 left-0 min-w-[174px] md:min-w-[280px] z-0"
|
className="absolute top-0 left-0 min-w-[174px] md:min-w-[280px] z-0"
|
||||||
src="/merch/Vector-1.svg"
|
src="/merch/Vector-1.svg"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -88,7 +92,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
~ 175k ~
|
~ 175k ~
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative overflow-hidden">
|
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative">
|
||||||
<div className="relative h-[130px] md:h-[230px]">
|
<div className="relative h-[130px] md:h-[230px]">
|
||||||
<img
|
<img
|
||||||
width={400}
|
width={400}
|
||||||
@@ -105,8 +109,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
alt="Merch 3"
|
alt="Merch 3"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
|
className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
|
||||||
src="/landing-arrow-2.svg"
|
src="/landing-arrow-2.svg"
|
||||||
alt="Merch 1"
|
alt="Merch 1"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
|||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<Toaster />
|
<Toaster position="top-right"/>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { LoaderFunctionArgs } from 'react-router-dom';
|
// import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
|
||||||
|
import { LoaderFunctionArgs } from 'react-router';
|
||||||
|
|
||||||
|
// const mappingPublicRoutes = ['/'];
|
||||||
|
|
||||||
|
// const mappingRoutePermissions = [
|
||||||
|
// {
|
||||||
|
// path: '/',
|
||||||
|
// permissions: [],
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
const pathname = url.pathname;
|
|
||||||
|
|
||||||
if (pathname) return null;
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
NEXT_PUBLIC_API_URL=
|
||||||
|
|
||||||
|
TURNSTILE_SECRET_KEY=
|
||||||
|
NEXT_PUBLIC_TURNSTILE_SITEKEY=
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"jsc": {
|
||||||
|
"target": "es2017",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"decorators": true,
|
||||||
|
"dynamicImport": true
|
||||||
|
},
|
||||||
|
"transform": {
|
||||||
|
"decoratorMetadata": true,
|
||||||
|
"legacyDecorator": true
|
||||||
|
},
|
||||||
|
"keepClassNames": true,
|
||||||
|
"externalHelpers": true,
|
||||||
|
"loose": true
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "commonjs"
|
||||||
|
},
|
||||||
|
"sourceMaps": true,
|
||||||
|
"exclude": [
|
||||||
|
"jest.config.ts",
|
||||||
|
".*\\.spec.tsx?$",
|
||||||
|
".*\\.test.tsx?$",
|
||||||
|
"./src/jest-setup.ts$",
|
||||||
|
"./**/jest-setup.ts$",
|
||||||
|
".*.js$",
|
||||||
|
".*.d.ts$"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { fixupConfigRules } from '@eslint/compat';
|
||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: dirname(fileURLToPath(import.meta.url)),
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...fixupConfigRules(compat.extends('next')),
|
||||||
|
...fixupConfigRules(compat.extends('next/core-web-vitals')),
|
||||||
|
...baseConfig,
|
||||||
|
...nx.configs['flat/react-typescript'],
|
||||||
|
{
|
||||||
|
ignores: ['.next/**/*'],
|
||||||
|
},
|
||||||
|
];
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: any;
|
||||||
|
export const ReactComponent: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const { composePlugins, withNx } = require('@nx/next');
|
||||||
|
|
||||||
|
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
|
||||||
|
const nextConfig = {
|
||||||
|
nx: { svgr: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = composePlugins(withNx)(nextConfig);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "landing",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/landing",
|
||||||
|
"projectType": "application",
|
||||||
|
"tags": [],
|
||||||
|
"// targets": "to see all targets run: nx show project landing --web",
|
||||||
|
"targets": {
|
||||||
|
"serve": {
|
||||||
|
"executor": "@nx/next:server",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "landing:build",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/next:build",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/landing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
SiCplusplus,
|
||||||
|
SiCss3,
|
||||||
|
SiGo,
|
||||||
|
SiHtml5,
|
||||||
|
SiJavascript,
|
||||||
|
SiPhp,
|
||||||
|
SiPython,
|
||||||
|
SiRuby,
|
||||||
|
SiRust,
|
||||||
|
SiSwift,
|
||||||
|
SiTypescript,
|
||||||
|
} from 'react-icons/si';
|
||||||
|
|
||||||
|
export function AnimatedBackground() {
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const iconColorMap = useMemo(
|
||||||
|
() => [
|
||||||
|
{ Icon: SiJavascript, color: '#F7DF1E' },
|
||||||
|
{ Icon: SiTypescript, color: '#3178C6' },
|
||||||
|
{ Icon: SiPython, color: '#3776AB' },
|
||||||
|
{ Icon: SiCplusplus, color: '#00599C' },
|
||||||
|
{ Icon: SiRuby, color: '#CC342D' },
|
||||||
|
{ Icon: SiSwift, color: '#F05138' },
|
||||||
|
{ Icon: SiRust, color: '#000000' },
|
||||||
|
{ Icon: SiGo, color: '#00ADD8' },
|
||||||
|
{ Icon: SiPhp, color: '#777BB4' },
|
||||||
|
{ Icon: SiHtml5, color: '#E34F26' },
|
||||||
|
{ Icon: SiCss3, color: '#1572B6' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRandom = (min: number, max: number) =>
|
||||||
|
Math.random() * (max - min) + min;
|
||||||
|
|
||||||
|
const floatingIcons = useMemo(() => {
|
||||||
|
if (!isClient) return [];
|
||||||
|
|
||||||
|
return Array.from({ length: 40 }).map((_, i) => {
|
||||||
|
const { Icon, color } = iconColorMap[i % iconColorMap.length];
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="pointer-events-none absolute"
|
||||||
|
style={{
|
||||||
|
top: `${getRandom(0, 100)}%`,
|
||||||
|
left: `${getRandom(0, 100)}%`,
|
||||||
|
fontSize: `${getRandom(16, 32)}px`,
|
||||||
|
color: color,
|
||||||
|
opacity: getRandom(0.1, 0.2),
|
||||||
|
rotate: getRandom(-180, 180),
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, getRandom(-100, 100), 0],
|
||||||
|
x: [0, getRandom(-50, 50), 0],
|
||||||
|
rotate: getRandom(-180, 180),
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: getRandom(15, 25),
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: 'loop',
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon className="h-full w-full" />
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [isClient, iconColorMap]);
|
||||||
|
|
||||||
|
if (!isClient) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-0 overflow-hidden">{floatingIcons}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export async function fetchPostverifyTurnstile(
|
||||||
|
token: string,
|
||||||
|
remoteIp?: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
secret: String(process.env.TURNSTILE_SECRET_KEY),
|
||||||
|
response: token,
|
||||||
|
});
|
||||||
|
if (remoteIp) {
|
||||||
|
params.append('remoteip', remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: params.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Turnstile verify HTTP error', res.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as TurnstileVerifyResponse;
|
||||||
|
if (!data.success) {
|
||||||
|
console.warn('Turnstile failure', data['error-codes']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TurnstileVerifyResponse {
|
||||||
|
success: boolean;
|
||||||
|
challenge_ts: string;
|
||||||
|
hostname: string;
|
||||||
|
'error-codes'?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { fetcher } from '@/lib/fetcher';
|
||||||
|
import { getRemoteIp } from '@/lib/headers';
|
||||||
|
import { fetchPostverifyTurnstile } from '../../_http/fetch-post-verify-turnstile';
|
||||||
|
import {
|
||||||
|
forgotPasswordValidationSchema,
|
||||||
|
ForgotPasswordValidationType,
|
||||||
|
} from '../_validation/forgot-password-validation';
|
||||||
|
|
||||||
|
export async function ForgotPasswordAction(
|
||||||
|
request: ForgotPasswordValidationType
|
||||||
|
) {
|
||||||
|
const validRequest = forgotPasswordValidationSchema.parse(request);
|
||||||
|
const remoteIp = await getRemoteIp();
|
||||||
|
|
||||||
|
const isCapchaValid = await fetchPostverifyTurnstile(
|
||||||
|
validRequest.token,
|
||||||
|
remoteIp
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCapchaValid) throw new Error('Failed to verify captcha');
|
||||||
|
|
||||||
|
const { data, error } = await fetcher.POST('/v1/auth/forgot', {
|
||||||
|
body: {
|
||||||
|
email: validRequest.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components';
|
||||||
|
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { LuLoader } from 'react-icons/lu';
|
||||||
|
import { useFormForgotPassword } from '../_hooks/use-form-forgot-password';
|
||||||
|
import { usePostForgotPassowrd } from '../_hooks/use-post-forgot-password';
|
||||||
|
import { ForgotPasswordValidationType } from '../_validation/forgot-password-validation';
|
||||||
|
|
||||||
|
export function ForgotPasswordForm() {
|
||||||
|
const ref = useRef<TurnstileInstance | null>(null);
|
||||||
|
|
||||||
|
const [step, setStep] = useState<number>(1);
|
||||||
|
const [emailValue, setEmailValue] = useState<string>('');
|
||||||
|
|
||||||
|
const form = useFormForgotPassword();
|
||||||
|
const { mutate, isPending, error } = usePostForgotPassowrd(form);
|
||||||
|
|
||||||
|
const handleFirstStep = (values: ForgotPasswordValidationType) => {
|
||||||
|
setEmailValue(values.email);
|
||||||
|
setStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondStep = () => {
|
||||||
|
mutate({ email: emailValue, token: form.getValues('token') });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={
|
||||||
|
step === 1
|
||||||
|
? form.handleSubmit(handleFirstStep)
|
||||||
|
: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSecondStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
className="w-full space-y-4"
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="emailmu@mail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<Turnstile
|
||||||
|
ref={ref}
|
||||||
|
siteKey={String(process.env.NEXT_PUBLIC_TURNSTILE_SITEKEY)}
|
||||||
|
onSuccess={(token) => form.setValue('token', token)}
|
||||||
|
options={{ theme: 'light', size: 'flexible', language: 'id' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending || (step === 2 && !form.watch('token'))}
|
||||||
|
className="w-full hover:bg-[#5fbaef] bg-[#22a5f1] font-bold"
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<LuLoader className="h-5 w-5 animate-spin" />
|
||||||
|
) : step === 1 ? (
|
||||||
|
'Selanjutnya'
|
||||||
|
) : (
|
||||||
|
'Reset password'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
forgotPasswordValidationSchema,
|
||||||
|
type ForgotPasswordValidationType,
|
||||||
|
} from '../_validation/forgot-password-validation';
|
||||||
|
|
||||||
|
export function useFormForgotPassword() {
|
||||||
|
return useForm<ForgotPasswordValidationType>({
|
||||||
|
resolver: zodResolver(forgotPasswordValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
token: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { ForgotPasswordAction } from '../_actions/forgot-password-action';
|
||||||
|
import { useFormForgotPassword } from './use-form-forgot-password';
|
||||||
|
|
||||||
|
export function usePostForgotPassowrd(
|
||||||
|
form: ReturnType<typeof useFormForgotPassword>
|
||||||
|
) {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ForgotPasswordAction,
|
||||||
|
onSuccess: ({ message }) => {
|
||||||
|
form.reset();
|
||||||
|
toast.success(message);
|
||||||
|
},
|
||||||
|
onError: ({ message }) => {
|
||||||
|
form.reset();
|
||||||
|
toast.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const forgotPasswordValidationSchema = z.object({
|
||||||
|
email: z.string().email({ message: 'Format email tidak valid' }),
|
||||||
|
token: z.string(),
|
||||||
|
});
|
||||||
|
export type ForgotPasswordValidationType = z.infer<
|
||||||
|
typeof forgotPasswordValidationSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { LogoSimple } from '@/app/_components/logo';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import { ForgotPasswordForm } from './_components/forgot-password-form';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'IMPHNEN - Signin',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LogoSimple />
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-center text-sm sm:text-base">
|
||||||
|
Reset password akunmu
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ForgotPasswordForm />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { baiJamjureeFont } from '@/lib/fonts';
|
||||||
|
import { Card } from '@components';
|
||||||
|
import { cn } from '@utils';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { AnimatedBackground } from './_components/animated-background';
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
baiJamjureeFont.className,
|
||||||
|
'bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AnimatedBackground />
|
||||||
|
<div className="z-10 w-full">
|
||||||
|
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
|
||||||
|
<div className="flex flex-col items-center space-y-6">
|
||||||
|
<div className="w-full space-y-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { fetcher } from '@/lib/fetcher';
|
||||||
|
import {
|
||||||
|
resetPasswordValidationSchema,
|
||||||
|
ResetPasswordValidationSchema,
|
||||||
|
} from '../_validation/reset-password-validation';
|
||||||
|
|
||||||
|
export async function resetPasswordAction(
|
||||||
|
request: ResetPasswordValidationSchema
|
||||||
|
) {
|
||||||
|
const validRequest = resetPasswordValidationSchema.parse(request);
|
||||||
|
|
||||||
|
if (validRequest.confirm_password !== validRequest.confirm_password) {
|
||||||
|
throw new Error('Password missmatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await fetcher.POST('/v1/auth/new-password', {
|
||||||
|
body: {
|
||||||
|
password: validRequest.password,
|
||||||
|
token: validRequest.token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components';
|
||||||
|
import { LuLoaderCircle } from 'react-icons/lu';
|
||||||
|
import { usePostResetPassword } from '../_hooks/use-post-reset-password';
|
||||||
|
import { useResetPasswordForm } from '../_hooks/use-reset-password-form';
|
||||||
|
import { ResetPasswordValidationSchema } from '../_validation/reset-password-validation';
|
||||||
|
|
||||||
|
export function ResetPasswordForm() {
|
||||||
|
const form = useResetPasswordForm();
|
||||||
|
const { mutate, error, isPending } = usePostResetPassword(form);
|
||||||
|
|
||||||
|
const onSubmit = (values: ResetPasswordValidationSchema) => {
|
||||||
|
mutate(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm w-full">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>New Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirm_password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirm New Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={isPending}>
|
||||||
|
{isPending ? (
|
||||||
|
<LuLoaderCircle className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Ubah password'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { resetPasswordAction } from '../_actions/reset-password-actions';
|
||||||
|
import { useResetPasswordForm } from './use-reset-password-form';
|
||||||
|
|
||||||
|
export function usePostResetPassword(
|
||||||
|
form: ReturnType<typeof useResetPasswordForm>
|
||||||
|
) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: resetPasswordAction,
|
||||||
|
onSuccess: ({ message }) => {
|
||||||
|
form.reset();
|
||||||
|
router.replace('/signin');
|
||||||
|
toast.success(message);
|
||||||
|
},
|
||||||
|
onError: ({ message }) => {
|
||||||
|
form.reset();
|
||||||
|
toast.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import {
|
||||||
|
resetPasswordValidationSchema,
|
||||||
|
ResetPasswordValidationSchema,
|
||||||
|
} from '../_validation/reset-password-validation';
|
||||||
|
|
||||||
|
export function useResetPasswordForm() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const tokenFromQuery = searchParams.get('token') ?? '';
|
||||||
|
|
||||||
|
return useForm<ResetPasswordValidationSchema>({
|
||||||
|
resolver: zodResolver(resetPasswordValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
token: tokenFromQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const resetPasswordValidationSchema = z
|
||||||
|
.object({
|
||||||
|
password: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Password tidak boleh kosong',
|
||||||
|
invalid_type_error: 'Password harus berupa string',
|
||||||
|
})
|
||||||
|
.min(8, 'Password harus minimal 8 karakter')
|
||||||
|
.max(50, 'Password tidak boleh lebih dari 50 karakter')
|
||||||
|
.regex(
|
||||||
|
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*\W).+$/,
|
||||||
|
'Password harus mengandung setidaknya satu huruf kapital, satu huruf kecil, satu angka, dan satu karakter spesial'
|
||||||
|
),
|
||||||
|
confirm_password: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Konfirmasi password tidak boleh kosong',
|
||||||
|
})
|
||||||
|
.min(8, 'Konfirmasi password harus minimal 8 karakter')
|
||||||
|
.max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'),
|
||||||
|
token: z.string(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirm_password, {
|
||||||
|
message: 'Password dan Konfirmasi Password harus sama',
|
||||||
|
path: ['confirm_password'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResetPasswordValidationSchema = z.infer<
|
||||||
|
typeof resetPasswordValidationSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { LogoSimple } from '@/app/_components/logo';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { ResetPasswordForm } from './_components/reset-password-form';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ token?: string }>;
|
||||||
|
}) {
|
||||||
|
const { token } = use(searchParams);
|
||||||
|
|
||||||
|
if (!token) redirect('/signin');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LogoSimple />
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-center text-sm sm:text-base">
|
||||||
|
Ubah passwordmu
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { setAccessToken, setRefreshToken } from '@/lib/cookies';
|
||||||
|
import { fetchPostSignin } from '../_http/fetch-post-signin';
|
||||||
|
import {
|
||||||
|
type SignInValidationType,
|
||||||
|
signInValidationSchema,
|
||||||
|
} from '../_validation/signin-validation';
|
||||||
|
|
||||||
|
export async function SigninAction(request: SignInValidationType) {
|
||||||
|
const validRequest = signInValidationSchema.parse(request);
|
||||||
|
|
||||||
|
const { data } = await fetchPostSignin(validRequest);
|
||||||
|
|
||||||
|
const accessToken = data.token.access_token;
|
||||||
|
const refreshToken = data.token.refresh_token;
|
||||||
|
|
||||||
|
await setAccessToken(accessToken);
|
||||||
|
await setRefreshToken(refreshToken);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { LuLoader } from 'react-icons/lu';
|
||||||
|
import { useFormSignin } from '../_hooks/use-form-signin';
|
||||||
|
import { usePostSignin } from '../_hooks/use-post-signin';
|
||||||
|
import { type SignInValidationType } from '../_validation/signin-validation';
|
||||||
|
|
||||||
|
export function SigninForm() {
|
||||||
|
const form = useFormSignin();
|
||||||
|
const { mutate, isPending, error } = usePostSignin(form);
|
||||||
|
|
||||||
|
const onSubmit = (values: SignInValidationType) => {
|
||||||
|
mutate(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="emailmu@mail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<div className="text-right mt-1">
|
||||||
|
<Link
|
||||||
|
href="/forgot-password"
|
||||||
|
className="text-sm text-primary-500 font-bold hover:underline"
|
||||||
|
>
|
||||||
|
Lupa Password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" disabled={isPending} className="w-full font-bold">
|
||||||
|
{isPending ? <LuLoader className="h-5 w-5 animate-spin" /> : 'Masuk'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
signInValidationSchema,
|
||||||
|
type SignInValidationType,
|
||||||
|
} from '../_validation/signin-validation';
|
||||||
|
|
||||||
|
export function useFormSignin() {
|
||||||
|
return useForm<SignInValidationType>({
|
||||||
|
resolver: zodResolver(signInValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { SigninAction } from '../_actions/signin-action';
|
||||||
|
import { useFormSignin } from './use-form-signin';
|
||||||
|
|
||||||
|
export function usePostSignin(form: ReturnType<typeof useFormSignin>) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: SigninAction,
|
||||||
|
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push('/');
|
||||||
|
},
|
||||||
|
onError: (error, variables) => {
|
||||||
|
form.resetField('password');
|
||||||
|
|
||||||
|
if (error.message.includes('not active')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push(`/verification?ref=${variables.email}`);
|
||||||
|
}, 750);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { fetcher } from '@/lib/fetcher';
|
||||||
|
import { SignInValidationType } from '../_validation/signin-validation';
|
||||||
|
|
||||||
|
export async function fetchPostSignin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}: SignInValidationType) {
|
||||||
|
const { data, error, response } = await fetcher.POST('/v1/auth/login', {
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Someting went wrong, please try again later');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const signInValidationSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Email tidak boleh kosong',
|
||||||
|
invalid_type_error: 'Email harus berupa string',
|
||||||
|
})
|
||||||
|
.min(1, 'Email tidak boleh kosong')
|
||||||
|
.email('Email harus valid'),
|
||||||
|
password: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Password tidak boleh kosong',
|
||||||
|
invalid_type_error: 'Password harus berupa string',
|
||||||
|
})
|
||||||
|
.min(1, 'Password tidak boleh kosong'),
|
||||||
|
});
|
||||||
|
export type SignInValidationType = z.infer<typeof signInValidationSchema>;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { LogoSimple } from '@/app/_components/logo';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { SigninForm } from './_components/signin-form';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'IMPHNEN - Signin',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LogoSimple />
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-center text-sm sm:text-base">
|
||||||
|
Masuk untuk mengakses akunmu
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SigninForm />
|
||||||
|
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
<p className="text-muted-foreground px-4 text-center text-xs leading-5 text-balance sm:text-sm">
|
||||||
|
Belum punya akun?{' '}
|
||||||
|
<Link
|
||||||
|
href="/signup"
|
||||||
|
className="font-bold hover:underline hover:underline-offset-4 transition-colors text-primary-500"
|
||||||
|
>
|
||||||
|
Daftar
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { getRemoteIp } from '@/lib/headers';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { fetchPostverifyTurnstile } from '../../_http/fetch-post-verify-turnstile';
|
||||||
|
import { fetchPostSignin } from '../_http/fetch-post-signup';
|
||||||
|
import { signupValidationSchema } from '../_validation/signup-validation';
|
||||||
|
|
||||||
|
export async function SignupAction(
|
||||||
|
request: z.infer<typeof signupValidationSchema>
|
||||||
|
) {
|
||||||
|
const validRequest = signupValidationSchema.parse(request);
|
||||||
|
const remoteIp = await getRemoteIp();
|
||||||
|
|
||||||
|
const isCapchaValid = await fetchPostverifyTurnstile(
|
||||||
|
validRequest.token,
|
||||||
|
remoteIp
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCapchaValid) throw new Error('Failed to verify captcha');
|
||||||
|
|
||||||
|
const data = await fetchPostSignin(validRequest);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { LuLoader } from 'react-icons/lu';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { SignupAction } from '../_actions/signup-action';
|
||||||
|
import {
|
||||||
|
signupValidationSchema,
|
||||||
|
stepOneSignupValidationSchema,
|
||||||
|
stepTwoSignupValidationSchema,
|
||||||
|
} from '../_validation/signup-validation';
|
||||||
|
|
||||||
|
export function SignupForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const ref = useRef<TurnstileInstance | null>(null);
|
||||||
|
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [stepOneData, setStepOneData] = useState<z.infer<
|
||||||
|
typeof stepOneSignupValidationSchema
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
const firstForm = useForm<z.infer<typeof stepOneSignupValidationSchema>>({
|
||||||
|
resolver: zodResolver(stepOneSignupValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
phone_number: '',
|
||||||
|
fullname: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondForm = useForm<z.infer<typeof stepTwoSignupValidationSchema>>({
|
||||||
|
resolver: zodResolver(stepTwoSignupValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
token: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate, isPending, error } = useMutation({
|
||||||
|
mutationFn: async (data: z.infer<typeof signupValidationSchema>) => {
|
||||||
|
const result = await SignupAction(data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
email: data.email,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onSuccess: ({ email }) => {
|
||||||
|
router.push(`/verification?ref=${email}`);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
ref.current?.reset();
|
||||||
|
secondForm.resetField('token');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFirstSubmit = (
|
||||||
|
values: z.infer<typeof stepOneSignupValidationSchema>
|
||||||
|
) => {
|
||||||
|
setStepOneData(values);
|
||||||
|
setStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondSubmit = (
|
||||||
|
values: z.infer<typeof stepTwoSignupValidationSchema>
|
||||||
|
) => {
|
||||||
|
if (stepOneData) {
|
||||||
|
mutate({ ...stepOneData, ...values });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{step === 1 && (
|
||||||
|
<Form {...firstForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={firstForm.handleSubmit(handleFirstSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={firstForm.control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Full Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Nama Lengkap" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={firstForm.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="emailmu@mail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={firstForm.control}
|
||||||
|
name="phone_number"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nomor Telepon</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="08123456789" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={firstForm.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={firstForm.control}
|
||||||
|
name="confirm_password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-[#5fbaef] hover:bg-[#22a5f1]"
|
||||||
|
>
|
||||||
|
Selanjutnya
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<Form {...secondForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={secondForm.handleSubmit(handleSecondSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Turnstile
|
||||||
|
ref={ref}
|
||||||
|
siteKey={String(process.env.NEXT_PUBLIC_TURNSTILE_SITEKEY)}
|
||||||
|
onSuccess={(token) => secondForm.setValue('token', token)}
|
||||||
|
options={{
|
||||||
|
theme: 'light',
|
||||||
|
size: 'flexible',
|
||||||
|
language: 'id',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending || !secondForm.watch('token')}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<LuLoader className="h-5 w-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Daftar'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user