Compare commits
114
Commits
cert
...
d5b267332f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -1,24 +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)",
|
|
||||||
"Bash(npx nx build:*)",
|
|
||||||
"Bash(git add:*)",
|
|
||||||
"Bash(git commit:*)",
|
|
||||||
"Bash(git push)",
|
|
||||||
"Bash(findstr:*)",
|
|
||||||
"Bash(ls:*)"
|
|
||||||
],
|
|
||||||
"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,44 +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
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-vps
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy via SSH
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
port: ${{ secrets.SSH_PORT }}
|
|
||||||
command_timeout: 30m
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d ~/imphnen-frontend-service-backoffice ]; then
|
|
||||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-backoffice
|
|
||||||
else
|
|
||||||
cd ~/imphnen-frontend-service-backoffice && git fetch origin && git reset --hard origin/develop && git pull
|
|
||||||
fi
|
|
||||||
cat > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env << EOF
|
|
||||||
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
|
||||||
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
|
||||||
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
|
||||||
EOF
|
|
||||||
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml down || true
|
|
||||||
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build --progress=plain
|
|
||||||
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,44 +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
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-vps
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy via SSH
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
port: ${{ secrets.SSH_PORT }}
|
|
||||||
command_timeout: 30m
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d ~/imphnen-frontend-service-dimentorin ]; then
|
|
||||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-dimentorin
|
|
||||||
else
|
|
||||||
cd ~/imphnen-frontend-service-dimentorin && git fetch origin && git reset --hard origin/develop && git pull
|
|
||||||
fi
|
|
||||||
cat > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env << EOF
|
|
||||||
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
|
||||||
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
|
||||||
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
|
||||||
EOF
|
|
||||||
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml down || true
|
|
||||||
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build --progress=plain
|
|
||||||
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,44 +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
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-vps
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy via SSH
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
port: ${{ secrets.SSH_PORT }}
|
|
||||||
command_timeout: 30m
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d ~/imphnen-frontend-service-gacha ]; then
|
|
||||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-gacha
|
|
||||||
else
|
|
||||||
cd ~/imphnen-frontend-service-gacha && git fetch origin && git reset --hard origin/develop && git pull
|
|
||||||
fi
|
|
||||||
cat > ~/imphnen-frontend-service-gacha/apps/gacha/.env << EOF
|
|
||||||
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
|
||||||
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
|
||||||
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
|
||||||
EOF
|
|
||||||
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml down || true
|
|
||||||
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build --progress=plain
|
|
||||||
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
name: Deploy Hackathon
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'apps/hackathon/**' # Auto deploy when changes pushed to apps/hackathon
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-vps
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy via SSH
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
port: ${{ secrets.SSH_PORT }}
|
|
||||||
command_timeout: 30m
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d ~/imphnen-frontend-service-hackathon ]; then
|
|
||||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-hackathon
|
|
||||||
else
|
|
||||||
cd ~/imphnen-frontend-service-hackathon && git fetch origin && git reset --hard origin/develop && git pull
|
|
||||||
fi
|
|
||||||
cat > ~/imphnen-frontend-service-hackathon/.env.local << EOF
|
|
||||||
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
|
||||||
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
|
||||||
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
|
||||||
EOF
|
|
||||||
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml down || true
|
|
||||||
docker builder prune -af || true
|
|
||||||
DOCKER_BUILDKIT=1 docker compose --env-file ~/imphnen-frontend-service-hackathon/.env.local -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml build --progress=plain
|
|
||||||
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -1,44 +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
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-vps
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy via SSH
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
port: ${{ secrets.SSH_PORT }}
|
|
||||||
command_timeout: 30m
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
if [ ! -d ~/imphnen-frontend-service ]; then
|
|
||||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
|
|
||||||
else
|
|
||||||
cd ~/imphnen-frontend-service && git fetch origin && git reset --hard origin/develop && git pull
|
|
||||||
fi
|
|
||||||
cat > ~/imphnen-frontend-service/apps/landing/.env << EOF
|
|
||||||
NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
|
|
||||||
NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
|
||||||
EOF
|
|
||||||
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
|
|
||||||
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build --progress=plain
|
|
||||||
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
|
|
||||||
docker image prune -af
|
|
||||||
@@ -0,0 +1,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
|
|
||||||
@@ -66,3 +66,10 @@ out
|
|||||||
.github/instructions/nx.instructions.md
|
.github/instructions/nx.instructions.md
|
||||||
|
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
.direnv
|
||||||
|
result
|
||||||
|
|
||||||
|
.claude/worktrees
|
||||||
|
.claude/settings.local.json
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"nx-mcp": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["nx", "mcp"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,93 @@
|
|||||||
<!-- nx configuration start-->
|
# IMPHNEN Frontend Service
|
||||||
<!-- Leave the start & end comments to automatically receive updates. -->
|
|
||||||
|
|
||||||
# General Guidelines for working with Nx
|
Nx monorepo for IMPHNEN (Ingin Menjadi Programmer Handal Namun Enggan Ngoding) — Indonesia's largest programmer community.
|
||||||
|
|
||||||
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
|
## Tech Stack
|
||||||
- You have access to the Nx MCP server and its tools, use them to help the user
|
|
||||||
- When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable.
|
|
||||||
- When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies
|
|
||||||
- For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration
|
|
||||||
- If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors
|
|
||||||
|
|
||||||
<!-- nx configuration end-->
|
- **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 +0,0 @@
|
|||||||
Tue, Nov 25, 2025 4:21:27 PM
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Deployment trigger
|
|
||||||
# Updated to deploy circular dependency fixes and auth hooks
|
|
||||||
@@ -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,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,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,32 +0,0 @@
|
|||||||
import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { FC, ReactElement } from 'react';
|
|
||||||
|
|
||||||
export const HackathonDashboardPage: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
|
||||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">Dashboard</h1>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-5 gap-5">
|
|
||||||
{/* Participant */}
|
|
||||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
|
||||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
|
|
||||||
1261
|
|
||||||
</h3>
|
|
||||||
<p className="text-neutral-400 text-p3">Total Participants</p>
|
|
||||||
</div>
|
|
||||||
{/* Team */}
|
|
||||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
|
||||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">206</h3>
|
|
||||||
<p className="text-neutral-400 text-p3">Total Teams</p>
|
|
||||||
</div>
|
|
||||||
{/* Project Submitted */}
|
|
||||||
<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 Project Submitted</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HackathonDashboardPage;
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { FC, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
BackofficeWrapper,
|
|
||||||
DataTable,
|
|
||||||
} from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
RowSelectionState,
|
|
||||||
useReactTable,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
export const HackathonUsersPage: FC = (): ReactElement => {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockData: any[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
project_name: `Project ${i + 1}`,
|
|
||||||
repository_url: `https://github.com/user/repo${i + 1}`,
|
|
||||||
demo_url: `https://demo.example.com/project${i + 1}`,
|
|
||||||
presentation_url: `https://slides.example.com/project${i + 1}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
type UserStatus = 'active' | 'inactive';
|
|
||||||
|
|
||||||
interface SubmissionType {
|
|
||||||
id: number;
|
|
||||||
project_name: string;
|
|
||||||
repository_url: string;
|
|
||||||
demo_url: string;
|
|
||||||
presentation_url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: ColumnDef<SubmissionType>[] = [
|
|
||||||
{
|
|
||||||
header: 'Project Name',
|
|
||||||
accessorKey: 'project_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Repository URL',
|
|
||||||
accessorKey: 'repository_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Demo URL',
|
|
||||||
accessorKey: 'demo_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Presentation URL',
|
|
||||||
accessorKey: 'presentation_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn('w-72') },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
onClick={() => {
|
|
||||||
// View detail logic
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditOutlined className="text-base" /> View & Manage
|
|
||||||
</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="IMPHNEN x Kolosal.ai Hackathon 2025">
|
|
||||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">
|
|
||||||
Project Submission
|
|
||||||
</h1>
|
|
||||||
{/* Filters and actions */}
|
|
||||||
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6">
|
|
||||||
<div className="flex flex-wrap gap-3 items-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-64"
|
|
||||||
placeholder="Search name or email"
|
|
||||||
/>
|
|
||||||
<select className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-40">
|
|
||||||
<option value="all">All Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="suspended">Suspended</option>
|
|
||||||
</select>
|
|
||||||
<select className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-40">
|
|
||||||
<option value="all">All City</option>
|
|
||||||
<option value="jakarta">Jakarta</option>
|
|
||||||
<option value="bandung">Bandung</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Modals extracted into shared backoffice components */}
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HackathonUsersPage;
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import { FC, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
BackofficeWrapper,
|
|
||||||
DataTable,
|
|
||||||
} from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
RowSelectionState,
|
|
||||||
useReactTable,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import { useTeams } from '@imphnen-frontend-service/service';
|
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
export const HackathonTeamsPage: FC = (): ReactElement => {
|
|
||||||
const { data: teamsData } = useTeams();
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockData: TeamType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: `team-${i + 1}`,
|
|
||||||
name: `Team ${i + 1} - ${i % 3 === 0 ? 'Innovators' : 'Hackers'}`,
|
|
||||||
city: i % 2 === 0 ? 'Jakarta' : 'Bandung',
|
|
||||||
visibility: i % 4 === 0 ? 'private' : 'public',
|
|
||||||
member_count: Math.floor(Math.random() * 4) + 1,
|
|
||||||
has_submission: i % 3 !== 0,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
leader: {
|
|
||||||
user: {
|
|
||||||
fullname: `Leader User ${i}`,
|
|
||||||
email: `leader${i}@example.com`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface TeamType {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
city: string;
|
|
||||||
visibility: 'public' | 'private';
|
|
||||||
member_count: number;
|
|
||||||
has_submission: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
leader?: {
|
|
||||||
user: {
|
|
||||||
fullname: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: ColumnDef<TeamType>[] = [
|
|
||||||
{
|
|
||||||
accessorKey: 'id',
|
|
||||||
header: 'ID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'name',
|
|
||||||
header: 'Team Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'city',
|
|
||||||
header: 'City',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'visibility',
|
|
||||||
header: 'Visibility',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const isPublic = row.original.visibility === 'public';
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'py-2 px-4 text-sm rounded-2xl text-center',
|
|
||||||
isPublic
|
|
||||||
? 'bg-info-200 text-info-700'
|
|
||||||
: 'bg-gray-200 text-gray-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isPublic ? 'Public' : 'Private'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'member_count',
|
|
||||||
header: 'Members',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'leader',
|
|
||||||
header: 'Leader',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const leader = row.original.leader?.user;
|
|
||||||
return leader ? (
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-gray-900">
|
|
||||||
{leader.fullname}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">{leader.email}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400 italic">-</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'has_submission',
|
|
||||||
header: 'Submitted',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const hasSubmission = row.original.has_submission;
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'py-2 px-4 text-sm rounded-2xl text-center',
|
|
||||||
hasSubmission
|
|
||||||
? 'bg-success-200 text-success-700'
|
|
||||||
: 'bg-danger-200 text-danger-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{hasSubmission ? 'Yes' : 'No'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'updated_at',
|
|
||||||
header: 'Last Updated',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return new Date(row.original.updated_at).toLocaleDateString();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn('w-48') },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
onClick={() => {
|
|
||||||
// View detail logic
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditOutlined className="text-base" /> View & Manage
|
|
||||||
</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="IMPHNEN x Kolosal.ai Hackathon 2025">
|
|
||||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">
|
|
||||||
Team Management
|
|
||||||
</h1>
|
|
||||||
{/* Filters and actions */}
|
|
||||||
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6">
|
|
||||||
<div className="flex flex-wrap gap-3 items-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-64"
|
|
||||||
placeholder="Search name or email"
|
|
||||||
/>
|
|
||||||
<select className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-40">
|
|
||||||
<option value="all">All Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="suspended">Suspended</option>
|
|
||||||
</select>
|
|
||||||
<select className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-40">
|
|
||||||
<option value="all">All City</option>
|
|
||||||
<option value="jakarta">Jakarta</option>
|
|
||||||
<option value="bandung">Bandung</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
{/* Modals extracted into shared backoffice components */}
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HackathonTeamsPage;
|
|
||||||
@@ -1,476 +0,0 @@
|
|||||||
import { FC, ReactElement, useState, useMemo, useCallback } from 'react';
|
|
||||||
import ModalUserDetail from './_components/modal-user-detail';
|
|
||||||
import {
|
|
||||||
BackofficeWrapper,
|
|
||||||
DataTable,
|
|
||||||
} from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import {
|
|
||||||
EditOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
FilterOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
// Removed unused SearchOutlined icon after schema revision
|
|
||||||
|
|
||||||
// Define interface outside component
|
|
||||||
interface UserType {
|
|
||||||
id: string; // UUID
|
|
||||||
avatar?: string;
|
|
||||||
fullname: string;
|
|
||||||
bio?: string;
|
|
||||||
location: string;
|
|
||||||
is_active: boolean; // admin can deactivate
|
|
||||||
skills: string[]; // Frontend Developer, Backend Developer, etc.
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move mock data outside component to prevent recreation
|
|
||||||
const skillsOptions = [
|
|
||||||
'Frontend Developer',
|
|
||||||
'Backend Developer',
|
|
||||||
'Full Stack Developer',
|
|
||||||
'DevOps Engineer',
|
|
||||||
'UI/UX Designer',
|
|
||||||
'Product Manager',
|
|
||||||
'Data Scientist',
|
|
||||||
'Mobile Developer',
|
|
||||||
];
|
|
||||||
|
|
||||||
const locations = ['Jakarta', 'Bandung', 'Surabaya', 'Medan', 'Yogyakarta'];
|
|
||||||
const bios = [
|
|
||||||
'Passionate developer with 5+ years experience',
|
|
||||||
'Tech enthusiast and problem solver',
|
|
||||||
'Building scalable solutions for modern problems',
|
|
||||||
'Creative designer with technical background',
|
|
||||||
'Data-driven decision maker',
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockData: UserType[] = Array.from({ length: 50 }, (_, i) => {
|
|
||||||
const randomSkillsCount = Math.floor(Math.random() * 3) + 1; // 1-3 skills
|
|
||||||
const randomSkills = skillsOptions
|
|
||||||
.sort(() => 0.5 - Math.random())
|
|
||||||
.slice(0, randomSkillsCount);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `24db9e4d-ca4c-46aa-ac36-8ef04bbe01${String(i).padStart(2, '0')}`,
|
|
||||||
avatar:
|
|
||||||
i % 4 === 0
|
|
||||||
? `https://ui-avatars.com/api/?name=${encodeURIComponent(
|
|
||||||
i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana'
|
|
||||||
)}&background=random`
|
|
||||||
: undefined,
|
|
||||||
fullname:
|
|
||||||
i % 3 === 0
|
|
||||||
? 'Ahmad Wijuana'
|
|
||||||
: i % 3 === 1
|
|
||||||
? 'Sofia Wijuana'
|
|
||||||
: 'Budi Santoso',
|
|
||||||
bio: i % 4 === 0 ? bios[i % bios.length] : undefined,
|
|
||||||
location: locations[i % locations.length],
|
|
||||||
is_active: i % 7 !== 0, // More realistic distribution
|
|
||||||
skills: randomSkills,
|
|
||||||
created_at: new Date(
|
|
||||||
Date.now() - i * 86400000 * (Math.random() * 30 + 1)
|
|
||||||
).toISOString(), // Random within last 30-60 days
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const HackathonUsersPage: FC = (): ReactElement => {
|
|
||||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
|
||||||
const [showNewUserModal, setShowNewUserModal] = useState(false);
|
|
||||||
const [selectedUser, setSelectedUser] = useState<UserType | null>(null);
|
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
|
||||||
|
|
||||||
// Advanced filtering states
|
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
|
||||||
const [locationFilter, setLocationFilter] = useState('all');
|
|
||||||
const [skillsFilter, setSkillsFilter] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const pageSize = 10;
|
|
||||||
|
|
||||||
// Memoize the callback to prevent recreation
|
|
||||||
const handleShowDetailModal = useCallback((user: UserType) => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
setShowDetailModal(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseDetailModal = useCallback(() => {
|
|
||||||
setShowDetailModal(false);
|
|
||||||
setSelectedUser(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShowNewUserModal = useCallback(() => {
|
|
||||||
setShowNewUserModal(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseNewUserModal = useCallback(() => {
|
|
||||||
setShowNewUserModal(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Filter data based on current filter states
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return mockData.filter((user) => {
|
|
||||||
// Status filter
|
|
||||||
if (statusFilter !== 'all') {
|
|
||||||
const isActive = statusFilter === 'active';
|
|
||||||
if (user.is_active !== isActive) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Location filter
|
|
||||||
if (locationFilter !== 'all' && user.location !== locationFilter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skills filter
|
|
||||||
if (skillsFilter.length > 0) {
|
|
||||||
const hasMatchingSkill = skillsFilter.some((skill) =>
|
|
||||||
user.skills.includes(skill)
|
|
||||||
);
|
|
||||||
if (!hasMatchingSkill) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}, [statusFilter, locationFilter, skillsFilter]);
|
|
||||||
|
|
||||||
// Memoize columns to prevent recreation on every render
|
|
||||||
const columns: ColumnDef<UserType>[] = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
accessorKey: 'fullname',
|
|
||||||
header: 'User',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Avatar */}
|
|
||||||
<div className="w-10 h-10 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden shrink-0">
|
|
||||||
{row.original.avatar ? (
|
|
||||||
<img
|
|
||||||
src={row.original.avatar}
|
|
||||||
alt={row.original.fullname}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<UserOutlined className="text-neutral-500 text-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Name only */}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="font-medium text-neutral-900 truncate">
|
|
||||||
{row.original.fullname}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'skills',
|
|
||||||
header: 'Skills',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
|
||||||
{row.original.skills.slice(0, 2).map((skill, index) => (
|
|
||||||
<span
|
|
||||||
key={index}
|
|
||||||
className="inline-flex items-center px-2 py-1 rounded-2xl text-xs font-medium bg-success-100 text-success-800"
|
|
||||||
>
|
|
||||||
{skill.replace(' Developer', '').replace(' Engineer', '')}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{row.original.skills.length > 2 && (
|
|
||||||
<span className="inline-flex items-center px-2 py-1 rounded-2xl text-xs font-medium bg-success-200 text-success-700">
|
|
||||||
+{row.original.skills.length - 2}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'location',
|
|
||||||
header: 'Location',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-neutral-700">{row.original.location}</span>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'is_active',
|
|
||||||
header: 'Status',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-2 h-2 rounded-full',
|
|
||||||
row.original.is_active ? 'bg-success-500' : 'bg-neutral-400'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
row.original.is_active ? 'text-success-700' : 'text-neutral-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{row.original.is_active ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: (rowA, rowB) => {
|
|
||||||
const aActive = rowA.original.is_active;
|
|
||||||
const bActive = rowB.original.is_active;
|
|
||||||
if (aActive && !bActive) return -1;
|
|
||||||
if (!aActive && bActive) return 1;
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
header: 'Joined',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-neutral-900 text-sm">
|
|
||||||
{new Date(row.original.created_at).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: 'datetime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: 'Actions',
|
|
||||||
meta: { cellClassName: cn('w-48') },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2 text-sm px-4 py-2"
|
|
||||||
onClick={() => handleShowDetailModal(row.original)}
|
|
||||||
>
|
|
||||||
<EditOutlined className="text-sm" />
|
|
||||||
Manage
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="text-sm px-4 py-2"
|
|
||||||
onClick={() => {
|
|
||||||
// Toggle user status - implement later
|
|
||||||
console.log(`Toggle status for ${row.original.fullname}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.original.is_active ? 'Deactivate' : 'Activate'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[handleShowDetailModal]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
|
||||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">
|
|
||||||
User Management
|
|
||||||
</h1>
|
|
||||||
{/* Filters and actions */}
|
|
||||||
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6">
|
|
||||||
<div className="flex flex-wrap gap-3 items-center justify-between">
|
|
||||||
{/* Left side - Search & filters */}
|
|
||||||
<div className="flex flex-wrap gap-3 items-center">
|
|
||||||
{/* Search bar */}
|
|
||||||
<div className="relative">
|
|
||||||
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="border border-neutral-200 rounded-lg pl-10 pr-4 py-2.5 text-sm w-full sm:w-80 focus:border-primary-500 focus:outline-none"
|
|
||||||
placeholder="Search users by name or location..."
|
|
||||||
value={globalFilter}
|
|
||||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Filter */}
|
|
||||||
<div className="relative">
|
|
||||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
|
||||||
<select
|
|
||||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-36 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Location Filter */}
|
|
||||||
<div className="relative">
|
|
||||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
|
||||||
<select
|
|
||||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-44 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
|
||||||
value={locationFilter}
|
|
||||||
onChange={(e) => setLocationFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Locations</option>
|
|
||||||
{locations.map((location) => (
|
|
||||||
<option key={location} value={location}>
|
|
||||||
{location}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Skills Filter with Icon */}
|
|
||||||
<div className="relative">
|
|
||||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
|
||||||
<select
|
|
||||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-44 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
|
||||||
value=""
|
|
||||||
onChange={(e) => {
|
|
||||||
if (
|
|
||||||
e.target.value &&
|
|
||||||
!skillsFilter.includes(e.target.value)
|
|
||||||
) {
|
|
||||||
setSkillsFilter((prev) => [...prev, e.target.value]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">Add Skill Filter</option>
|
|
||||||
{skillsOptions.map((skill) => (
|
|
||||||
<option
|
|
||||||
key={skill}
|
|
||||||
value={skill}
|
|
||||||
disabled={skillsFilter.includes(skill)}
|
|
||||||
>
|
|
||||||
{skill}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - Add User Button */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-2 px-4 py-2"
|
|
||||||
onClick={handleShowNewUserModal}
|
|
||||||
>
|
|
||||||
<PlusOutlined className="text-sm" />
|
|
||||||
Add User
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active filters display */}
|
|
||||||
{(skillsFilter.length > 0 ||
|
|
||||||
statusFilter !== 'all' ||
|
|
||||||
locationFilter !== 'all') && (
|
|
||||||
<div className="flex flex-wrap gap-2 items-center">
|
|
||||||
<span className="text-sm text-neutral-600">Active filters:</span>
|
|
||||||
|
|
||||||
{/* Status filter badge */}
|
|
||||||
{statusFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
|
|
||||||
Status: {statusFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setStatusFilter('all')}
|
|
||||||
className="text-info-600 hover:text-info-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Location filter badge */}
|
|
||||||
{locationFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
|
|
||||||
Location: {locationFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setLocationFilter('all')}
|
|
||||||
className="text-green-600 hover:text-green-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Skills filter badges */}
|
|
||||||
{skillsFilter.map((skill) => (
|
|
||||||
<span
|
|
||||||
key={skill}
|
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-800 rounded-2xl text-sm"
|
|
||||||
>
|
|
||||||
{skill.replace(' Developer', '').replace(' Engineer', '')}
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setSkillsFilter((prev) => prev.filter((s) => s !== skill))
|
|
||||||
}
|
|
||||||
className="text-purple-600 hover:text-purple-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Clear all filters */}
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setStatusFilter('all');
|
|
||||||
setLocationFilter('all');
|
|
||||||
setSkillsFilter([]);
|
|
||||||
setGlobalFilter('');
|
|
||||||
}}
|
|
||||||
className="text-sm text-neutral-600"
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pagination-aware results display */}
|
|
||||||
{filteredData.length > 0 && (
|
|
||||||
<div className="text-sm text-neutral-600">
|
|
||||||
Showing {Math.min(pageSize, filteredData.length)} of{' '}
|
|
||||||
{filteredData.length} users
|
|
||||||
{filteredData.length > pageSize}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={filteredData} columns={columns} pageSize={10} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Modals component */}
|
|
||||||
<ModalUserDetail
|
|
||||||
isOpen={showDetailModal}
|
|
||||||
onClose={handleCloseDetailModal}
|
|
||||||
user={selectedUser}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* New User Modal */}
|
|
||||||
<ModalUserDetail
|
|
||||||
isOpen={showNewUserModal}
|
|
||||||
onClose={handleCloseNewUserModal}
|
|
||||||
user={null} // null indicates creating new user
|
|
||||||
/>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HackathonUsersPage;
|
|
||||||
@@ -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;
|
|
||||||
-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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
type ModalDeleteProps = {
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
hadnleDelete?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalDelete: FC<ModalDeleteProps> = ({ open, setOpen, hadnleDelete }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete item?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Hapus Akun
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus akun ini?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full border-danger-500 text-danger-500 hover:bg-danger-50 hover:text-danger-600 hover:border-danger-600"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => hadnleDelete?.()}
|
|
||||||
>
|
|
||||||
Hapus Akun
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC, useState } from "react";
|
|
||||||
import { ModalDelete } from "../delete";
|
|
||||||
import { ModalProps } from "../type";
|
|
||||||
import { ModalSuspendOrBan } from "../suspend-or-ban";
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
|
||||||
|
|
||||||
export const AccountProfile: FC<ModalProps> = ({ setOpen }) => {
|
|
||||||
const [openDelete, setOpenDelete] = useState(false)
|
|
||||||
const [openSuspendOrBan, setOpenSuspendOrBan] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-x-8 mb-8">
|
|
||||||
<div className="size-[100px] rounded-full overflow-hidden">
|
|
||||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="mb-4">
|
|
||||||
Upload Foto
|
|
||||||
</Button>
|
|
||||||
<p className="text-neutral-400 font-medium">
|
|
||||||
Setidaknya rekomendasi ukuran 240x240 px. <br />
|
|
||||||
.jpg, .jpeg, .png diperbolehkan
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h1>
|
|
||||||
<div className="grid grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nama Depan</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Nama Depan" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nama Belakang</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Nama Belakang" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Email</label>
|
|
||||||
<Input type="email" className="min-w-full w-full" placeholder="Email" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nomor Telepon</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="+62 81234567890" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 className="mb-7 text-p2 font-semibold">Status Akun</h1>
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
className="border-danger-500 text-danger-500"
|
|
||||||
onClick={() => setOpenSuspendOrBan(true)}
|
|
||||||
>
|
|
||||||
Suspend/Ban
|
|
||||||
</Button>
|
|
||||||
<Button type="button" size="sm" variant="danger" onClick={() => setOpenDelete(true)}>
|
|
||||||
Delete Akun
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-x-5">
|
|
||||||
<Button type="button" variant="bordered" onClick={() => setOpen(false)}>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button" disabled>
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ModalDelete open={openDelete} setOpen={setOpenDelete} />
|
|
||||||
<ModalSuspendOrBan open={openSuspendOrBan} setOpen={setOpenSuspendOrBan} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
import { SearchOutlined } from "@ant-design/icons"
|
|
||||||
import { Input, Select } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { DataTable } from "@imphnen-frontend-service/ui/organisms"
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils"
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"
|
|
||||||
import { FC, useState } from "react"
|
|
||||||
import dayjs from "dayjs"
|
|
||||||
|
|
||||||
interface ActivityLogType {
|
|
||||||
id: number
|
|
||||||
date: string
|
|
||||||
menu: string
|
|
||||||
activity: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: ActivityLogType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
date: new Date().toISOString(),
|
|
||||||
menu: 'Mentoring',
|
|
||||||
activity: 'Mentoring Session',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const ActivityLog: FC = () => {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns: ColumnDef<ActivityLogType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-16") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'date',
|
|
||||||
header: 'Waktu',
|
|
||||||
accessorKey: 'date',
|
|
||||||
cell: (info) => dayjs(info.row.original.date).format('DD MMMM YYYY, HH:mm WIB'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'menu',
|
|
||||||
header: 'Menu',
|
|
||||||
accessorKey: 'menu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'activity',
|
|
||||||
header: 'Activity',
|
|
||||||
accessorKey: 'activity',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-8 shadow rounded-lg">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-9">
|
|
||||||
<div className="relative w-1/2">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari aktivitas"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/4">
|
|
||||||
<Input type="date" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Menu</option>
|
|
||||||
<option value="profle">Profile</option>
|
|
||||||
<option value="profle-2">Profile</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-89
@@ -1,89 +0,0 @@
|
|||||||
import { Icon } from "@iconify/react"
|
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { For } from "@imphnen-frontend-service/utils"
|
|
||||||
import { FC } from "react"
|
|
||||||
|
|
||||||
const SOCIAL_LINKS = [
|
|
||||||
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, url: "https://linkedin.com", label: "LinkedIn" },
|
|
||||||
{ icon: <Icon icon="mdi:github" className="text-2xl" />, url: "https://github.com", label: "Github" },
|
|
||||||
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, url: "https://facebook.com", label: "Facebook (Meta)" },
|
|
||||||
{ icon: <Icon icon="mdi:stack-overflow" className="text-2xl" />, url: "https://stackoverflow.com", label: "Stack Overflow" }
|
|
||||||
]
|
|
||||||
|
|
||||||
export const DetailProfile: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="shadow rounded-lg p-8 mb-7">
|
|
||||||
<div className="flex justify-between items-start mb-6">
|
|
||||||
<div className="flex items-center gap-x-6">
|
|
||||||
<div className="size-[54px] rounded-full overflow-hidden">
|
|
||||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-800">Muhammad Firdaus Oiwobo</h1>
|
|
||||||
<p className="text-p3 font-medium text-neutral-600">Mentor</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button type="button" variant="text" className="bg-primary-100">
|
|
||||||
Actively Seeking Job
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<For data={SOCIAL_LINKS}>
|
|
||||||
{({ icon, url, label }) => (
|
|
||||||
<a key={url} href={url} target="_blank" rel="noreferrer">
|
|
||||||
<Button type="button" size="sm" className="flex items-center gap-x-2">
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-7">
|
|
||||||
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h1>
|
|
||||||
<p className="text-p3 font-medium text-neutral-600">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placeLine truncated
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="px-8 py-10 rounded-lg shadow h-max">
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h1>
|
|
||||||
<div className="grid gap-6">
|
|
||||||
<div className="flex items-center gap-x-5">
|
|
||||||
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
|
||||||
<Icon icon="ic:outline-mail" className="text-3xl" />
|
|
||||||
</div>
|
|
||||||
<div className="text-p3">
|
|
||||||
<p className="text-neutral-800 font-semibold">rzalaxib23@gmail.com</p>
|
|
||||||
<p className="text-neutral-600 font-medium">Email Address</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-x-5">
|
|
||||||
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
|
||||||
<Icon icon="cil:phone" className="text-3xl" />
|
|
||||||
</div>
|
|
||||||
<div className="text-p3">
|
|
||||||
<p className="text-neutral-800 font-semibold">+62 888 8888 8888</p>
|
|
||||||
<p className="text-neutral-600 font-medium">Phone Number</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-x-5">
|
|
||||||
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
|
||||||
<Icon icon="ion:location-outline" className="text-3xl" />
|
|
||||||
</div>
|
|
||||||
<div className="text-p3">
|
|
||||||
<p className="text-neutral-800 font-semibold">Jl. Mergosari, Kec. Suryakencana, Banjaran</p>
|
|
||||||
<p className="text-neutral-600 font-medium">Location</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Loaded 50 of 628 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user