Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0017aff5f4 | ||
|
|
e52776e68a | ||
|
|
cd06a71a9e | ||
|
|
bc5b072ad8 | ||
|
|
0fcd93cb7d | ||
|
|
3470853858 | ||
|
|
ff9a02c020 | ||
|
|
f81a7c334e | ||
|
|
2700f7cbb7 | ||
|
|
cfcdfa527c | ||
|
|
c6579d5295 | ||
|
|
66b8b064ba | ||
|
|
63643cc286 | ||
|
|
19bd44a7ca | ||
|
|
a1640cfe1d | ||
|
|
00df69fb93 | ||
|
|
b3bdf25b5f | ||
|
|
810dbaa0ca | ||
|
|
cdd4fd4703 | ||
|
|
7d1b86acdb | ||
|
|
6e6c361eb8 | ||
|
|
cb09709800 | ||
|
|
62780c8889 | ||
|
|
1839f4cab0 | ||
|
|
7e385c6385 | ||
|
|
19aa5bb452 | ||
|
|
c9f5b9e4a5 | ||
|
|
33365affa2 | ||
|
|
63db7ff6ad | ||
|
|
7f8b53edd4 | ||
|
|
e8de897dc9 | ||
|
|
eb0628ac3e | ||
|
|
cbb880a053 | ||
|
|
083a912e91 | ||
|
|
eff89d28ed | ||
|
|
ab744b411d | ||
|
|
b75a88677a | ||
|
|
8947b4d05e | ||
|
|
7e3eaa6584 | ||
|
|
98a459c7d7 | ||
|
|
7642fef3e6 | ||
|
|
f40e0e35b9 | ||
|
|
c6c0d6fded | ||
|
|
9d24a11c05 | ||
|
|
ee60787100 | ||
|
|
301a349776 | ||
|
|
81488ceb6b | ||
|
|
9e99fd61e7 | ||
|
|
d36c693615 | ||
|
|
20125a6f4a | ||
|
|
4bc5006c91 | ||
|
|
71769b71aa | ||
|
|
a2cf67d210 | ||
|
|
d9c4217fd3 | ||
|
|
fcaa449630 | ||
|
|
4988980384 | ||
|
|
9547d34f5c | ||
|
|
aeb23a7255 | ||
|
|
719e36e46c | ||
|
|
bd052f4866 | ||
|
|
734e8540f2 | ||
|
|
1289555749 | ||
|
|
971fe7160f | ||
|
|
722caeec9d | ||
|
|
7404afe466 | ||
|
|
400daea60f | ||
|
|
40cd47c6be | ||
|
|
529d065a75 | ||
|
|
69d6d86342 | ||
|
|
31736cc403 | ||
|
|
1cb2443b58 | ||
|
|
9e9bfc2793 | ||
|
|
5a99f216cb | ||
|
|
efdbc65be6 | ||
|
|
f5905c0acc | ||
|
|
6348a4998b | ||
|
|
e067450fcb | ||
|
|
a281f279f8 | ||
|
|
a157e11e13 | ||
|
|
b38afd2b44 | ||
|
|
3c301ec2c4 | ||
|
|
65a6bb686c | ||
|
|
f3a19fa460 | ||
|
|
228f92e9ed | ||
|
|
dcd4ef88d6 | ||
|
|
ec13f2c693 | ||
|
|
302ee37ca1 | ||
|
|
b7282f9c99 | ||
|
|
2c347d1cc5 | ||
|
|
791f43986d | ||
|
|
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 |
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npx nx run-many:*)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(npm view:*)",
|
||||||
|
"Bash(npx nx build landing)",
|
||||||
|
"Bash(npm uninstall:*)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(ren page.tsx page-original.tsx)",
|
||||||
|
"Bash(ren:*)",
|
||||||
|
"Bash(npx supabase:*)",
|
||||||
|
"Bash(libs/service/src/types/supabase.ts)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Nx
|
||||||
|
.nx
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
.editorconfig
|
||||||
|
.prettierrc
|
||||||
|
.eslintrc
|
||||||
@@ -1 +1,3 @@
|
|||||||
VITE_API_URL=https://api.example.com
|
|
||||||
|
VITE_SUPABASE_URL=https://your-project-id.supabase.co
|
||||||
|
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Backoffice
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/backoffice/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-backoffice ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-backoffice
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-backoffice && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Dimentorin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/dimentorin/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-dimentorin ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-dimentorin
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-dimentorin && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Gacha
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/gacha/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-gacha ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-gacha
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-gacha && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-gacha/apps/gacha/.env << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Deploy Hackathon
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/hackathon/**' # Auto deploy when changes pushed to apps/hackathon
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-hackathon ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-hackathon
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-hackathon && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-hackathon/.env.local << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml down || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --env-file ~/imphnen-frontend-service-hackathon/.env.local -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Landing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service/apps/landing/.env << EOF
|
||||||
|
NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -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
|
||||||
@@ -51,3 +51,18 @@ 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
|
||||||
|
.cursor/rules/nx-rules.mdc
|
||||||
|
.github/instructions/nx.instructions.md
|
||||||
|
|
||||||
|
.env.local
|
||||||
|
|||||||
@@ -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,19 +92,22 @@ 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
|
||||||
|
|
||||||
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
||||||
|
|
||||||
- **Run Storybook**
|
- **Run Storybook**
|
||||||
|
|
||||||
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run ui:storybook
|
npm run ui:storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- **Run Unit Test**
|
- **Run Unit Test**
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Tue, Nov 25, 2025 4:21:27 PM
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Deployment trigger
|
||||||
|
# Updated to deploy circular dependency fixes and auth hooks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -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,52 @@
|
|||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
||||||
|
import { PieLabelProps } from "recharts/types/polar/Pie"
|
||||||
|
|
||||||
|
type ChartProps = {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: ChartProps[] = [
|
||||||
|
{ name: "Active", value: 49, color: "#23A1EB" },
|
||||||
|
{ name: "Done", value: 24, color: "#81CBF8" },
|
||||||
|
{ name: "Canceled", value: 27, color: "#BCE1FB" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: PieLabelProps) => {
|
||||||
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||||
|
const x = cx + radius * Math.cos(-(midAngle ?? 0) * RADIAN);
|
||||||
|
const y = cy + radius * Math.sin(-(midAngle ?? 0) * RADIAN);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
||||||
|
{`${((percent ?? 1) * 100).toFixed(0)}%`}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessionStatusChart = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
|
<PieChart width={500} height={320}>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={40}
|
||||||
|
outerRadius={100}
|
||||||
|
labelLine={false}
|
||||||
|
label={renderCustomizedLabel}
|
||||||
|
>
|
||||||
|
{chartData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
||||||
|
|
||||||
|
type ChartProps = {
|
||||||
|
name: string
|
||||||
|
activeUser: number
|
||||||
|
activeSession: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: ChartProps[] = [
|
||||||
|
{ name: "2014", activeUser: 0, activeSession: 0 },
|
||||||
|
{ name: "2015", activeUser: 15, activeSession: 25 },
|
||||||
|
{ name: "2016", activeUser: 30, activeSession: 40 },
|
||||||
|
{ name: "2017", activeUser: 45, activeSession: 55 },
|
||||||
|
{ name: "2018", activeUser: 60, activeSession: 70 },
|
||||||
|
{ name: "2019", activeUser: 75, activeSession: 85 },
|
||||||
|
{ name: "2020", activeUser: 90, activeSession: 95 },
|
||||||
|
{ name: "2021", activeUser: 85, activeSession: 80 },
|
||||||
|
{ name: "2022", activeUser: 95, activeSession: 90 },
|
||||||
|
{ name: "2023", activeUser: 100, activeSession: 100 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const UserGrowthChart = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
|
<LineChart data={chartData} width={500} height={320} margin={{ left: -32 }}>
|
||||||
|
<CartesianGrid />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis tickCount={10} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line dataKey="activeUser" stroke="#23A1EB" />
|
||||||
|
<Line dataKey="activeSession" stroke="#0877C1" />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { UserGrowthChart } from "./_components/chart/user-growth";
|
||||||
|
import { SessionStatusChart } from "./_components/chart/session-status";
|
||||||
|
|
||||||
|
const Overview = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white px-6 py-4 rounded-md shadow">
|
||||||
|
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
|
||||||
|
<p className="text-neutral-400 text-p3">Total Users</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
||||||
|
|
||||||
|
<div className="space-y-14">
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
||||||
|
Overview
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-5">
|
||||||
|
<For data={Array.from({ length: 5 })}>
|
||||||
|
{(_, index) => <Overview key={index} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
||||||
|
Trends & Analytics
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-x-5">
|
||||||
|
<div className="bg-white px-6 py-4 rounded-lg col-span-5">
|
||||||
|
<div className="flex items-center justify-between mb-7">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700">User Growth</h2>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<UserGrowthChart />
|
||||||
|
</div>
|
||||||
|
<div className="bg-white px-6 py-4 rounded-lg col-span-2">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700 mb-7">Session Status</h2>
|
||||||
|
<SessionStatusChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-5">
|
||||||
|
<div className="bg-white px-7 py-4 rounded-lg">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top 5 Mentors</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-label1 bg-primary-50 text-left rounded-full">
|
||||||
|
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
||||||
|
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
||||||
|
<th className="font-medium py-4 px-5 rounded-r-lg">Avg Rating</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<For data={Array.from({ length: 5 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<tr key={index} className="shadow rounded-lg">
|
||||||
|
<td className="py-4 px-5">{index + 1}</td>
|
||||||
|
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
||||||
|
<td className="py-4 px-5">4.9</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white px-6 py-4 rounded-lg">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top Booked Mentoring Topics</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
||||||
|
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
||||||
|
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
||||||
|
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<For data={Array.from({ length: 5 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<tr key={index} className="shadow rounded-lg">
|
||||||
|
<td className="py-4 px-5">{index + 1}</td>
|
||||||
|
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
||||||
|
<td className="py-4 px-5">1000</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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,181 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { ReactElement, useState } from "react"
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
MENTORING: 'Mentoring',
|
||||||
|
PLATFORM: 'Platform'
|
||||||
|
} as const
|
||||||
|
type Tabs = typeof TABS[keyof typeof TABS]
|
||||||
|
|
||||||
|
type FeedbackStatus = 'done' | 'todo';
|
||||||
|
|
||||||
|
interface FeedbackType {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
rating: number
|
||||||
|
status: FeedbackStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||||
|
email: 'fullname23@gmail.com',
|
||||||
|
rating: 4.5,
|
||||||
|
status: i % 2 === 0 ? 'done' : 'todo',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<FeedbackType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
accessorKey: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rating',
|
||||||
|
header: 'Rating',
|
||||||
|
accessorKey: 'rating',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<FeedbackStatus, string> = {
|
||||||
|
done: 'bg-success-200 text-success-500',
|
||||||
|
todo: 'bg-primary-200 text-primary-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<FeedbackStatus, string> = {
|
||||||
|
done: 'Done',
|
||||||
|
todo: 'To Do',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-72") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<SearchOutlined className="text-[16px]" /> Lihat Feedback
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<div className="mb-8 flex justify-between items-center">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700">Feedback</h1>
|
||||||
|
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
||||||
|
<For data={Object.values(TABS)}>
|
||||||
|
{(tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
variant="text"
|
||||||
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama mentor/mentee"
|
||||||
|
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>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Rating</option>
|
||||||
|
<option value="4.5">4.5</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Status</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
|
|||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
<BackofficeSidebar />
|
<BackofficeSidebar />
|
||||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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,85 @@
|
|||||||
|
import { ArrowRightOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { Accordion, Modal, ModalProps } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const ModalCreateRoadmap: FC<Omit<ModalProps, 'children'>> = ({ isOpen, onClose }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div className="overflow-y-auto max-h-[80vh]">
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Create Roadmaps
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-6 mb-12">
|
||||||
|
<div className="col-span-3">
|
||||||
|
<label className={labelClass}>Prompt</label>
|
||||||
|
<Textarea
|
||||||
|
className="min-w-full w-full h-[calc(100%-2rem)]"
|
||||||
|
placeholder="Create a learning roadmap for (subject) at the (level) level. Include topics, estimated duration, and logical progression."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Roadmap Name</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Nama Roadmap" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Roadmap Name</label>
|
||||||
|
<Select className="w-full">
|
||||||
|
<option value="pemula">Pemula</option>
|
||||||
|
<option value="menengah">Menengah</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Model</label>
|
||||||
|
<Select className="w-full">
|
||||||
|
<option value="gpt-3.5-turbo">GPT 3.5 Turbo</option>
|
||||||
|
<option value="gpt-4">GPT 4</option>
|
||||||
|
<option value="gpt-4-32k">GPT 4 32k</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full">
|
||||||
|
<Button>
|
||||||
|
Generate Roadmap
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-neutral-600 text-p2 font-semibold mb-8">
|
||||||
|
Roadmap Preview
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<For data={Array.from({ length: 3 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<Accordion
|
||||||
|
key={index}
|
||||||
|
title={`Day ${index + 1}`}
|
||||||
|
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In convallis tincidunt nisl, id consequat mi malesuada vel. Nulla facilisi. Nam in turpis ligula."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-12">
|
||||||
|
<Button type="button" className="flex items-center gap-5">
|
||||||
|
Submit Roadmap
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap";
|
||||||
|
|
||||||
|
type LearningStatus = 'active' | 'inactive'
|
||||||
|
|
||||||
|
type RoadmapType = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
learningLevel: string
|
||||||
|
status: LearningStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i === 0 ? 'Ahmad Wijuana' : 'Anna Wiguana',
|
||||||
|
learningLevel: ['Pemula', 'Menengah'][Math.floor(Math.random() * 2)],
|
||||||
|
status: i % 2 === 0 ? 'active' : 'inactive',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): React.ReactElement {
|
||||||
|
const [openCreateModal, setOpenCreateModal] = useState(false)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<RoadmapType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
header: 'No',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Nama Roadmap',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'learningLevel',
|
||||||
|
header: 'Tingkat Belajar',
|
||||||
|
accessorKey: 'learningLevel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<LearningStatus, string> = {
|
||||||
|
inactive: 'bg-danger-200 text-danger-700',
|
||||||
|
active: 'bg-success-200 text-success-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<LearningStatus, string> = {
|
||||||
|
inactive: 'Inactive',
|
||||||
|
active: 'Active',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-72") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex gap-[8px]">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<EditOutlined /> Action
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
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 (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Content & Roadmap</h1>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex items-center justify-between mb-9">
|
||||||
|
<h2 className="text-p2 font-semibold text-neutral-600">AI Roadmaps</h2>
|
||||||
|
<Button type="button" variant="primary" className="flex items-center gap-2" onClick={() => setOpenCreateModal(true)}>
|
||||||
|
<PlusOutlined /> Buat Roadmap
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama roadmap"
|
||||||
|
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>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Tingkat Belajar</option>
|
||||||
|
<option value="pemula">Pemula</option>
|
||||||
|
<option value="menengah">Menengah</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const TOPICS = [
|
||||||
|
{ id: 2, icon: '🏢', name: 'Industry Insight' },
|
||||||
|
{ id: 4, icon: '🖥️', name: 'Basic IT' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||||
|
|
||||||
|
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||||
|
|
||||||
|
Saya ingin tahu secara khusus tentang ___.
|
||||||
|
1.Pertanyaan Anda
|
||||||
|
2. ...
|
||||||
|
3. ...`
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalDetailSession: FC<ModalProps> = ({ open, setOpen }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Detail Sesi
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-9 px-9 py-7 border rounded-md gap-12 mb-10">
|
||||||
|
<div className="col-span-4 flex items-center gap-x-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentor</h2>
|
||||||
|
<div>
|
||||||
|
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
|
||||||
|
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Icon icon="ph:arrow-right" className="text-9xl text-primary-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-5 flex items-center gap-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentee</h2>
|
||||||
|
<div>
|
||||||
|
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
|
||||||
|
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-p2 font-semibold text-neutral-700 mb-4">Status</h2>
|
||||||
|
<div className="py-2 px-6 rounded-md text-center bg-success-200 text-success-500 font-semibold">
|
||||||
|
Finished
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p3 font-medium mb-2.5">Topics</h1>
|
||||||
|
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5 mb-8">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium"
|
||||||
|
>
|
||||||
|
<span>{item.icon} </span>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Tanggal</label>
|
||||||
|
<Input type="date" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Waktu</label>
|
||||||
|
<Input type="time" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="relative md:col-span-full">
|
||||||
|
<label className={labelClass}>Lokasi</label>
|
||||||
|
<Select className="min-w-full w-full">
|
||||||
|
<option value="online">Online</option>
|
||||||
|
<option value="offline">Offline</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-full">
|
||||||
|
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||||
|
<Textarea
|
||||||
|
className="min-w-full w-full h-40"
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { ReactElement, useState } from "react";
|
||||||
|
import { ModalDetailSession } from "./_components/modal/detail";
|
||||||
|
|
||||||
|
type SessionStatus = 'ongoing' | 'finished';
|
||||||
|
|
||||||
|
interface SessionType {
|
||||||
|
id: string
|
||||||
|
mentorName: string
|
||||||
|
menteeName: string
|
||||||
|
datetime: number
|
||||||
|
status: SessionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: `DS-${i + 1}`,
|
||||||
|
mentorName: 'Ahmad Wijuana',
|
||||||
|
menteeName: 'Sofia Wijuana',
|
||||||
|
datetime: new Date().getTime(),
|
||||||
|
status: i % 2 === 0 ? 'ongoing' : 'finished',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const [openDetail, setOpenDetail] = useState(false);
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<SessionType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
header: 'ID Sesi',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mentorName',
|
||||||
|
header: 'Nama Mentor',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'menteeName',
|
||||||
|
header: 'Nama Mentee',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'datetime',
|
||||||
|
header: 'Waktu',
|
||||||
|
accessorKey: 'datetime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<SessionStatus, string> = {
|
||||||
|
ongoing: 'bg-warning-200 text-warning-700',
|
||||||
|
finished: 'bg-success-200 text-success-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<SessionStatus, string> = {
|
||||||
|
ongoing: 'On Going',
|
||||||
|
finished: 'Finished',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-52") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpenDetail(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<SearchOutlined className="text-[16px]" /> Cek Detail
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Session Management</h1>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap"
|
||||||
|
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>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Rating</option>
|
||||||
|
<option value="4.5">4.5</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Status</option>
|
||||||
|
<option value="finished">Finished</option>
|
||||||
|
<option value="ongoing">On Going</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button, Input, Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const GeneralSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">General Settings</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ToggleInput label="Mode Maintenance" />
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Platform Settings</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Platform</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Bahasa</label>
|
||||||
|
<Select defaultValue="id" className="w-full">
|
||||||
|
<option value="id">Indonesia</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Logo Platform</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Favicon</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Legal Settings</h2>
|
||||||
|
<div className="grid gap-x-8 gap-y-5 mb-20">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>URL Syarat & Ketentuan</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>URL Kebijakan Privasi</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Button, Textarea, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export const NotificationSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Notification Settings</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
|
<ToggleInput label="Nyalakan Notifikasi Email" />
|
||||||
|
<ToggleInput label="Beritahu mentor ketika ada request" />
|
||||||
|
<ToggleInput label="Beritahu mentee untuk update sesi " />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="text-neutral-800 font-medium inline-block mb-2 text-p3">
|
||||||
|
API Integrasi Notifikasi (URL)
|
||||||
|
</label>
|
||||||
|
<Textarea placeholder="Masukkan url API notifikasi yang akan digunakan" className="w-full h-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const PaymentSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Payment</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Integrasi Payment Gateway</label>
|
||||||
|
<Textarea className="min-w-full w-full h-20" placeholder="Durasi dalam menit" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Mata Uang</label>
|
||||||
|
<Select defaultValue="idr">
|
||||||
|
<option value="idr">Rupiah (IDR)</option>
|
||||||
|
<option value="usd">Dollar (USD)</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Harga sesi mentoring <span className="text-neutral-600">(default)</span></label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Masukkan harga sesi mentoring" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Tarif Komisi untuk Platform</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Masukkan persentase komisi" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Invoice</h2>
|
||||||
|
<div className="mb-32">
|
||||||
|
<label className={labelClass}>Masukkan Format Invoice</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" placeholder="InvoiceDimentorin.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const SecuritySettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Security Settings</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-8 mb-20">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Durasi Session Timeout</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Durasi dalam menit" />
|
||||||
|
<p className="text-[10px] text-neutral-800">Auto logout setelah X menit tidak aktif</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Blokir Setelah Upaya Gagal</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="x kali perobaan login" />
|
||||||
|
<p className="text-[10px] text-neutral-800">Misal: 5 kali salah login = lock akun</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { DeleteOutlined, UserSwitchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
|
||||||
|
type UserRolesPermissionType = {
|
||||||
|
id: number
|
||||||
|
role: string
|
||||||
|
totalUser: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
|
||||||
|
totalUser: 10
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const UserRolesPermission: FC = () => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<UserRolesPermissionType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role',
|
||||||
|
header: 'Role',
|
||||||
|
accessorKey: 'role',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'totalUser',
|
||||||
|
header: 'Total User',
|
||||||
|
accessorKey: 'totalUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-96") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<UserSwitchOutlined className="text-[16px]" /> Manage Permissions
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<DeleteOutlined className="text-[16px]" /> Delete Role
|
||||||
|
</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 (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
|
||||||
|
<Button type="button">
|
||||||
|
Add Rols
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white shadow p-8 rounded-lg">
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { GeneralSettings } from "./_components/general"
|
||||||
|
import { UserRolesPermission } from "./_components/user-roles-permission"
|
||||||
|
import { NotificationSettings } from "./_components/notification"
|
||||||
|
import { SecuritySettings } from "./_components/security"
|
||||||
|
import { PaymentSettings } from "./_components/payment"
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
general: "General Settings",
|
||||||
|
userRolePermissions: "User Roles & Permissions",
|
||||||
|
notification: "Notification Settings",
|
||||||
|
security: "Security",
|
||||||
|
payment: "Payment",
|
||||||
|
} as const
|
||||||
|
type Tabs = typeof TABS[keyof typeof TABS]
|
||||||
|
|
||||||
|
export default function Components(): React.ReactElement {
|
||||||
|
const [activeTab, setActiveTab] = useState<Tabs>(TABS.general)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Settings</h1>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-x-8">
|
||||||
|
<div className="w-64 bg-white p-2.5 shadow space-y-2 rounded-md">
|
||||||
|
<For data={Object.values(TABS)}>
|
||||||
|
{(tab) => (
|
||||||
|
<button
|
||||||
|
key={tab}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100",
|
||||||
|
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600"
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white px-8 py-6 shadow space-y-2 rounded-md flex-1">
|
||||||
|
{activeTab === TABS.general && <GeneralSettings />}
|
||||||
|
{activeTab === TABS.userRolePermissions && <UserRolesPermission />}
|
||||||
|
{activeTab === TABS.notification && <NotificationSettings />}
|
||||||
|
{activeTab === TABS.security && <SecuritySettings />}
|
||||||
|
{activeTab === TABS.payment && <PaymentSettings />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
type ModalDeleteProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
hadnleDelete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalDelete: FC<ModalDeleteProps> = ({ open, setOpen, hadnleDelete }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
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">
|
||||||
|
Hapus Akun
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Apakah kamu yakin untuk menghapus akun ini?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="lg"
|
||||||
|
className="w-full border-danger-500 text-danger-500 hover:bg-danger-50 hover:text-danger-600 hover:border-danger-600"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => hadnleDelete?.()}
|
||||||
|
>
|
||||||
|
Hapus Akun
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import { ModalDelete } from "../delete";
|
||||||
|
import { ModalProps } from "../type";
|
||||||
|
import { ModalSuspendOrBan } from "../suspend-or-ban";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
export const AccountProfile: FC<ModalProps> = ({ setOpen }) => {
|
||||||
|
const [openDelete, setOpenDelete] = useState(false)
|
||||||
|
const [openSuspendOrBan, setOpenSuspendOrBan] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-x-8 mb-8">
|
||||||
|
<div className="size-[100px] rounded-full overflow-hidden">
|
||||||
|
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" variant="bordered" className="mb-4">
|
||||||
|
Upload Foto
|
||||||
|
</Button>
|
||||||
|
<p className="text-neutral-400 font-medium">
|
||||||
|
Setidaknya rekomendasi ukuran 240x240 px. <br />
|
||||||
|
.jpg, .jpeg, .png diperbolehkan
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h1>
|
||||||
|
<div className="grid grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Depan</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Nama Depan" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Belakang</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Nama Belakang" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Email</label>
|
||||||
|
<Input type="email" className="min-w-full w-full" placeholder="Email" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nomor Telepon</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="+62 81234567890" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="mb-7 text-p2 font-semibold">Status Akun</h1>
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
className="border-danger-500 text-danger-500"
|
||||||
|
onClick={() => setOpenSuspendOrBan(true)}
|
||||||
|
>
|
||||||
|
Suspend/Ban
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" variant="danger" onClick={() => setOpenDelete(true)}>
|
||||||
|
Delete Akun
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-x-5">
|
||||||
|
<Button type="button" variant="bordered" onClick={() => setOpen(false)}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button" disabled>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalDelete open={openDelete} setOpen={setOpenDelete} />
|
||||||
|
<ModalSuspendOrBan open={openSuspendOrBan} setOpen={setOpenSuspendOrBan} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons"
|
||||||
|
import { Input, Select } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { DataTable } from "@imphnen-frontend-service/ui/organisms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"
|
||||||
|
import { FC, useState } from "react"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
|
interface ActivityLogType {
|
||||||
|
id: number
|
||||||
|
date: string
|
||||||
|
menu: string
|
||||||
|
activity: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: ActivityLogType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
menu: 'Mentoring',
|
||||||
|
activity: 'Mentoring Session',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const ActivityLog: FC = () => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns: ColumnDef<ActivityLogType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-16") },
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'date',
|
||||||
|
header: 'Waktu',
|
||||||
|
accessorKey: 'date',
|
||||||
|
cell: (info) => dayjs(info.row.original.date).format('DD MMMM YYYY, HH:mm WIB'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'menu',
|
||||||
|
header: 'Menu',
|
||||||
|
accessorKey: 'menu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'activity',
|
||||||
|
header: 'Activity',
|
||||||
|
accessorKey: 'activity',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="p-8 shadow rounded-lg">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-9">
|
||||||
|
<div className="relative w-1/2">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari aktivitas"
|
||||||
|
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="w-1/4">
|
||||||
|
<Input type="date" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Menu</option>
|
||||||
|
<option value="profle">Profile</option>
|
||||||
|
<option value="profle-2">Profile</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Icon } from "@iconify/react"
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const SOCIAL_LINKS = [
|
||||||
|
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, url: "https://linkedin.com", label: "LinkedIn" },
|
||||||
|
{ icon: <Icon icon="mdi:github" className="text-2xl" />, url: "https://github.com", label: "Github" },
|
||||||
|
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, url: "https://facebook.com", label: "Facebook (Meta)" },
|
||||||
|
{ icon: <Icon icon="mdi:stack-overflow" className="text-2xl" />, url: "https://stackoverflow.com", label: "Stack Overflow" }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const DetailProfile: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="shadow rounded-lg p-8 mb-7">
|
||||||
|
<div className="flex justify-between items-start mb-6">
|
||||||
|
<div className="flex items-center gap-x-6">
|
||||||
|
<div className="size-[54px] rounded-full overflow-hidden">
|
||||||
|
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-800">Muhammad Firdaus Oiwobo</h1>
|
||||||
|
<p className="text-p3 font-medium text-neutral-600">Mentor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="text" className="bg-primary-100">
|
||||||
|
Actively Seeking Job
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<For data={SOCIAL_LINKS}>
|
||||||
|
{({ icon, url, label }) => (
|
||||||
|
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||||
|
<Button type="button" size="sm" className="flex items-center gap-x-2">
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-7">
|
||||||
|
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h1>
|
||||||
|
<p className="text-p3 font-medium text-neutral-600">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-8 py-10 rounded-lg shadow h-max">
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h1>
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||||
|
<Icon icon="ic:outline-mail" className="text-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-p3">
|
||||||
|
<p className="text-neutral-800 font-semibold">rzalaxib23@gmail.com</p>
|
||||||
|
<p className="text-neutral-600 font-medium">Email Address</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||||
|
<Icon icon="cil:phone" className="text-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-p3">
|
||||||
|
<p className="text-neutral-800 font-semibold">+62 888 8888 8888</p>
|
||||||
|
<p className="text-neutral-600 font-medium">Phone Number</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||||
|
<Icon icon="ion:location-outline" className="text-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-p3">
|
||||||
|
<p className="text-neutral-800 font-semibold">Jl. Mergosari, Kec. Suryakencana, Banjaran</p>
|
||||||
|
<p className="text-neutral-600 font-medium">Location</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules"
|
||||||
|
import { cn, For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC, useEffect, useState } from "react"
|
||||||
|
import { AccountProfile } from "./account-profile"
|
||||||
|
import { DetailProfile } from "./detail-profile"
|
||||||
|
import { ActivityLog } from "./activity-log"
|
||||||
|
import { ModalDetailUserProps } from "./type"
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
account: 'account profile',
|
||||||
|
detail: 'detail profile',
|
||||||
|
activity: 'activity logs',
|
||||||
|
} as const
|
||||||
|
type TabType = typeof TABS[keyof typeof TABS]
|
||||||
|
|
||||||
|
export const ModalDetailUser: FC<ModalDetailUserProps> = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>(TABS.account)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) setActiveTab(TABS.account)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="xl:max-w-[84rem] bg-white px-10 py-9"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Detail - Muhammad Firdaus Oiwobo
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md w-max mb-8">
|
||||||
|
<For data={Object.values(TABS)}>
|
||||||
|
{(tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
variant="text"
|
||||||
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Show condition={activeTab === TABS.account}>
|
||||||
|
<AccountProfile open={open} setOpen={setOpen} />
|
||||||
|
</Show>
|
||||||
|
<Show condition={activeTab === TABS.detail}>
|
||||||
|
<DetailProfile />
|
||||||
|
</Show>
|
||||||
|
<Show condition={activeTab === TABS.activity}>
|
||||||
|
<ActivityLog />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ModalProps } from "../type";
|
||||||
|
|
||||||
|
export interface ModalDetailUserProps extends ModalProps {
|
||||||
|
userId?: number | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { ModalProps } from "../type";
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { Button, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
export const ModalSuspendOrBan: FC<ModalProps> = ({ open, setOpen }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="bg-white px-10 py-9 xl:max-w-[32rem]"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Suspend/Ban
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Suspend/Ban</label>
|
||||||
|
<Select defaultValue="suspend" className="w-full">
|
||||||
|
<option value="suspend">Suspend</option>
|
||||||
|
<option value="banned">Banned</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Alasan</label>
|
||||||
|
<Textarea placeholder="Masukkan alasan suspend/ban" className="w-full h-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="button" className="w-full">
|
||||||
|
Selesai
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ModalProps {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { ReactElement, useState } from "react";
|
||||||
|
import { ModalDetailUser } from "./_components/modal/detail";
|
||||||
|
|
||||||
|
type UserStatus = 'active' | 'inactive';
|
||||||
|
|
||||||
|
interface UserType {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
rating: number
|
||||||
|
status: UserStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||||
|
email: 'fullname23@gmail.com',
|
||||||
|
rating: 4.5,
|
||||||
|
status: i % 2 === 0 ? 'active' : 'inactive',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const TABS = ['mentor', 'mentee'] as const
|
||||||
|
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
|
||||||
|
const [showDetail, setShowDetail] = useState(false)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<UserType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
accessorKey: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rating',
|
||||||
|
header: 'Rating',
|
||||||
|
accessorKey: 'rating',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<UserStatus, string> = {
|
||||||
|
active: 'bg-success-200 text-success-500',
|
||||||
|
inactive: 'bg-danger-200 text-danger-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<UserStatus, string> = {
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-72") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedUserId(row.original.id);
|
||||||
|
setShowDetail(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<div className="mb-8 flex justify-between items-center">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700">User Management</h1>
|
||||||
|
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
||||||
|
<For data={TABS}>
|
||||||
|
{(tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
variant="text"
|
||||||
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap"
|
||||||
|
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>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Rating</option>
|
||||||
|
<option value="4.5">4.5</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Status</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ModalDetailUser
|
||||||
|
open={showDetail}
|
||||||
|
setOpen={setShowDetail}
|
||||||
|
userId={selectedUserId}
|
||||||
|
/>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -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,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,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;
|
|
||||||
@@ -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 @@
|
|||||||
|
Tue, Nov 25, 2025 4:21:27 PM
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Deployment trigger
|
||||||
|
# Updated to deploy circular dependency fixes and auth hooks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 511 KiB |