Compare commits
206
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d3e2470c8 | ||
|
|
48411d3a08 | ||
|
|
426b71f341 | ||
|
|
06c69cb3c5 | ||
|
|
81999aa044 | ||
|
|
d83a6d9ec7 | ||
|
|
63dfb7299f | ||
|
|
53079fe868 | ||
|
|
2944a2b98a | ||
|
|
79f087e2fc | ||
|
|
d5b267332f | ||
|
|
8cb7b592eb | ||
|
|
b2ed098067 | ||
|
|
8749e83dad | ||
|
|
00b0997a9b | ||
|
|
05bdac6ee0 | ||
|
|
5cbad2a996 | ||
|
|
e78002ff69 | ||
|
|
b48f653a97 | ||
|
|
63da701b1b | ||
|
|
7bef667e96 | ||
|
|
82d1fae3d5 | ||
|
|
49d8419c1c | ||
|
|
4240e8eb51 | ||
|
|
86d9e759a6 | ||
|
|
6b3faf2a21 | ||
|
|
bcfac70e2a | ||
|
|
cb32146daa | ||
|
|
b82b03a8a2 | ||
|
|
0e0ca9720a | ||
|
|
964dd009cc | ||
|
|
a7ec622925 | ||
|
|
931355e64e | ||
|
|
58fbfcd0d8 | ||
|
|
063587aca9 | ||
|
|
7576b2557d | ||
|
|
95bf09c2fa | ||
|
|
0ade3c3e50 | ||
|
|
e945da4b7f | ||
|
|
61b650bb25 | ||
|
|
e61c003e07 | ||
|
|
8fff0df753 | ||
|
|
054f16053d | ||
|
|
0193245551 | ||
|
|
7f5948724b | ||
|
|
2cba806cd5 | ||
|
|
414e4d19b8 | ||
|
|
7268403ca4 | ||
|
|
651fd65ccb | ||
|
|
4e9d110bb0 | ||
|
|
c6719b19e5 | ||
|
|
53ad40f3fd | ||
|
|
8d2aaf5188 | ||
|
|
3f27e8d622 | ||
|
|
629d609792 | ||
|
|
8439c5d5e0 | ||
|
|
1afd7c014c | ||
|
|
419335d6b3 | ||
|
|
aef8d59c9f | ||
|
|
e14f6d4ef7 | ||
|
|
aa3e935fee | ||
|
|
1100d6f417 | ||
|
|
54ebcf1a1e | ||
|
|
9dabbb625d | ||
|
|
3f4461c65c | ||
|
|
f68d97188c | ||
|
|
f882668e7f | ||
|
|
dab4fa4941 | ||
|
|
bbb1bae909 | ||
|
|
3de1748f12 | ||
|
|
a610e180ba | ||
|
|
ce8cb8188c | ||
|
|
3aaf022393 | ||
|
|
ebe27a3f05 | ||
|
|
f846a5a785 | ||
|
|
708cbde596 | ||
|
|
9d37cba65f | ||
|
|
8728d0abd7 | ||
|
|
1f4f4ddaa1 | ||
|
|
4fcc249ff4 | ||
|
|
c2bed98b06 | ||
|
|
ae1864c134 | ||
|
|
3f30fbe89c | ||
|
|
951db0f28a | ||
|
|
ad59d39f55 | ||
|
|
1283689d84 | ||
|
|
7d1370d7f2 | ||
|
|
ab0b56780f | ||
|
|
1a39ac37db | ||
|
|
e5dc784301 | ||
|
|
b2de22b0ef | ||
|
|
ad20eda182 | ||
|
|
da65aa4109 | ||
|
|
1f0798752c | ||
|
|
5f28b9b406 | ||
|
|
9e8fc7c07f | ||
|
|
ca51f8aca2 | ||
|
|
c086b1371c | ||
|
|
398350795d | ||
|
|
5be9ec1edf | ||
|
|
f77967a092 | ||
|
|
fe172cc128 | ||
|
|
38ee9a9549 | ||
|
|
3ad828e759 | ||
|
|
782cce820b | ||
|
|
f5882e3cdc | ||
|
|
afaaff4a7f | ||
|
|
da8b08cd73 | ||
|
|
1dbe704230 | ||
|
|
59c1bb0366 | ||
|
|
c518784810 | ||
|
|
0267de664b | ||
|
|
134ff14946 | ||
|
|
ed3b711c89 | ||
|
|
6b7b1c4d20 | ||
|
|
b4b19e02fa | ||
|
|
e61979cbb6 | ||
|
|
0c7887268c | ||
|
|
76933c0be7 | ||
|
|
abc4c26d5d | ||
|
|
a3d35716a6 | ||
|
|
ac22bd6cab | ||
|
|
27ed5605ce | ||
|
|
2c6389a67a | ||
|
|
a4c75770ae | ||
|
|
d923237969 | ||
|
|
b2a28c6fd3 | ||
|
|
417ff3390b | ||
|
|
0c646ceb85 | ||
|
|
6bbc87479a | ||
|
|
23b6677323 | ||
|
|
45c75b3283 | ||
|
|
764eb34e47 | ||
|
|
fe4e9291fa | ||
|
|
ec9a81d7b5 | ||
|
|
e652c6ed0e | ||
|
|
d431564aac | ||
|
|
350d08a2b4 | ||
|
|
eb5e043357 | ||
|
|
d67e3e7244 | ||
|
|
a7196dd453 | ||
|
|
c83e54fae8 | ||
|
|
35a5761a7c | ||
|
|
b472fbdbbf | ||
|
|
4fcc96ac15 | ||
|
|
2dfcd3adad | ||
|
|
d79788258b | ||
|
|
427d2b69c7 | ||
|
|
7e5e4cc29b | ||
|
|
a1e89b882a | ||
|
|
a74f0181f1 | ||
|
|
a9a1e5450b | ||
|
|
5e65942060 | ||
|
|
d044d0ef50 | ||
|
|
d40a913ccc | ||
|
|
66178ccc18 | ||
|
|
8280ba2a64 | ||
|
|
8259020450 | ||
|
|
692a7a7258 | ||
|
|
1ee04ada14 | ||
|
|
193cb61b45 | ||
|
|
4777a34f23 | ||
|
|
4d3677ebd2 | ||
|
|
f0eb11010d | ||
|
|
e1f13b3872 | ||
|
|
fa30849d03 | ||
|
|
f8d592d811 | ||
|
|
c385aa1996 | ||
|
|
9a97eb3e8e | ||
|
|
70ee5300f7 | ||
|
|
35c9539c08 | ||
|
|
bc5b072ad8 | ||
|
|
66b8b064ba | ||
|
|
63643cc286 | ||
|
|
19bd44a7ca | ||
|
|
b3bdf25b5f | ||
|
|
cdd4fd4703 | ||
|
|
7d1b86acdb | ||
|
|
6e6c361eb8 | ||
|
|
62780c8889 | ||
|
|
1839f4cab0 | ||
|
|
c9f5b9e4a5 | ||
|
|
33365affa2 | ||
|
|
63db7ff6ad | ||
|
|
7f8b53edd4 | ||
|
|
eb0628ac3e | ||
|
|
cbb880a053 | ||
|
|
083a912e91 | ||
|
|
ab744b411d | ||
|
|
8947b4d05e | ||
|
|
7e3eaa6584 | ||
|
|
98a459c7d7 | ||
|
|
9d24a11c05 | ||
|
|
ee60787100 | ||
|
|
301a349776 | ||
|
|
81488ceb6b | ||
|
|
9e99fd61e7 | ||
|
|
d36c693615 | ||
|
|
20125a6f4a | ||
|
|
4bc5006c91 | ||
|
|
71769b71aa | ||
|
|
a2cf67d210 | ||
|
|
d9c4217fd3 | ||
|
|
719e36e46c | ||
|
|
bd052f4866 | ||
|
|
734e8540f2 |
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# 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,3 +0,0 @@
|
|||||||
|
|
||||||
VITE_SUPABASE_URL=https://your-project-id.supabase.co
|
|
||||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Deploy Backoffice
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'apps/backoffice/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
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 compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build
|
|
||||||
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Deploy Dimentorin
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'apps/dimentorin/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
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 compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build
|
|
||||||
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Deploy Gacha
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'apps/gacha/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
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 compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build
|
|
||||||
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Deploy Hackathon
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'apps/hackathon/**' # Auto deploy when changes pushed to apps/hackathon
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
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/apps/hackathon/.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-hackathon/docker-compose-hackathon.yml down || true
|
|
||||||
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml build
|
|
||||||
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Deploy Landing
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
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 compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build
|
|
||||||
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
name: Nix Build & Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['develop']
|
||||||
|
pull_request:
|
||||||
|
branches: ['develop']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
affected: ${{ steps.affected.outputs.apps }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Detect affected apps
|
||||||
|
id: affected
|
||||||
|
run: |
|
||||||
|
AFFECTED=$(npx nx show projects --affected --base=HEAD~1 --type=app 2>/dev/null | tr '\n' ' ')
|
||||||
|
echo "Affected apps: $AFFECTED"
|
||||||
|
APPS="[]"
|
||||||
|
for app in landing backoffice gacha dimentorin hackathon infra qrcampaign; do
|
||||||
|
if echo "$AFFECTED" | grep -qw "$app"; then
|
||||||
|
APPS=$(echo "$APPS" | jq -c ". + [\"$app\"]")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "apps=$APPS" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Matrix: $APPS"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: detect
|
||||||
|
if: needs.detect.outputs.affected != '[]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
app: ${{ fromJson(needs.detect.outputs.affected) }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup Cachix
|
||||||
|
uses: cachix/cachix-action@v15
|
||||||
|
with:
|
||||||
|
name: msdqn
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.app }}
|
||||||
|
run: nix build .#${{ matrix.app }} -o result-${{ matrix.app }}
|
||||||
|
|
||||||
|
- name: Push to Cachix
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||||
|
run: cachix push msdqn result-${{ matrix.app }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
env:
|
||||||
|
INFRA_DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${INFRA_DEPLOY_KEY}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
ssh-keyscan 167.235.70.37 >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Update infra flake.lock
|
||||||
|
run: |
|
||||||
|
export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes"
|
||||||
|
git clone git@github.com:IMPHNEN/imphnen-infrastructure.git /tmp/infra
|
||||||
|
cd /tmp/infra
|
||||||
|
nix flake update imphnen-frontend
|
||||||
|
if git diff --quiet flake.lock; then
|
||||||
|
echo "flake.lock unchanged, skipping"
|
||||||
|
else
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "chore: update imphnen-frontend-service to ${GITHUB_SHA::7}"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new root@167.235.70.37 \
|
||||||
|
'nixos-rebuild switch --flake github:IMPHNEN/imphnen-infrastructure#hetzner --refresh 2>&1 | tail -30'
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: rm -f ~/.ssh/deploy_key
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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
|
|
||||||
+15
-1
@@ -4,7 +4,7 @@
|
|||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
out-tsc
|
out-tsc
|
||||||
|
docs/FigmaImage/
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
@@ -66,3 +66,17 @@ out
|
|||||||
.github/instructions/nx.instructions.md
|
.github/instructions/nx.instructions.md
|
||||||
|
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
.direnv
|
||||||
|
result
|
||||||
|
|
||||||
|
.claude/worktrees
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Screenshots and Test Reports
|
||||||
|
**/screenshots
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# path to a directory with all packages
|
|
||||||
storage: ../tmp/local-registry/storage
|
|
||||||
|
|
||||||
# a list of other known repositories we can talk to
|
|
||||||
uplinks:
|
|
||||||
npmjs:
|
|
||||||
url: https://registry.npmjs.org/
|
|
||||||
maxage: 60m
|
|
||||||
|
|
||||||
packages:
|
|
||||||
'**':
|
|
||||||
# give all users (including non-authenticated users) full access
|
|
||||||
# because it is a local registry
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
unpublish: $all
|
|
||||||
|
|
||||||
# if package is not available locally, proxy requests to npm registry
|
|
||||||
proxy: npmjs
|
|
||||||
|
|
||||||
# log settings
|
|
||||||
log:
|
|
||||||
type: stdout
|
|
||||||
format: pretty
|
|
||||||
level: warn
|
|
||||||
|
|
||||||
publish:
|
|
||||||
allow_offline: true # set offline to true to allow publish offline
|
|
||||||
Vendored
-8
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"nrwl.angular-console",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"ms-playwright.playwright"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# IMPHNEN Frontend Service
|
||||||
|
|
||||||
|
Nx monorepo for IMPHNEN (Ingin Menjadi Programmer Handal Namun Enggan Ngoding) — Indonesia's largest programmer community.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Monorepo**: Nx 22.6
|
||||||
|
- **Framework**: Next.js 16 (landing), Vite + React 19 (all other apps)
|
||||||
|
- **Routing**: react-router v7 (Vite apps), Next.js App Router (landing)
|
||||||
|
- **Styling**: Tailwind CSS v4, class-variance-authority (CVA)
|
||||||
|
- **State**: Zustand, TanStack React Query
|
||||||
|
- **Forms**: react-hook-form + zod
|
||||||
|
- **Build/Deploy**: Nix flakes, Cachix binary cache, NixOS modules
|
||||||
|
- **CI/CD**: GitHub Actions with `nx affected` + matrix strategy
|
||||||
|
- **Node**: v22
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Apps
|
||||||
|
| App | Framework | Description |
|
||||||
|
|-----|-----------|-------------|
|
||||||
|
| `landing` | Next.js 16 | Public website |
|
||||||
|
| `backoffice` | Vite + React | Admin dashboard |
|
||||||
|
| `hackathon` | Vite + React | Hackathon platform |
|
||||||
|
| `dimentorin` | Vite + React | Mentoring platform |
|
||||||
|
| `gacha` | Vite + React | Merch gacha system |
|
||||||
|
| `qrcampaign` | Vite + React | QR campaign tool |
|
||||||
|
| `infra` | Vite + React | Infrastructure dashboard |
|
||||||
|
|
||||||
|
### Shared Libraries
|
||||||
|
| Lib | Purpose | Depends on |
|
||||||
|
|-----|---------|------------|
|
||||||
|
| `utils` | Pure utilities only: `cn`, `For`, `Show`, `useQueryState`, `useModalLogin`, react-query helpers, react-router file-based routing | nothing |
|
||||||
|
| `service` | Business logic: API clients, auth hooks, storage (SessionToken/SessionUser), constants (PERMISSIONS, cities) | `utils` |
|
||||||
|
| `ui` | UI components: atoms (Button, Input, Card, Dialog, Drawer, Form, Label), molecules, organisms (Navbar, Sidebar, Datatable) | `utils`, `service` |
|
||||||
|
|
||||||
|
**Dependency rule**: `ui` → `service` → `utils` (never the reverse)
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
nx dev <app> # Start dev server
|
||||||
|
nx build <app> # Build single app
|
||||||
|
nx run-many -t build --all # Build everything
|
||||||
|
|
||||||
|
# Test
|
||||||
|
nx test <lib> # Run unit tests (vitest)
|
||||||
|
nx e2e <app>-e2e # Run e2e tests (playwright)
|
||||||
|
nx lint <project> # Lint
|
||||||
|
|
||||||
|
# Build all affected
|
||||||
|
nx affected -t build # Only build what changed
|
||||||
|
nx affected -t test # Only test what changed
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
nix build .#<app> # Build Nix package for an app
|
||||||
|
nix develop # Enter dev shell (node 22, bun, git, jq)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nix / Deployment
|
||||||
|
|
||||||
|
All Nix config is in a single `flake.nix`:
|
||||||
|
- `mkViteApp`: Builds Vite apps as Nix packages
|
||||||
|
- `mkLandingApp`: Builds the Next.js landing app
|
||||||
|
- `mkLandingModule`: NixOS module (systemd service for Next.js)
|
||||||
|
- `mkStaticAppModule`: NixOS module (nginx for static Vite apps)
|
||||||
|
- `npmDepsHash`: Must be updated when `package-lock.json` changes. Use `lib.fakeHash` to get the new hash from a failed build.
|
||||||
|
|
||||||
|
## CI/CD Pipeline (.github/workflows/nix-build.yml)
|
||||||
|
|
||||||
|
1. **detect**: Uses `nx affected` to find changed apps
|
||||||
|
2. **build**: Matrix strategy builds only affected apps with Nix, pushes to Cachix
|
||||||
|
3. **deploy**: Clones `imphnen-infrastructure`, updates `flake.lock`, pushes, then runs `clan machines update hetzner` to deploy to the Hetzner server
|
||||||
|
|
||||||
|
Required GitHub secrets: `CACHIX_AUTH_TOKEN`, `INFRA_DEPLOY_KEY` (SSH key for both GitHub and server access)
|
||||||
|
|
||||||
|
The server (167.235.70.37) pulls pre-built packages from Cachix during `nixos-rebuild`.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- Vite apps: `VITE_API_URL`, `VITE_GITHUB_CLIENT_ID`
|
||||||
|
- Next.js (landing): `NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_GITHUB_CLIENT_ID`
|
||||||
|
- The `getBaseURL()` function in `libs/service/src/api/index.ts` handles both environments
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- Atomic design: atoms → molecules → organisms
|
||||||
|
- Components have their own folder with `component.tsx`, `index.ts`, `spec.tsx`, `stories.tsx`
|
||||||
|
- Use `cn()` from `@imphnen-frontend-service/utils` for className merging
|
||||||
|
- Use `'use client'` directive on any component using React hooks (for Next.js compatibility)
|
||||||
|
- Button variants via CVA: `primary`, `secondary`, `text`, `bordered`, `success`, `danger`
|
||||||
|
- Landing app uses `container` class — Tailwind v4 requires explicit `margin-inline: auto` (defined in globals.css)
|
||||||
@@ -4,135 +4,117 @@
|
|||||||
<img src="docs/logo.svg" alt="IMPHNEN">
|
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
Monorepo for all frontend services of [IMPHNEN](https://imphnen.dev) (Ingin Menjadi Programmer Handal Namun Enggan Ngoding) — Indonesia's largest programmer community.
|
||||||
|
|
||||||
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Gacha Website</a>.
|
## Apps
|
||||||
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
|
||||||
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
|
| App | Framework | URL |
|
||||||
|
|-----|-----------|-----|
|
||||||
|
| **Landing** | Next.js 16 | [imphnen.dev](https://imphnen.dev) |
|
||||||
|
| **Backoffice** | Vite + React | [backoffice.imphnen.dev](https://backoffice.imphnen.dev) |
|
||||||
|
| **Hackathon** | Vite + React | [hackathon.imphnen.dev](https://dimentorin.imphnen.dev) |
|
||||||
|
| **Dimentorin** | Vite + React | [dimentorin.imphnen.dev](https://dimentorin.imphnen.dev) |
|
||||||
|
| **Gacha** | Vite + React | [gacha.imphnen.dev](https://gacha.imphnen.dev) |
|
||||||
|
| **QR Campaign** | Vite + React | [qr.imphnen.dev](https://qr.imphnen.dev) |
|
||||||
|
| **Infra** | Vite + React | [infra.imphnen.dev](https://infra.imphnen.dev) |
|
||||||
|
|
||||||
|
## Shared Libraries
|
||||||
|
|
||||||
|
| Lib | Purpose |
|
||||||
|
|-----|---------|
|
||||||
|
| `utils` | Pure utilities — `cn()`, `For`, `Show`, `useQueryState`, `useModalLogin` |
|
||||||
|
| `service` | Business logic — API clients, auth hooks, storage, constants |
|
||||||
|
| `ui` | UI components — atoms, molecules, organisms (atomic design) |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 22
|
||||||
|
- [Nix](https://nixos.org/download/) (optional, for reproducible builds)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
1. Clone this repository:
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git
|
||||||
cd imphnen-frontend-service
|
cd imphnen-frontend-service
|
||||||
```
|
|
||||||
2. Install all dependencies:
|
|
||||||
```sh
|
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to run
|
Or with Nix:
|
||||||
|
|
||||||
### 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
|
```sh
|
||||||
cd apps/{appname}
|
nix develop # enters dev shell with node 22, bun, git, jq
|
||||||
cp .env.example .env
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` in the app directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp apps/<app>/.env.example apps/<app>/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
Use the following commands to run in development mode:
|
|
||||||
|
|
||||||
- **Gacha**:
|
|
||||||
```sh
|
```sh
|
||||||
npm run gacha:dev
|
nx dev <app> # e.g. nx dev landing, nx dev backoffice
|
||||||
```
|
|
||||||
- **Backoffice**:
|
|
||||||
```sh
|
|
||||||
npm run backoffice:dev
|
|
||||||
```
|
|
||||||
- **Dimentorin**:
|
|
||||||
```sh
|
|
||||||
npm run dimentorin:dev
|
|
||||||
```
|
|
||||||
- **Landing Page**:
|
|
||||||
```sh
|
|
||||||
npm run landing:dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
Use the following commands to build the applications:
|
|
||||||
|
|
||||||
- **Gacha**:
|
|
||||||
```sh
|
```sh
|
||||||
npm run gacha:build
|
nx build <app> # build single app
|
||||||
```
|
nx run-many -t build --all # build everything
|
||||||
- **Backoffice**:
|
nx affected -t build # build only what changed
|
||||||
```sh
|
|
||||||
npm run backoffice:build
|
|
||||||
```
|
|
||||||
- **Dimentorin**:
|
|
||||||
```sh
|
|
||||||
npm run dimentorin:build
|
|
||||||
```
|
|
||||||
- **Landing Page**:
|
|
||||||
```sh
|
|
||||||
npm run landing:build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production
|
### Nix Build
|
||||||
|
|
||||||
Use the following commands to run the applications in production mode:
|
```sh
|
||||||
|
nix build .#<app> # e.g. nix build .#landing, .#dimentorin
|
||||||
|
```
|
||||||
|
|
||||||
|
All Nix config lives in `flake.nix`. When `package-lock.json` changes, update `npmDepsHash` using `lib.fakeHash`.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
- **Gacha**:
|
|
||||||
```sh
|
```sh
|
||||||
npm run gacha:prod
|
nx test <project> # unit tests (vitest)
|
||||||
```
|
nx e2e <app>-e2e # e2e tests (playwright)
|
||||||
- **Backoffice**:
|
nx lint <project> # eslint
|
||||||
```sh
|
|
||||||
npm run backoffice:prod
|
|
||||||
```
|
|
||||||
- **Dimentorin**:
|
|
||||||
```sh
|
|
||||||
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:
|
|
||||||
|
|
||||||
- **Run Storybook**
|
|
||||||
|
|
||||||
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run ui:storybook
|
nx storybook ui # run storybook for ui lib
|
||||||
|
nx build-storybook ui # build static storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Run Unit Test**
|
## CI/CD
|
||||||
|
|
||||||
This command runs unit tests for the UI components to ensure they function as expected.
|
GitHub Actions pipeline (`.github/workflows/nix-build.yml`):
|
||||||
|
|
||||||
```sh
|
1. **detect** — uses `nx affected` to find changed apps
|
||||||
npm run ui:test
|
2. **build** — matrix strategy builds only affected apps with Nix, pushes to [Cachix](https://app.cachix.org/cache/msdqn)
|
||||||
```
|
3. **deploy** — updates `flake.lock` in [imphnen-infrastructure](https://github.com/IMPHNEN/imphnen-infrastructure) and deploys to the server using [clan](https://clan.lol)
|
||||||
|
|
||||||
- **Build Components**
|
## Tech Stack
|
||||||
|
|
||||||
This command generates a static build of Storybook, which can be deployed for sharing and documentation purposes.
|
- **Monorepo**: Nx 22.6
|
||||||
|
- **Frontend**: React 19, TypeScript
|
||||||
|
- **Styling**: Tailwind CSS v4, CVA
|
||||||
|
- **State**: Zustand, TanStack React Query
|
||||||
|
- **Forms**: react-hook-form + zod
|
||||||
|
- **Build**: Nix flakes, Cachix
|
||||||
|
- **CI**: GitHub Actions
|
||||||
|
|
||||||
```sh
|
## Contributing
|
||||||
npm run ui:build
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to contribute
|
1. Fork and clone the repository
|
||||||
|
2. Create a branch: `git checkout -b feat/feature-name`
|
||||||
|
3. Make changes, commit, and push
|
||||||
|
4. Open a pull request to the `develop` branch
|
||||||
|
|
||||||
1. Fork the repository and clone it locally.
|
Issues and feedback welcome via [GitHub Issues](https://github.com/IMPHNEN/imphnen-frontend-service/issues).
|
||||||
2. Create a new branch for a new feature or fix:
|
|
||||||
```sh
|
|
||||||
git checkout -b feat/feature-name
|
|
||||||
```
|
|
||||||
3. Make changes, commit, and push to your forked repository.
|
|
||||||
4. Create a pull request to this repository `develop` branch.
|
|
||||||
|
|
||||||
If you encounter any issues or problems, feel free to create a new Issue.
|
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Use this section to tell people about which versions of your project are
|
||||||
|
currently being supported with security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 5.1.x | :white_check_mark: |
|
||||||
|
| 5.0.x | :x: |
|
||||||
|
| 4.0.x | :white_check_mark: |
|
||||||
|
| < 4.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Use this section to tell people how to report a vulnerability.
|
||||||
|
|
||||||
|
Tell them where to go, how often they can expect to get an update on a
|
||||||
|
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||||
|
declined, etc.
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Deployment trigger
|
|
||||||
# Updated to deploy circular dependency fixes and auth hooks
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
VITE_API_URL=
|
VITE_API_URL=
|
||||||
|
VITE_GITHUB_CLIENT_ID=
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Backoffice</title>
|
<title>Backoffice</title>
|
||||||
@@ -7,10 +7,9 @@
|
|||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
||||||
<link rel="stylesheet" href="/src/styles.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"><div style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f9fafb"><div style="width:40px;height:40px;border:3px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite"></div></div><style>@keyframes spin{to{transform:rotate(360deg)}}</style></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
interface IModalEditAccount {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleEditAccount?: () => void;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalEditAccount = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
handleEditAccount,
|
|
||||||
}: IModalEditAccount) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleEditAccount={handleEditAccount}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const [fullName, setFullName] = useState('Ahmad Wiyana');
|
|
||||||
const [email, setEmail] = useState('fullname23@gmail.com');
|
|
||||||
const [phoneNumber, setPhoneNumber] = useState('081904423804');
|
|
||||||
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Edit Data Akun
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<InputField
|
|
||||||
label="Nama Lengkap"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nama Lengkap"
|
|
||||||
value={fullName}
|
|
||||||
onChange={(e) => setFullName(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Email"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Nomor Telepon"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nomor Telepon"
|
|
||||||
value={phoneNumber}
|
|
||||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Alamat"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Alamat"
|
|
||||||
value={address}
|
|
||||||
onChange={(e) => setAddress(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={nextStep}
|
|
||||||
>
|
|
||||||
Perbarui Data
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleEditAccount?: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Update Data
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin dengan
|
|
||||||
<br /> perubahan yang dilakukan?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
handleEditAccount && handleEditAccount();
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ModalEditAccount;
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
useReactTable,
|
|
||||||
RowSelectionState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalEditAccount from './_components/modal-edit-account';
|
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
interface Account {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data for demonstration
|
|
||||||
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
phone: '081904423804',
|
|
||||||
address: 'Jl. Pantai Cibaduyut Indah',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
step: currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
} = useQueryState('step', {
|
|
||||||
defaultValue: 1,
|
|
||||||
maxValue: 2,
|
|
||||||
minValue: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
|
|
||||||
const columns: ColumnDef<Account>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Lengkap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nomor Telp',
|
|
||||||
accessorKey: 'phone',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Alamat Pengiriman',
|
|
||||||
accessorKey: 'address',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalEditAccount(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Edit
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Data Akun</h1>
|
|
||||||
</header>
|
|
||||||
{/* Account Table Section */}
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
{/* Search and Filter */}
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap, email"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
disabled
|
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
{showFilter && (
|
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
|
||||||
<Filter onClose={() => setShowFilter(false)} options={[]} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Modal Edit Account */}
|
|
||||||
<ModalEditAccount
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalEditAccount}
|
|
||||||
onClose={() => setShowModalEditAccount(false)}
|
|
||||||
handleEditAccount={() => {
|
|
||||||
console.log('Account updated');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
UsergroupAddOutlined,
|
|
||||||
UsergroupDeleteOutlined,
|
|
||||||
UserSwitchOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import ModalAddItem from './_components/modal-add-item';
|
|
||||||
import ModalEditItem from './_components/modal-edit-item';
|
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
|
||||||
const [showModalEditItem, setShowModalEditItem] = useState(false);
|
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
step: currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
} = useQueryState('step', {
|
|
||||||
defaultValue: 1,
|
|
||||||
maxValue: 2,
|
|
||||||
minValue: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
|
||||||
<div className="w-full flex flex-col gap-[40px]">
|
|
||||||
<section>
|
|
||||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
|
||||||
Summary
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<UsergroupAddOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">Participants</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<ReloadOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">
|
|
||||||
Roll and Reroll
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<UserSwitchOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">Redeem</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">
|
|
||||||
Inactive Users
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-8">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h2 className="text-p2 font-medium text-primary-500">
|
|
||||||
Gacha Items
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="items-end gap-3"
|
|
||||||
onClick={() => setShowModalAddItem(true)}
|
|
||||||
>
|
|
||||||
<span>Tambah Item</span>
|
|
||||||
<PlusOutlined className="text-[16px]" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-p3 text-primary-500 font-medium">
|
|
||||||
Lanyard IMPHNEN
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
|
||||||
<span>Prize {item}</span>
|
|
||||||
<span>Quantity: 10</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start gap-2">
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="sm"
|
|
||||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
|
|
||||||
onClick={() => setShowModalEditItem(true)}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="sm"
|
|
||||||
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
|
||||||
onClick={() => setShowModalDeleteItem(true)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right-side illustration */}
|
|
||||||
<img
|
|
||||||
src="gacha.webp"
|
|
||||||
alt=""
|
|
||||||
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Modal Add Item */}
|
|
||||||
<ModalAddItem
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalAddItem}
|
|
||||||
onClose={() => setShowModalAddItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal Edit Item */}
|
|
||||||
<ModalEditItem
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalEditItem}
|
|
||||||
onClose={() => setShowModalEditItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal Delete Item */}
|
|
||||||
<ModalDeleteItem
|
|
||||||
isOpen={showModalDeleteItem}
|
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -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">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalAddPermission {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalAddPermission = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
}: IModalAddPermission) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleAddItem={handleAddItem}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500">
|
|
||||||
Tambah Permissions
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Nama Permission"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
|
||||||
Tambah Permission
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
{
|
|
||||||
success: 'Data permissions berhasil ditambahkan',
|
|
||||||
error: 'Data permissions gagal ditambahkan',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-7 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Permissions
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-center text-neutral-400">
|
|
||||||
Apakah kamu yakin ingin
|
|
||||||
<br /> menambahkan permission ini?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex mb-0 gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Tambahkan
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalAddPermission;
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalDeletePermission {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleDelete?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalDeletePermission = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleDelete,
|
|
||||||
}: IModalDeletePermission) => {
|
|
||||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
|
||||||
success: 'Data permissions berhasil dihapus',
|
|
||||||
error: 'Data permissions gagal dihapus',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Delete Permissions
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini
|
|
||||||
mungkin akan mempengaruhi fungsional sistem
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Batal Hapus
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Hapus Item
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalDeletePermission;
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
AuditOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
useReactTable,
|
|
||||||
RowSelectionState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalProcessDelivery from './_components/modal-process-item';
|
|
||||||
|
|
||||||
type OrderValid = 'valid' | 'invalid' | 'unchecked';
|
|
||||||
type Status = 'undelivered' | 'delivered';
|
|
||||||
|
|
||||||
interface Prize {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
orderValid: OrderValid;
|
|
||||||
items: string;
|
|
||||||
address: string;
|
|
||||||
status: Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
'Sertifikat + Laminating',
|
|
||||||
'Lanyard + ID Card',
|
|
||||||
'Pin',
|
|
||||||
'Sticker Isi 3',
|
|
||||||
'Sticker Isi 5',
|
|
||||||
'Gelang Karet',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock data for transactions
|
|
||||||
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: 'Nama Lengkap',
|
|
||||||
orderValid: (i % 3 === 0
|
|
||||||
? 'invalid'
|
|
||||||
: i % 5 === 0
|
|
||||||
? 'unchecked'
|
|
||||||
: 'valid') as OrderValid,
|
|
||||||
items: items[i % items.length],
|
|
||||||
address: 'Jl. Pantai Cibaduyut Indah',
|
|
||||||
status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalProcessDelivery, setShowModalProcessDelivery] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
|
|
||||||
const deliveryOptions = [
|
|
||||||
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
|
|
||||||
{ id: 'option1', value: 'delivered', label: 'Delivered' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns: ColumnDef<Prize>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Lengkap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Order Valid?',
|
|
||||||
accessorKey: 'orderValid',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.orderValid;
|
|
||||||
const statusColors: Record<OrderValid, string> = {
|
|
||||||
valid: 'bg-success-200 text-success-500',
|
|
||||||
invalid: 'bg-danger-200 text-danger-500',
|
|
||||||
unchecked: 'bg-warning-200 text-warning-900',
|
|
||||||
};
|
|
||||||
const statusText: Record<OrderValid, string> = {
|
|
||||||
valid: 'Valid',
|
|
||||||
invalid: 'Invalid',
|
|
||||||
unchecked: 'Unchecked',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Items',
|
|
||||||
accessorKey: 'items',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Alamat Pengiriman',
|
|
||||||
accessorKey: 'address',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<Status, string> = {
|
|
||||||
delivered: 'bg-success-200 text-success-500',
|
|
||||||
undelivered: 'bg-danger-200 text-danger-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<Status, string> = {
|
|
||||||
delivered: 'Delivered',
|
|
||||||
undelivered: 'Undelivered',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalProcessDelivery(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-full"
|
|
||||||
>
|
|
||||||
<AuditOutlined className="text-[16px]" /> Process
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
|
|
||||||
</header>
|
|
||||||
{/* Account Table Section */}
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
{/* Search and Filter */}
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
{showFilter && (
|
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
|
||||||
<Filter
|
|
||||||
options={deliveryOptions}
|
|
||||||
onClose={() => setShowFilter(false)}
|
|
||||||
onFilterChange={(value) => {
|
|
||||||
console.log('Selected filter:', value);
|
|
||||||
// Filter logic di sini
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Modal Process Delivery */}
|
|
||||||
<ModalProcessDelivery
|
|
||||||
isOpen={showModalProcessDelivery}
|
|
||||||
onClose={() => setShowModalProcessDelivery(false)}
|
|
||||||
handleProcessDelivery={() => {
|
|
||||||
console.log('Action ketika user menekan tombol Proses Pengiriman');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
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;
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
AuditOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
useReactTable,
|
|
||||||
RowSelectionState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalValidate from './_components/modal-validate';
|
|
||||||
|
|
||||||
type TransactionStatus = 'valid' | 'invalid' | 'unchecked';
|
|
||||||
|
|
||||||
interface Transaction {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
transactionNumber: string;
|
|
||||||
status: TransactionStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data for transactions
|
|
||||||
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
|
||||||
transactionNumber: '25D2133Y9AFYBD',
|
|
||||||
status: (i % 3 === 0
|
|
||||||
? 'invalid'
|
|
||||||
: i % 5 === 0
|
|
||||||
? 'unchecked'
|
|
||||||
: 'valid') as TransactionStatus,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalValidate, setShowModalValidate] = useState(false);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
|
|
||||||
const validationOptions = [
|
|
||||||
{ id: 'option1', value: 'unchecked', label: 'Unchecked' },
|
|
||||||
{ id: 'option2', value: 'valid', label: 'Valid' },
|
|
||||||
{ id: 'option3', value: 'invalid', label: 'Invalid' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns: ColumnDef<Transaction>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Lengkap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nomor Transaksi',
|
|
||||||
accessorKey: 'transactionNumber',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Order Valid?',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<TransactionStatus, string> = {
|
|
||||||
valid: 'bg-success-200 text-success-500',
|
|
||||||
invalid: 'bg-danger-200 text-danger-500',
|
|
||||||
unchecked: 'bg-warning-200 text-warning-900',
|
|
||||||
};
|
|
||||||
const statusText: Record<TransactionStatus, string> = {
|
|
||||||
valid: 'Valid',
|
|
||||||
invalid: 'Invalid',
|
|
||||||
unchecked: 'Unchecked',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalValidate(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-full"
|
|
||||||
>
|
|
||||||
<AuditOutlined className="text-[16px]" /> Update
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockTransactions,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockTransactions.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Account Table Section */}
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
{/* Search and Filter */}
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
{showFilter && (
|
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
|
||||||
<Filter
|
|
||||||
options={validationOptions}
|
|
||||||
onClose={() => setShowFilter(false)}
|
|
||||||
onFilterChange={(value) => {
|
|
||||||
console.log('Selected filter:', value);
|
|
||||||
// Filter logic di sini
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockTransactions} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<ModalValidate
|
|
||||||
isOpen={showModalValidate}
|
|
||||||
onClose={() => setShowModalValidate(false)}
|
|
||||||
handleValid={() => {
|
|
||||||
console.log('Action ketika user klik Valid');
|
|
||||||
}}
|
|
||||||
handleInvalid={() => {
|
|
||||||
console.log('Action ketika user klik Tidak Valid');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user