Compare commits
125
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d3e2470c8 | ||
|
|
48411d3a08 | ||
|
|
426b71f341 | ||
|
|
06c69cb3c5 | ||
|
|
81999aa044 | ||
|
|
d83a6d9ec7 | ||
|
|
63dfb7299f | ||
|
|
53079fe868 | ||
|
|
2944a2b98a | ||
|
|
79f087e2fc | ||
|
|
d5b267332f | ||
|
|
8cb7b592eb | ||
|
|
b2ed098067 | ||
|
|
8749e83dad | ||
|
|
00b0997a9b | ||
|
|
05bdac6ee0 | ||
|
|
5cbad2a996 | ||
|
|
e78002ff69 | ||
|
|
b48f653a97 | ||
|
|
63da701b1b | ||
|
|
7bef667e96 | ||
|
|
82d1fae3d5 | ||
|
|
49d8419c1c | ||
|
|
4240e8eb51 | ||
|
|
86d9e759a6 | ||
|
|
6b3faf2a21 | ||
|
|
bcfac70e2a | ||
|
|
cb32146daa | ||
|
|
b82b03a8a2 | ||
|
|
0e0ca9720a | ||
|
|
964dd009cc | ||
|
|
a7ec622925 | ||
|
|
931355e64e | ||
|
|
58fbfcd0d8 | ||
|
|
063587aca9 | ||
|
|
7576b2557d | ||
|
|
95bf09c2fa | ||
|
|
0ade3c3e50 | ||
|
|
e945da4b7f | ||
|
|
61b650bb25 | ||
|
|
e61c003e07 | ||
|
|
8fff0df753 | ||
|
|
054f16053d | ||
|
|
0193245551 | ||
|
|
7f5948724b | ||
|
|
2cba806cd5 | ||
|
|
414e4d19b8 | ||
|
|
7268403ca4 | ||
|
|
651fd65ccb | ||
|
|
4e9d110bb0 | ||
|
|
c6719b19e5 | ||
|
|
53ad40f3fd | ||
|
|
8d2aaf5188 | ||
|
|
3f27e8d622 | ||
|
|
629d609792 | ||
|
|
8439c5d5e0 | ||
|
|
1afd7c014c | ||
|
|
419335d6b3 | ||
|
|
aef8d59c9f | ||
|
|
e14f6d4ef7 | ||
|
|
aa3e935fee | ||
|
|
1100d6f417 | ||
|
|
54ebcf1a1e | ||
|
|
9dabbb625d | ||
|
|
3f4461c65c | ||
|
|
f68d97188c | ||
|
|
f882668e7f | ||
|
|
dab4fa4941 | ||
|
|
bbb1bae909 | ||
|
|
3de1748f12 | ||
|
|
a610e180ba | ||
|
|
ce8cb8188c | ||
|
|
3aaf022393 | ||
|
|
ebe27a3f05 | ||
|
|
f846a5a785 | ||
|
|
708cbde596 | ||
|
|
9d37cba65f | ||
|
|
8728d0abd7 | ||
|
|
1f4f4ddaa1 | ||
|
|
4fcc249ff4 | ||
|
|
c2bed98b06 | ||
|
|
ae1864c134 | ||
|
|
3f30fbe89c | ||
|
|
951db0f28a | ||
|
|
ad59d39f55 | ||
|
|
1283689d84 | ||
|
|
7d1370d7f2 | ||
|
|
ab0b56780f | ||
|
|
1a39ac37db | ||
|
|
e5dc784301 | ||
|
|
b2de22b0ef | ||
|
|
ad20eda182 | ||
|
|
da65aa4109 | ||
|
|
1f0798752c | ||
|
|
5f28b9b406 | ||
|
|
9e8fc7c07f | ||
|
|
ca51f8aca2 | ||
|
|
c086b1371c | ||
|
|
398350795d | ||
|
|
5be9ec1edf | ||
|
|
f77967a092 | ||
|
|
fe172cc128 | ||
|
|
38ee9a9549 | ||
|
|
3ad828e759 | ||
|
|
782cce820b | ||
|
|
f5882e3cdc | ||
|
|
afaaff4a7f | ||
|
|
da8b08cd73 | ||
|
|
1dbe704230 | ||
|
|
59c1bb0366 | ||
|
|
c518784810 | ||
|
|
0267de664b | ||
|
|
134ff14946 | ||
|
|
ed3b711c89 | ||
|
|
6b7b1c4d20 | ||
|
|
b4b19e02fa | ||
|
|
e61979cbb6 | ||
|
|
0c7887268c | ||
|
|
76933c0be7 | ||
|
|
abc4c26d5d | ||
|
|
a3d35716a6 | ||
|
|
ac22bd6cab | ||
|
|
27ed5605ce | ||
|
|
2c6389a67a | ||
|
|
a4c75770ae |
@@ -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
|
|
||||||
+15
-1
@@ -4,7 +4,7 @@
|
|||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
out-tsc
|
out-tsc
|
||||||
|
docs/FigmaImage/
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
@@ -66,3 +66,17 @@ out
|
|||||||
.github/instructions/nx.instructions.md
|
.github/instructions/nx.instructions.md
|
||||||
|
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
.direnv
|
||||||
|
result
|
||||||
|
|
||||||
|
.claude/worktrees
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Screenshots and Test Reports
|
||||||
|
**/screenshots
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache
|
||||||
@@ -1,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 +1,2 @@
|
|||||||
VITE_API_URL=
|
VITE_API_URL=
|
||||||
|
VITE_GITHUB_CLIENT_ID=
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Backoffice</title>
|
<title>Backoffice</title>
|
||||||
@@ -7,10 +7,9 @@
|
|||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
||||||
<link rel="stylesheet" href="/src/styles.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"><div style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f9fafb"><div style="width:40px;height:40px;border:3px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite"></div></div><style>@keyframes spin{to{transform:rotate(360deg)}}</style></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
interface IModalEditAccount {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleEditAccount?: () => void;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalEditAccount = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
handleEditAccount,
|
|
||||||
}: IModalEditAccount) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleEditAccount={handleEditAccount}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const [fullName, setFullName] = useState('Ahmad Wiyana');
|
|
||||||
const [email, setEmail] = useState('fullname23@gmail.com');
|
|
||||||
const [phoneNumber, setPhoneNumber] = useState('081904423804');
|
|
||||||
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Edit Data Akun
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<InputField
|
|
||||||
label="Nama Lengkap"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nama Lengkap"
|
|
||||||
value={fullName}
|
|
||||||
onChange={(e) => setFullName(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Email"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Nomor Telepon"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nomor Telepon"
|
|
||||||
value={phoneNumber}
|
|
||||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Alamat"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Alamat"
|
|
||||||
value={address}
|
|
||||||
onChange={(e) => setAddress(e.target.value)}
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={nextStep}
|
|
||||||
>
|
|
||||||
Perbarui Data
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleEditAccount?: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Update Data
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin dengan
|
|
||||||
<br /> perubahan yang dilakukan?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
handleEditAccount && handleEditAccount();
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ModalEditAccount;
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
useReactTable,
|
|
||||||
RowSelectionState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalEditAccount from './_components/modal-edit-account';
|
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
interface Account {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data for demonstration
|
|
||||||
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
phone: '081904423804',
|
|
||||||
address: 'Jl. Pantai Cibaduyut Indah',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
step: currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
} = useQueryState('step', {
|
|
||||||
defaultValue: 1,
|
|
||||||
maxValue: 2,
|
|
||||||
minValue: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
|
|
||||||
const columns: ColumnDef<Account>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Lengkap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nomor Telp',
|
|
||||||
accessorKey: 'phone',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Alamat Pengiriman',
|
|
||||||
accessorKey: 'address',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalEditAccount(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Edit
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Data Akun</h1>
|
|
||||||
</header>
|
|
||||||
{/* Account Table Section */}
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
{/* Search and Filter */}
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap, email"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
disabled
|
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
{showFilter && (
|
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
|
||||||
<Filter onClose={() => setShowFilter(false)} options={[]} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Modal Edit Account */}
|
|
||||||
<ModalEditAccount
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalEditAccount}
|
|
||||||
onClose={() => setShowModalEditAccount(false)}
|
|
||||||
handleEditAccount={() => {
|
|
||||||
console.log('Account updated');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ReactElement } from "react";
|
|
||||||
import { UserGrowthChart } from "./_components/chart/user-growth";
|
|
||||||
import { SessionStatusChart } from "./_components/chart/session-status";
|
|
||||||
|
|
||||||
const Overview = () => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
|
||||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
|
|
||||||
<p className="text-neutral-400 text-p3">Total Users</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
|
||||||
|
|
||||||
<div className="space-y-14">
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
|
||||||
Overview
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-5">
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => <Overview key={index} />}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
|
||||||
Trends & Analytics
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-x-5">
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg col-span-5">
|
|
||||||
<div className="flex items-center justify-between mb-7">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700">User Growth</h2>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<UserGrowthChart />
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg col-span-2">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-7">Session Status</h2>
|
|
||||||
<SessionStatusChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-5">
|
|
||||||
<div className="bg-white px-7 py-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top 5 Mentors</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-label1 bg-primary-50 text-left rounded-full">
|
|
||||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
|
||||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
|
||||||
<th className="font-medium py-4 px-5 rounded-r-lg">Avg Rating</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<tr key={index} className="shadow rounded-lg">
|
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
|
||||||
<td className="py-4 px-5">4.9</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top Booked Mentoring Topics</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
|
||||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
|
||||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
|
||||||
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<tr key={index} className="shadow rounded-lg">
|
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
|
||||||
<td className="py-4 px-5">1000</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
interface IModalAddItem {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalAddItem = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
}: IModalAddItem) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleAddItem={handleAddItem}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Item Gacha
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Nama Hadiah"
|
|
||||||
name="itemName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nama Hadiah"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Quantity"
|
|
||||||
name="quantity"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
placeholder="Masukkan Kuantitas Item"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Foto Barang"
|
|
||||||
type="file"
|
|
||||||
name="foto"
|
|
||||||
placeholder=".jpg, .jpeg, atau .png"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={nextStep}
|
|
||||||
>
|
|
||||||
Tambahkan Item
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
{
|
|
||||||
success: 'Item ditambahkan ke gacha item',
|
|
||||||
error: 'Item gagal ditambahkan ke gacha item',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Item
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin ingin
|
|
||||||
<br /> menambahkan item ini?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Tambahkan
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalAddItem;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
|
|
||||||
interface IModalDeleteItem {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleDeleteItem?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalDeleteItem = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
handleDeleteItem,
|
|
||||||
}: IModalDeleteItem) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete item?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Delete Item
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus item ini?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Batal Hapus
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
handleDeleteItem && handleDeleteItem();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Hapus Item
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalDeleteItem;
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
interface IModalEditItem {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleEditItem?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalEditItem = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleEditItem,
|
|
||||||
}: IModalEditItem) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleEditItem={handleEditItem}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const initialValues = {
|
|
||||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
|
||||||
quantity: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Edit Item Gacha
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Silakan mengubah detail dari item yang diperlukan
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Nama Hadiah"
|
|
||||||
type="text"
|
|
||||||
name="itemName"
|
|
||||||
placeholder="Masukkan Nama Hadiah"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Quantity"
|
|
||||||
type="number"
|
|
||||||
name="quantity"
|
|
||||||
placeholder="Masukkan Kuantitas Item"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Foto Barang"
|
|
||||||
type="file"
|
|
||||||
name="foto"
|
|
||||||
placeholder=".jpg, .jpeg, atau .png"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={nextStep}
|
|
||||||
>
|
|
||||||
Perbarui Item
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleEditItem?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleEditItem,
|
|
||||||
{
|
|
||||||
success: 'Perubahan item berhasil dilakukan',
|
|
||||||
error: 'Perubahan item gagal dilakukan',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Update Item
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin dengan
|
|
||||||
<br /> perubahan yang dilakukan?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalEditItem;
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
UsergroupAddOutlined,
|
|
||||||
UsergroupDeleteOutlined,
|
|
||||||
UserSwitchOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import ModalAddItem from './_components/modal-add-item';
|
|
||||||
import ModalEditItem from './_components/modal-edit-item';
|
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
|
||||||
const [showModalEditItem, setShowModalEditItem] = useState(false);
|
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
step: currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
} = useQueryState('step', {
|
|
||||||
defaultValue: 1,
|
|
||||||
maxValue: 2,
|
|
||||||
minValue: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
|
||||||
<div className="w-full flex flex-col gap-[40px]">
|
|
||||||
<section>
|
|
||||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
|
||||||
Summary
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<UsergroupAddOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">Participants</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<ReloadOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">
|
|
||||||
Roll and Reroll
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<UserSwitchOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">Redeem</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
|
||||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
|
||||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h3 className="text-p1 font-semibold">1000</h3>
|
|
||||||
<p className="text-label1 text-neutral-500">
|
|
||||||
Inactive Users
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-8">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h2 className="text-p2 font-medium text-primary-500">
|
|
||||||
Gacha Items
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="items-end gap-3"
|
|
||||||
onClick={() => setShowModalAddItem(true)}
|
|
||||||
>
|
|
||||||
<span>Tambah Item</span>
|
|
||||||
<PlusOutlined className="text-[16px]" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-p3 text-primary-500 font-medium">
|
|
||||||
Lanyard IMPHNEN
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
|
||||||
<span>Prize {item}</span>
|
|
||||||
<span>Quantity: 10</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start gap-2">
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="sm"
|
|
||||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
|
|
||||||
onClick={() => setShowModalEditItem(true)}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="sm"
|
|
||||||
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
|
||||||
onClick={() => setShowModalDeleteItem(true)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right-side illustration */}
|
|
||||||
<img
|
|
||||||
src="gacha.webp"
|
|
||||||
alt=""
|
|
||||||
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Modal Add Item */}
|
|
||||||
<ModalAddItem
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalAddItem}
|
|
||||||
onClose={() => setShowModalAddItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal Edit Item */}
|
|
||||||
<ModalEditItem
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalEditItem}
|
|
||||||
onClose={() => setShowModalEditItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal Delete Item */}
|
|
||||||
<ModalDeleteItem
|
|
||||||
isOpen={showModalDeleteItem}
|
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { ReactElement, useState } from "react"
|
|
||||||
|
|
||||||
const TABS = {
|
|
||||||
MENTORING: 'Mentoring',
|
|
||||||
PLATFORM: 'Platform'
|
|
||||||
} as const
|
|
||||||
type Tabs = typeof TABS[keyof typeof TABS]
|
|
||||||
|
|
||||||
type FeedbackStatus = 'done' | 'todo';
|
|
||||||
|
|
||||||
interface FeedbackType {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
rating: number
|
|
||||||
status: FeedbackStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
rating: 4.5,
|
|
||||||
status: i % 2 === 0 ? 'done' : 'todo',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<FeedbackType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rating',
|
|
||||||
header: 'Rating',
|
|
||||||
accessorKey: 'rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<FeedbackStatus, string> = {
|
|
||||||
done: 'bg-success-200 text-success-500',
|
|
||||||
todo: 'bg-primary-200 text-primary-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<FeedbackStatus, string> = {
|
|
||||||
done: 'Done',
|
|
||||||
todo: 'To Do',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-72") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<SearchOutlined className="text-[16px]" /> Lihat Feedback
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<div className="mb-8 flex justify-between items-center">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700">Feedback</h1>
|
|
||||||
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
|
||||||
<For data={Object.values(TABS)}>
|
|
||||||
{(tab) => (
|
|
||||||
<Button
|
|
||||||
key={tab}
|
|
||||||
variant="text"
|
|
||||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama mentor/mentee"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalAddItem {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalAddItem = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
}: IModalAddItem) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleAddItem={handleAddItem}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Item Roll Gacha
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Lengkapi detal di bawah ini, untuk menambahkan item gacha
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Pilih Item"
|
|
||||||
name="itemName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Pilih item yang dimasukkan ke roll"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Quantity"
|
|
||||||
name="quantity"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
placeholder="Masukkan Kuantitas Item"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Chance Rate"
|
|
||||||
name="chanceRate"
|
|
||||||
type="number"
|
|
||||||
value={0.1}
|
|
||||||
min={0.1}
|
|
||||||
step={0.1}
|
|
||||||
max={1}
|
|
||||||
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
|
||||||
Tambahkan Item
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
{
|
|
||||||
success: 'Item ditambahkan ke roll gacha',
|
|
||||||
error: 'Item gagal ditambahkan ke roll gacha',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah ke Roll Gacha
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin ingin
|
|
||||||
<br /> menambahkan item ini ke roll gacha?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Tambahkan
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalAddItem;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
|
|
||||||
interface IModalDeleteItem {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleDeleteItem?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalDeleteItem = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
handleDeleteItem,
|
|
||||||
}: IModalDeleteItem) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete item?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Delete Item
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus item ini?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Batal Hapus
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
handleDeleteItem && handleDeleteItem();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Hapus Item
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalDeleteItem;
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
interface IModalUpdateItem {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleUpdateItem?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalUpdateItem = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleUpdateItem,
|
|
||||||
}: IModalUpdateItem) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const initialValues = {
|
|
||||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
|
||||||
quantity: 10,
|
|
||||||
chanceRate: 0.1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Update Item Roll Gacha
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Pilih Item"
|
|
||||||
name="itemName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Pilih item yang dimasukkan ke roll"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Quantity"
|
|
||||||
name="quantity"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
placeholder="Masukkan Kuantitas Item"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Chance Rate"
|
|
||||||
name="chanceRate"
|
|
||||||
type="number"
|
|
||||||
value={0.1}
|
|
||||||
min={0.1}
|
|
||||||
step={0.1}
|
|
||||||
max={1}
|
|
||||||
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={nextStep}
|
|
||||||
>
|
|
||||||
Perbarui Item
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleUpdateItem?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleUpdateItem, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleUpdateItem,
|
|
||||||
{
|
|
||||||
success: 'Perubahan item roll berhasil dilakukan',
|
|
||||||
error: 'Perubahan item roll gagal dilakukan',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-0 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Update Item
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin dengan
|
|
||||||
<br /> perubahan yang dilakukan?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalUpdateItem;
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
useReactTable,
|
|
||||||
RowSelectionState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalAddItem from './_components/modal-add-item';
|
|
||||||
import ModalUpdateItem from './_components/modal-update-item';
|
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
interface GachaItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
chanceRate: number;
|
|
||||||
quantity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: 'Hoodie IMPHNEN Official 2025',
|
|
||||||
chanceRate: 0.1,
|
|
||||||
quantity: 10,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
|
||||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
step: currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
} = useQueryState('step', {
|
|
||||||
defaultValue: 1,
|
|
||||||
maxValue: 2,
|
|
||||||
minValue: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
const columns: ColumnDef<GachaItem>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Item',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Chance Rate',
|
|
||||||
accessorKey: 'chanceRate',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Quantity',
|
|
||||||
accessorKey: 'quantity',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: () => (
|
|
||||||
<div className="flex gap-[8px]">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalUpdateItem(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Update
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalDeleteItem(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined /> Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Gacha Roll</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama item"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex gap-3 text-nowrap"
|
|
||||||
onClick={() => {
|
|
||||||
setShowModalAddItem(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusOutlined />
|
|
||||||
Tambah Item
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<ModalAddItem
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalAddItem}
|
|
||||||
onClose={() => setShowModalAddItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
<ModalUpdateItem
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalUpdateItem}
|
|
||||||
onClose={() => setShowModalUpdateItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
<ModalDeleteItem
|
|
||||||
isOpen={showModalDeleteItem}
|
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
|
||||||
handleDeleteItem={() => {
|
|
||||||
console.log('Item deleted');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,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;
|
|
||||||
-822
@@ -1,822 +0,0 @@
|
|||||||
import { FC, useState, useEffect, useMemo, useRef } from 'react';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import TeamBannerPlaceholder from './team-banner-placeholder';
|
|
||||||
import { CityFilterSelect } from '../../../../components/city-filter-select';
|
|
||||||
import {
|
|
||||||
TeamOutlined,
|
|
||||||
CalendarOutlined,
|
|
||||||
SaveOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
ExclamationOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
EyeInvisibleOutlined,
|
|
||||||
CrownOutlined,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
CloseCircleOutlined,
|
|
||||||
CameraOutlined,
|
|
||||||
UploadOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
|
|
||||||
interface TeamMember {
|
|
||||||
id: string;
|
|
||||||
joined_at: string;
|
|
||||||
role: 'leader' | 'member';
|
|
||||||
status: 'pending' | 'accepted' | 'rejected';
|
|
||||||
team_id: string;
|
|
||||||
user: {
|
|
||||||
avatar?: string;
|
|
||||||
bio?: string;
|
|
||||||
created_at: string;
|
|
||||||
email: string;
|
|
||||||
fullname: string;
|
|
||||||
id: string;
|
|
||||||
is_active: boolean;
|
|
||||||
location: string;
|
|
||||||
phone_number?: string;
|
|
||||||
skills: string[];
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TeamType {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
city: string;
|
|
||||||
banner?: string;
|
|
||||||
logo?: string;
|
|
||||||
visibility: 'public' | 'private';
|
|
||||||
member_count: number;
|
|
||||||
has_submission: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
leader_id: string;
|
|
||||||
members: TeamMember[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
team: TeamType | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
|
||||||
const [formData, setFormData] = useState<TeamType | null>(null);
|
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
||||||
const [activeTab, setActiveTab] = useState<'details' | 'members'>('details');
|
|
||||||
const [showLogoMenu, setShowLogoMenu] = useState(false);
|
|
||||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const bannerInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// Initialize form data when modal opens
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
if (team) {
|
|
||||||
// Edit existing team
|
|
||||||
setFormData({ ...team });
|
|
||||||
} else {
|
|
||||||
// Create new team
|
|
||||||
setFormData({
|
|
||||||
id: '', // Will be generated by backend
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
city: '',
|
|
||||||
banner: undefined,
|
|
||||||
logo: undefined,
|
|
||||||
visibility: 'public',
|
|
||||||
member_count: 1,
|
|
||||||
has_submission: false,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
leader_id: '',
|
|
||||||
members: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isOpen, team]);
|
|
||||||
|
|
||||||
// Check if form has changes
|
|
||||||
const hasChanges = useMemo(() => {
|
|
||||||
if (!formData) return false;
|
|
||||||
if (!team) return true; // New team always has changes
|
|
||||||
return (
|
|
||||||
formData.name !== team.name ||
|
|
||||||
formData.description !== team.description ||
|
|
||||||
formData.city !== team.city ||
|
|
||||||
formData.visibility !== team.visibility ||
|
|
||||||
formData.logo !== team.logo ||
|
|
||||||
formData.banner !== team.banner
|
|
||||||
);
|
|
||||||
}, [formData, team]);
|
|
||||||
|
|
||||||
// Check if required fields are filled
|
|
||||||
const isFormValid = useMemo(() => {
|
|
||||||
if (!formData) return false;
|
|
||||||
return formData.name.trim() !== '' && formData.city.trim() !== '';
|
|
||||||
}, [formData]);
|
|
||||||
|
|
||||||
const canSave = hasChanges && isFormValid;
|
|
||||||
|
|
||||||
// Get leader and other members
|
|
||||||
const leader = team?.members.find((m) => m.role === 'leader');
|
|
||||||
const acceptedMembers =
|
|
||||||
team?.members.filter((m) => m.status === 'accepted') || [];
|
|
||||||
const pendingMembers =
|
|
||||||
team?.members.filter((m) => m.status === 'pending') || [];
|
|
||||||
|
|
||||||
if (!isOpen || !formData) return null;
|
|
||||||
|
|
||||||
const handleInputChange = (
|
|
||||||
field: keyof TeamType,
|
|
||||||
value: string | boolean | 'public' | 'private' | undefined
|
|
||||||
) => {
|
|
||||||
setFormData((prev) => (prev ? { ...prev, [field]: value } : null));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
if (!formData) return;
|
|
||||||
|
|
||||||
console.log('Saving team:', formData);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
if (!team) return;
|
|
||||||
|
|
||||||
console.log('Deleting team:', team.id);
|
|
||||||
setShowDeleteConfirm(false);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogoUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
alert('Please select an image file');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
alert('Image size must be less than 5MB');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const logoUrl = e.target?.result as string;
|
|
||||||
handleInputChange('logo', logoUrl);
|
|
||||||
setShowLogoMenu(false);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBannerUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
alert('Please select an image file');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
alert('Image size must be less than 5MB');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const bannerUrl = e.target?.result as string;
|
|
||||||
handleInputChange('banner', bannerUrl);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveLogo = () => {
|
|
||||||
handleInputChange('logo', undefined);
|
|
||||||
setShowLogoMenu(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveBanner = () => {
|
|
||||||
handleInputChange('banner', undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerLogoUpload = () => {
|
|
||||||
logoInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center">
|
|
||||||
<TeamOutlined className="text-primary-600 text-lg" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-neutral-900">
|
|
||||||
{team ? 'Team Details' : 'Create New Team'}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-neutral-500">
|
|
||||||
{team
|
|
||||||
? 'View and manage team information'
|
|
||||||
: 'Add a new team to the hackathon'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="p-2 hover:bg-neutral-100 rounded-lg transition-colors cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setShowLogoMenu(false);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseOutlined className="text-neutral-400 text-lg" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-6 space-y-6" onClick={() => setShowLogoMenu(false)}>
|
|
||||||
{/* Hidden File Inputs */}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={logoInputRef}
|
|
||||||
onChange={handleLogoUpload}
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={bannerInputRef}
|
|
||||||
onChange={handleBannerUpload}
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Interactive Banner Section */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Team Banner
|
|
||||||
<span className="text-xs text-neutral-500 ml-2">
|
|
||||||
(3:1 aspect ratio recommended)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative group">
|
|
||||||
<TeamBannerPlaceholder
|
|
||||||
banner={formData.banner}
|
|
||||||
teamName={formData.name || 'Team Name'}
|
|
||||||
className="rounded-lg border border-neutral-200 transition-all group-hover:border-primary-300"
|
|
||||||
/>
|
|
||||||
{/* Banner Action Buttons */}
|
|
||||||
<div className="absolute inset-0 bg-black/40 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
bannerInputRef.current?.click();
|
|
||||||
}}
|
|
||||||
className="bg-white/90 hover:bg-white text-neutral-700 border-transparent shadow-sm gap-2"
|
|
||||||
>
|
|
||||||
<UploadOutlined className="text-sm" />
|
|
||||||
{formData.banner ? 'Change Banner' : 'Add Banner'}
|
|
||||||
</Button>
|
|
||||||
{formData.banner && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveBanner();
|
|
||||||
}}
|
|
||||||
className="bg-white/90 hover:bg-white text-red-600 border-transparent shadow-sm hover:text-red-700 gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined className="text-sm" />
|
|
||||||
Delete Banner
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team Logo & Name Row */}
|
|
||||||
<div className="grid grid-cols-12 gap-4 items-start">
|
|
||||||
{/* Interactive Team Logo */}
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
|
||||||
Logo
|
|
||||||
</label>
|
|
||||||
<div className="relative group">
|
|
||||||
<div className="w-24 h-24 rounded-full bg-neutral-100 flex items-center justify-center overflow-hidden border border-neutral-200 group-hover:border-primary-300 transition-colors">
|
|
||||||
{formData.logo ? (
|
|
||||||
<img
|
|
||||||
src={formData.logo}
|
|
||||||
alt={formData.name || 'Team Logo'}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TeamOutlined className="text-neutral-400 text-xl" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Logo Hover Overlay - Full circle */}
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowLogoMenu(!showLogoMenu);
|
|
||||||
}}
|
|
||||||
className="absolute inset-0 bg-neutral-300/80 cursor-pointer rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center w-24 h-24"
|
|
||||||
>
|
|
||||||
<CameraOutlined className="text-white text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Logo Menu Dropdown */}
|
|
||||||
{showLogoMenu && (
|
|
||||||
<div className="absolute top-full left-0 mt-2 bg-white rounded-lg shadow-lg border border-neutral-200 py-2 min-w-[140px] z-10">
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
triggerLogoUpload();
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
<UploadOutlined className="text-sm" />
|
|
||||||
{formData.logo ? 'Change Logo' : 'Upload Logo'}
|
|
||||||
</button>
|
|
||||||
{formData.logo && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveLogo();
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
<DeleteOutlined className="text-sm" />
|
|
||||||
Remove Logo
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team Name */}
|
|
||||||
<div className="col-span-10 space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Team Name <span className="text-danger-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full border border-neutral-200 rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none"
|
|
||||||
placeholder="Enter team name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-neutral-500">
|
|
||||||
Click logo to upload or change team logo (circular format)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team Description */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className="w-full border border-neutral-200 rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none resize-none"
|
|
||||||
placeholder="Enter team description (optional)"
|
|
||||||
rows={3}
|
|
||||||
value={formData.description || ''}
|
|
||||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* City */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
City <span className="text-danger-500">*</span>
|
|
||||||
</label>
|
|
||||||
<CityFilterSelect
|
|
||||||
value={formData.city || 'all'}
|
|
||||||
onChange={(city) =>
|
|
||||||
handleInputChange('city', city === 'all' ? '' : city)
|
|
||||||
}
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Search cities..."
|
|
||||||
allOptionLabel="Select a city"
|
|
||||||
filterIcon={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Visibility */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Team Visibility
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="visibility"
|
|
||||||
value="public"
|
|
||||||
checked={formData.visibility === 'public'}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange('visibility', e.target.value as 'public')
|
|
||||||
}
|
|
||||||
className="text-primary-600"
|
|
||||||
/>
|
|
||||||
<EyeOutlined className="text-info-600" />
|
|
||||||
<span className="text-sm">Public</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="visibility"
|
|
||||||
value="private"
|
|
||||||
checked={formData.visibility === 'private'}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange('visibility', e.target.value as 'private')
|
|
||||||
}
|
|
||||||
className="text-primary-600"
|
|
||||||
/>
|
|
||||||
<EyeInvisibleOutlined className="text-neutral-600" />
|
|
||||||
<span className="text-sm">Private</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team Information (Read-only for existing teams) */}
|
|
||||||
{team && (
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Members
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<UserOutlined className="text-neutral-500" />
|
|
||||||
<span className="text-sm text-neutral-700">
|
|
||||||
{team.member_count} member
|
|
||||||
{team.member_count !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Submission Status
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-2 h-2 rounded-full',
|
|
||||||
team.has_submission ? 'bg-success-500' : 'bg-danger-500'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
team.has_submission
|
|
||||||
? 'text-success-700'
|
|
||||||
: 'text-danger-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{team.has_submission ? 'Submitted' : 'Not Submitted'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tabs for Details and Members */}
|
|
||||||
{team && (
|
|
||||||
<div>
|
|
||||||
<div className="flex border-b border-neutral-200">
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'px-4 py-2 text-sm font-medium border-b-2 transition-colors cursor-pointer',
|
|
||||||
activeTab === 'details'
|
|
||||||
? 'border-primary-500 text-primary-600'
|
|
||||||
: 'border-transparent text-neutral-500 hover:text-neutral-700'
|
|
||||||
)}
|
|
||||||
onClick={() => setActiveTab('details')}
|
|
||||||
>
|
|
||||||
Team Details
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'px-4 py-2 text-sm font-medium border-b-2 transition-colors cursor-pointer',
|
|
||||||
activeTab === 'members'
|
|
||||||
? 'border-primary-500 text-primary-600'
|
|
||||||
: 'border-transparent text-neutral-500 hover:text-neutral-700'
|
|
||||||
)}
|
|
||||||
onClick={() => setActiveTab('members')}
|
|
||||||
>
|
|
||||||
Members ({team.member_count})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
{activeTab === 'details' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Leader Information */}
|
|
||||||
{leader && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Team Leader
|
|
||||||
</label>
|
|
||||||
<div className="p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden">
|
|
||||||
{leader.user.avatar ? (
|
|
||||||
<img
|
|
||||||
src={leader.user.avatar}
|
|
||||||
alt={leader.user.fullname}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CrownOutlined className="text-yellow-600 text-sm" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-neutral-900">
|
|
||||||
{leader.user.fullname}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-neutral-500">
|
|
||||||
{leader.user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Timestamps */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Created
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<CalendarOutlined className="text-neutral-500" />
|
|
||||||
<span className="text-sm text-neutral-700">
|
|
||||||
{new Date(team.created_at).toLocaleDateString(
|
|
||||||
'en-US',
|
|
||||||
{
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Last Updated
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<CalendarOutlined className="text-neutral-500" />
|
|
||||||
<span className="text-sm text-neutral-700">
|
|
||||||
{new Date(team.updated_at).toLocaleDateString(
|
|
||||||
'en-US',
|
|
||||||
{
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'members' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Accepted Members */}
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-neutral-700 mb-3">
|
|
||||||
Active Members ({acceptedMembers.length})
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{acceptedMembers.map((member) => (
|
|
||||||
<div
|
|
||||||
key={member.id}
|
|
||||||
className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden">
|
|
||||||
{member.user.avatar ? (
|
|
||||||
<img
|
|
||||||
src={member.user.avatar}
|
|
||||||
alt={member.user.fullname}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<UserOutlined className="text-neutral-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="text-sm font-medium text-neutral-900">
|
|
||||||
{member.user.fullname}
|
|
||||||
</p>
|
|
||||||
{member.role === 'leader' && (
|
|
||||||
<CrownOutlined className="text-yellow-600 text-xs" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-neutral-500">
|
|
||||||
{member.user.email}
|
|
||||||
</p>
|
|
||||||
{member.user.skills.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
|
||||||
{member.user.skills
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((skill, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-1 py-0.5 bg-primary-100 text-primary-700 text-xs rounded"
|
|
||||||
>
|
|
||||||
{skill}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{member.user.skills.length > 2 && (
|
|
||||||
<span className="text-xs text-neutral-400">
|
|
||||||
+{member.user.skills.length - 2}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-neutral-500">
|
|
||||||
Joined{' '}
|
|
||||||
{new Date(member.joined_at).toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pending Members */}
|
|
||||||
{pendingMembers.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-neutral-700 mb-3">
|
|
||||||
Pending Members ({pendingMembers.length})
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{pendingMembers.map((member) => (
|
|
||||||
<div
|
|
||||||
key={member.id}
|
|
||||||
className="flex items-center gap-3 p-3 bg-orange-50 border border-orange-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden">
|
|
||||||
{member.user.avatar ? (
|
|
||||||
<img
|
|
||||||
src={member.user.avatar}
|
|
||||||
alt={member.user.fullname}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<UserOutlined className="text-neutral-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="text-sm font-medium text-neutral-900">
|
|
||||||
{member.user.fullname}
|
|
||||||
</p>
|
|
||||||
<ClockCircleOutlined className="text-orange-600 text-xs" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-neutral-500">
|
|
||||||
{member.user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
onClick={() =>
|
|
||||||
console.log('Accept member:', member.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckCircleOutlined className="text-xs" />
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
onClick={() =>
|
|
||||||
console.log('Reject member:', member.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CloseCircleOutlined className="text-xs" />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-between p-6 border-t border-neutral-200">
|
|
||||||
<div>
|
|
||||||
{team && (
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="md"
|
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
Delete Team
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="secondary" size="md" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={!canSave}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<SaveOutlined />
|
|
||||||
{team ? 'Save Changes' : 'Create Team'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
{showDeleteConfirm && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-60 p-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md">
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<div className="w-12 h-12 rounded-full bg-danger-100 flex items-center justify-center">
|
|
||||||
<ExclamationOutlined className="text-danger-600 text-xl" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-neutral-900">
|
|
||||||
Delete Team
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-neutral-500">
|
|
||||||
This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-neutral-700 mb-6">
|
|
||||||
Are you sure you want to delete "{team?.name}"? This will
|
|
||||||
permanently remove the team and all associated data.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 justify-end">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="md"
|
|
||||||
onClick={handleDelete}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
Delete Team
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalTeamDetail;
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import {
|
|
||||||
CloseOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
ProjectOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
|
|
||||||
interface SubmissionModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
teamId: string;
|
|
||||||
teamName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SubmissionModal: FC<SubmissionModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
teamId,
|
|
||||||
teamName,
|
|
||||||
}) => {
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
// Mock submission data
|
|
||||||
const mockSubmission = {
|
|
||||||
id: `submission-${teamId}`,
|
|
||||||
project_name: `${teamName} Project`,
|
|
||||||
repository_url: `https://github.com/${teamName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, '-')}/hackathon-project`,
|
|
||||||
demo_url: `https://${teamName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, '-')}.vercel.app`,
|
|
||||||
presentation_url: `https://docs.google.com/presentation/d/${teamId}/edit`,
|
|
||||||
submitted_at: new Date().toISOString(),
|
|
||||||
status: 'submitted',
|
|
||||||
description:
|
|
||||||
'An innovative solution built during the IMPHNEN x Kolosal.ai Hackathon 2025.',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-full bg-success-100 flex items-center justify-center">
|
|
||||||
<ProjectOutlined className="text-success-600 text-lg" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-neutral-900">
|
|
||||||
Project Submission
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-neutral-500">
|
|
||||||
{teamName} - Hackathon Submission Details
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="p-2 hover:bg-neutral-100 rounded-lg transition-colors cursor-pointer"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<CloseOutlined className="text-neutral-400 text-lg" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Submission Status */}
|
|
||||||
<div className="flex items-center gap-3 p-4 bg-success-50 border border-success-200 rounded-lg">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-success-500"></div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-success-800">
|
|
||||||
Submission Completed
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-success-600">
|
|
||||||
Submitted on{' '}
|
|
||||||
{new Date(mockSubmission.submitted_at).toLocaleDateString(
|
|
||||||
'en-UK',
|
|
||||||
{
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Project Information */}
|
|
||||||
<div className="grid gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Project Name
|
|
||||||
</label>
|
|
||||||
<p className="text-sm text-neutral-900 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
{mockSubmission.project_name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Project Description
|
|
||||||
</label>
|
|
||||||
<p className="text-sm text-neutral-900 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
{mockSubmission.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Links Section */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Repository
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<span className="text-sm text-neutral-700 flex-1 truncate">
|
|
||||||
{mockSubmission.repository_url}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="shrink-0"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(mockSubmission.repository_url, '_blank')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LinkOutlined className="text-xs" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Live Demo
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<span className="text-sm text-neutral-700 flex-1 truncate">
|
|
||||||
{mockSubmission.demo_url}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="shrink-0"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(mockSubmission.demo_url, '_blank')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LinkOutlined className="text-xs" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-neutral-700">
|
|
||||||
Presentation
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-neutral-50 rounded-lg">
|
|
||||||
<span className="text-sm text-neutral-700 flex-1 truncate">
|
|
||||||
{mockSubmission.presentation_url}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="shrink-0"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(mockSubmission.presentation_url, '_blank')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LinkOutlined className="text-xs" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Note */}
|
|
||||||
<div className="p-4 bg-info-50 border border-info-200 rounded-lg">
|
|
||||||
<p className="text-sm text-info-800">
|
|
||||||
<strong>Note:</strong> This is a submission preview. The team has
|
|
||||||
successfully submitted their project. You can review the
|
|
||||||
submission details and access the project links above.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-end p-6 border-t border-neutral-200">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="secondary" size="md" onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
onClick={() => {
|
|
||||||
// Navigate to hackathon-submissions page
|
|
||||||
console.log('Navigate to full submissions page');
|
|
||||||
// You can implement navigation here
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ProjectOutlined />
|
|
||||||
View All Submissions
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SubmissionModal;
|
|
||||||
@@ -1,684 +0,0 @@
|
|||||||
import { FC, ReactElement, useState, useMemo, useCallback } from 'react';
|
|
||||||
import ModalTeamDetail from './_components/modal-team-detail-new';
|
|
||||||
import SubmissionModal from './_components/submission-modal';
|
|
||||||
import { CityFilterSelect } from '../../../components/city-filter-select';
|
|
||||||
import INDONESIAN_CITIES from '../../../constants/cities';
|
|
||||||
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,
|
|
||||||
TeamOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
FilterOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
ArrowRightOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
|
|
||||||
// Define interface outside component
|
|
||||||
interface TeamMember {
|
|
||||||
id: string;
|
|
||||||
joined_at: string;
|
|
||||||
role: 'leader' | 'member';
|
|
||||||
status: 'pending' | 'accepted' | 'rejected';
|
|
||||||
team_id: string;
|
|
||||||
user: {
|
|
||||||
avatar?: string;
|
|
||||||
bio?: string;
|
|
||||||
created_at: string;
|
|
||||||
email: string;
|
|
||||||
fullname: string;
|
|
||||||
id: string;
|
|
||||||
is_active: boolean;
|
|
||||||
location: string;
|
|
||||||
phone_number?: string;
|
|
||||||
skills: string[];
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TeamType {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
city: string;
|
|
||||||
banner?: string;
|
|
||||||
logo?: string;
|
|
||||||
visibility: 'public' | 'private';
|
|
||||||
member_count: number;
|
|
||||||
has_submission: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
leader_id: string;
|
|
||||||
members: TeamMember[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move mock data outside component to prevent recreation
|
|
||||||
// Sample data for popular cities from the INDONESIAN_CITIES constant
|
|
||||||
const cities = INDONESIAN_CITIES.slice(0, 20); // Use first 20 cities for variety
|
|
||||||
const teamNames = [
|
|
||||||
'Innovators',
|
|
||||||
'Hackers',
|
|
||||||
'Builders',
|
|
||||||
'Creators',
|
|
||||||
'Pioneers',
|
|
||||||
'Developers',
|
|
||||||
'Engineers',
|
|
||||||
'Coders',
|
|
||||||
'Tech Stars',
|
|
||||||
'Digital Wizards',
|
|
||||||
];
|
|
||||||
|
|
||||||
const descriptions = [
|
|
||||||
'Building innovative solutions for modern problems with cutting-edge technology',
|
|
||||||
'Passionate developers creating the next generation of web applications',
|
|
||||||
'Focused on sustainable tech solutions that make a positive impact',
|
|
||||||
'Experienced team working on scalable fintech innovations',
|
|
||||||
'Creative minds developing user-centric mobile applications',
|
|
||||||
'Full-stack developers building comprehensive business solutions',
|
|
||||||
'AI enthusiasts creating intelligent automation tools',
|
|
||||||
'Open source advocates building community-driven platforms',
|
|
||||||
];
|
|
||||||
|
|
||||||
const skills = [
|
|
||||||
'Frontend Developer',
|
|
||||||
'Backend Developer',
|
|
||||||
'Full Stack Developer',
|
|
||||||
'DevOps Engineer',
|
|
||||||
'UI/UX Designer',
|
|
||||||
'Product Manager',
|
|
||||||
'Data Scientist',
|
|
||||||
'Mobile Developer',
|
|
||||||
];
|
|
||||||
|
|
||||||
const generateMembers = (
|
|
||||||
count: number,
|
|
||||||
teamId: string,
|
|
||||||
leaderId: string
|
|
||||||
): TeamMember[] => {
|
|
||||||
return Array.from({ length: count }, (_, i) => {
|
|
||||||
const isLeader = i === 0;
|
|
||||||
const memberId = isLeader ? leaderId : `user-${teamId}-${i}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `member-${teamId}-${i}`,
|
|
||||||
joined_at: new Date(
|
|
||||||
Date.now() - (count - i) * 86400000 * Math.random() * 5
|
|
||||||
).toISOString(),
|
|
||||||
role: isLeader ? 'leader' : 'member',
|
|
||||||
status: Math.random() > 0.8 ? 'pending' : 'accepted',
|
|
||||||
team_id: teamId,
|
|
||||||
user: {
|
|
||||||
id: memberId,
|
|
||||||
avatar:
|
|
||||||
Math.random() > 0.6
|
|
||||||
? `https://ui-avatars.com/api/?name=${encodeURIComponent(
|
|
||||||
`User ${i}`
|
|
||||||
)}`
|
|
||||||
: undefined,
|
|
||||||
bio:
|
|
||||||
Math.random() > 0.5
|
|
||||||
? `Passionate ${skills[
|
|
||||||
Math.floor(Math.random() * skills.length)
|
|
||||||
].toLowerCase()} with ${
|
|
||||||
Math.floor(Math.random() * 8) + 1
|
|
||||||
}+ years experience`
|
|
||||||
: undefined,
|
|
||||||
created_at: new Date(
|
|
||||||
Date.now() - Math.random() * 365 * 86400000
|
|
||||||
).toISOString(),
|
|
||||||
email: `user${i}.team${teamId}@example.com`,
|
|
||||||
fullname: `${
|
|
||||||
['Ahmad', 'Sofia', 'Budi', 'Sari', 'Rizki', 'Maya', 'Andi', 'Dina'][
|
|
||||||
Math.floor(Math.random() * 8)
|
|
||||||
]
|
|
||||||
} ${
|
|
||||||
[
|
|
||||||
'Wijuana',
|
|
||||||
'Santoso',
|
|
||||||
'Pratama',
|
|
||||||
'Dewi',
|
|
||||||
'Nugroho',
|
|
||||||
'Sari',
|
|
||||||
'Putra',
|
|
||||||
'Lestari',
|
|
||||||
][Math.floor(Math.random() * 8)]
|
|
||||||
}`,
|
|
||||||
is_active: true,
|
|
||||||
location: cities[Math.floor(Math.random() * cities.length)],
|
|
||||||
phone_number:
|
|
||||||
Math.random() > 0.7
|
|
||||||
? `+62${Math.floor(Math.random() * 9000000000) + 1000000000}`
|
|
||||||
: undefined,
|
|
||||||
skills: skills.slice(0, Math.floor(Math.random() * 3) + 1),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
user_id: memberId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockData: TeamType[] = Array.from({ length: 50 }, (_, i) => {
|
|
||||||
const teamId = `team-${String(i + 1).padStart(3, '0')}`;
|
|
||||||
const memberCount = Math.floor(Math.random() * 5) + 1; // 1-5 members
|
|
||||||
const leaderId = `leader-${teamId}`;
|
|
||||||
const members = generateMembers(memberCount, teamId, leaderId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: teamId,
|
|
||||||
name: `Team ${teamNames[i % teamNames.length]} ${
|
|
||||||
Math.floor(i / teamNames.length) + 1
|
|
||||||
}`,
|
|
||||||
description:
|
|
||||||
i % 4 === 0 ? undefined : descriptions[i % descriptions.length],
|
|
||||||
city: cities[i % cities.length],
|
|
||||||
banner:
|
|
||||||
i % 3 === 0 ? undefined : `https://picsum.photos/600/200?random=${i}`, // 3:1 aspect ratio
|
|
||||||
logo:
|
|
||||||
i % 4 === 0
|
|
||||||
? undefined
|
|
||||||
: `https://ui-avatars.com/api/?name=${encodeURIComponent(
|
|
||||||
teamNames[i % teamNames.length]
|
|
||||||
)}&background=random&size=120`,
|
|
||||||
visibility: i % 4 === 0 ? 'private' : 'public',
|
|
||||||
member_count: memberCount,
|
|
||||||
has_submission: i % 3 !== 0,
|
|
||||||
created_at: new Date(
|
|
||||||
Date.now() - i * 86400000 * (Math.random() * 15 + 1)
|
|
||||||
).toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
leader_id: leaderId,
|
|
||||||
members: members,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const HackathonTeamsPage: FC = (): ReactElement => {
|
|
||||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
|
||||||
const [showNewTeamModal, setShowNewTeamModal] = useState(false);
|
|
||||||
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
|
||||||
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null);
|
|
||||||
const [selectedSubmissionTeam, setSelectedSubmissionTeam] =
|
|
||||||
useState<TeamType | null>(null);
|
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
|
||||||
|
|
||||||
// Advanced filtering states
|
|
||||||
const [visibilityFilter, setVisibilityFilter] = useState('all');
|
|
||||||
const [cityFilter, setCityFilter] = useState('all');
|
|
||||||
const [submissionFilter, setSubmissionFilter] = useState('all');
|
|
||||||
const [memberCountFilter, setMemberCountFilter] = useState('all');
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const pageSize = 10;
|
|
||||||
|
|
||||||
// Memoize the callback to prevent recreation
|
|
||||||
const handleShowDetailModal = useCallback((team: TeamType) => {
|
|
||||||
setSelectedTeam(team);
|
|
||||||
setShowDetailModal(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseDetailModal = useCallback(() => {
|
|
||||||
setShowDetailModal(false);
|
|
||||||
setSelectedTeam(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShowNewTeamModal = useCallback(() => {
|
|
||||||
setShowNewTeamModal(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseNewTeamModal = useCallback(() => {
|
|
||||||
setShowNewTeamModal(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShowSubmissionModal = useCallback((team: TeamType) => {
|
|
||||||
setSelectedSubmissionTeam(team);
|
|
||||||
setShowSubmissionModal(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseSubmissionModal = useCallback(() => {
|
|
||||||
setShowSubmissionModal(false);
|
|
||||||
setSelectedSubmissionTeam(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Filter data based on current filter states
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return mockData.filter((team) => {
|
|
||||||
// Global search filter
|
|
||||||
if (globalFilter) {
|
|
||||||
const searchTerm = globalFilter.toLowerCase();
|
|
||||||
const leaderName =
|
|
||||||
team.members.find((m) => m.role === 'leader')?.user.fullname || '';
|
|
||||||
const memberNames = team.members.map((m) => m.user.fullname).join(' ');
|
|
||||||
|
|
||||||
if (
|
|
||||||
!team.name.toLowerCase().includes(searchTerm) &&
|
|
||||||
!team.city.toLowerCase().includes(searchTerm) &&
|
|
||||||
!leaderName.toLowerCase().includes(searchTerm) &&
|
|
||||||
!memberNames.toLowerCase().includes(searchTerm)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visibility filter
|
|
||||||
if (visibilityFilter !== 'all' && team.visibility !== visibilityFilter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// City filter
|
|
||||||
if (cityFilter !== 'all' && team.city !== cityFilter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submission filter
|
|
||||||
if (submissionFilter !== 'all') {
|
|
||||||
const hasSubmission = submissionFilter === 'submitted';
|
|
||||||
if (team.has_submission !== hasSubmission) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Member count filter
|
|
||||||
if (memberCountFilter !== 'all') {
|
|
||||||
const count = parseInt(memberCountFilter);
|
|
||||||
if (team.member_count !== count) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
globalFilter,
|
|
||||||
visibilityFilter,
|
|
||||||
cityFilter,
|
|
||||||
submissionFilter,
|
|
||||||
memberCountFilter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Memoize columns to prevent recreation on every render
|
|
||||||
const columns: ColumnDef<TeamType>[] = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
accessorKey: 'name',
|
|
||||||
header: 'Team',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const team = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Team Logo */}
|
|
||||||
<div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center shrink-0 overflow-hidden">
|
|
||||||
{team.logo ? (
|
|
||||||
<img
|
|
||||||
src={team.logo}
|
|
||||||
alt={team.name}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TeamOutlined className="text-neutral-400 text-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Team Name & Description */}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="font-medium text-neutral-900 truncate">
|
|
||||||
{team.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'city',
|
|
||||||
header: 'City',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-neutral-700">{row.original.city}</span>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'visibility',
|
|
||||||
header: 'Visibility',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const isPublic = row.original.visibility === 'public';
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1 px-2 py-1 rounded-2xl text-xs font-medium',
|
|
||||||
isPublic
|
|
||||||
? 'bg-success-100 text-success-800'
|
|
||||||
: 'bg-neutral-100 text-neutral-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isPublic ? 'Public' : 'Private'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'member_count',
|
|
||||||
header: 'Members',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserOutlined className="text-neutral-400 text-sm" />
|
|
||||||
<span className="text-sm text-neutral-700">
|
|
||||||
{row.original.member_count}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'leader',
|
|
||||||
header: 'Leader',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const leader = row.original.members.find(
|
|
||||||
(m) => m.role === 'leader'
|
|
||||||
)?.user;
|
|
||||||
return leader ? (
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-neutral-900">
|
|
||||||
{leader.fullname}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-neutral-500">{leader.email}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-neutral-400 italic">No leader</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableSorting: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'has_submission',
|
|
||||||
header: 'Submission',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const hasSubmission = row.original.has_submission;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-2 h-2 rounded-full',
|
|
||||||
hasSubmission ? 'bg-success-500' : 'bg-danger-500'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
hasSubmission ? 'text-success-700' : 'text-danger-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{hasSubmission ? 'Submitted' : 'Not Submitted'}
|
|
||||||
</span>
|
|
||||||
{hasSubmission && (
|
|
||||||
<button
|
|
||||||
className="text-xs text-primary-600 hover:text-primary-800 text-left cursor-pointer"
|
|
||||||
onClick={() => handleShowSubmissionModal(row.original)}
|
|
||||||
>
|
|
||||||
View Submission <ArrowRightOutlined />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: (rowA, rowB) => {
|
|
||||||
const aSubmission = rowA.original.has_submission;
|
|
||||||
const bSubmission = rowB.original.has_submission;
|
|
||||||
if (aSubmission && !bSubmission) return -1;
|
|
||||||
if (!aSubmission && bSubmission) return 1;
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
header: 'Created',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-neutral-900 text-sm">
|
|
||||||
{new Date(row.original.created_at).toLocaleDateString('en-UK', {
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[handleShowDetailModal, handleShowSubmissionModal]
|
|
||||||
);
|
|
||||||
|
|
||||||
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 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 teams by name, city, or leader..."
|
|
||||||
value={globalFilter}
|
|
||||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Visibility 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-40 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
|
||||||
value={visibilityFilter}
|
|
||||||
onChange={(e) => setVisibilityFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Visibility</option>
|
|
||||||
<option value="public">Public</option>
|
|
||||||
<option value="private">Private</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* City Filter */}
|
|
||||||
<CityFilterSelect
|
|
||||||
value={cityFilter}
|
|
||||||
onChange={setCityFilter}
|
|
||||||
className="w-full sm:w-44"
|
|
||||||
placeholder="Search cities..."
|
|
||||||
allOptionLabel="All Cities"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Member Count 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-55 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
|
||||||
value={memberCountFilter}
|
|
||||||
onChange={(e) => setMemberCountFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Member Count</option>
|
|
||||||
<option value="1">1 Member</option>
|
|
||||||
<option value="2">2 Members</option>
|
|
||||||
<option value="3">3 Members</option>
|
|
||||||
<option value="4">4 Members</option>
|
|
||||||
<option value="5">5 Members</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submission 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={submissionFilter}
|
|
||||||
onChange={(e) => setSubmissionFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Submissions</option>
|
|
||||||
<option value="submitted">Submitted</option>
|
|
||||||
<option value="not_submitted">Not Submitted</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - Add Team Button */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-2 px-4 py-2"
|
|
||||||
onClick={handleShowNewTeamModal}
|
|
||||||
>
|
|
||||||
<PlusOutlined className="text-sm" />
|
|
||||||
Add Team
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active filters display */}
|
|
||||||
{(visibilityFilter !== 'all' ||
|
|
||||||
cityFilter !== 'all' ||
|
|
||||||
submissionFilter !== 'all' ||
|
|
||||||
memberCountFilter !== 'all') && (
|
|
||||||
<div className="flex flex-wrap gap-2 items-center">
|
|
||||||
<span className="text-sm text-neutral-600">Active filters:</span>
|
|
||||||
|
|
||||||
{/* Visibility filter badge */}
|
|
||||||
{visibilityFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
|
|
||||||
Visibility: {visibilityFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setVisibilityFilter('all')}
|
|
||||||
className="text-info-600 hover:text-info-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* City filter badge */}
|
|
||||||
{cityFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
|
|
||||||
City: {cityFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setCityFilter('all')}
|
|
||||||
className="text-green-600 hover:text-green-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Member count filter badge */}
|
|
||||||
{memberCountFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-800 rounded-2xl text-sm">
|
|
||||||
Members: {memberCountFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setMemberCountFilter('all')}
|
|
||||||
className="text-blue-600 hover:text-blue-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submission filter badge */}
|
|
||||||
{submissionFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-800 rounded-2xl text-sm">
|
|
||||||
Submission: {submissionFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setSubmissionFilter('all')}
|
|
||||||
className="text-purple-600 hover:text-purple-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Clear all filters */}
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setVisibilityFilter('all');
|
|
||||||
setCityFilter('all');
|
|
||||||
setSubmissionFilter('all');
|
|
||||||
setMemberCountFilter('all');
|
|
||||||
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} teams
|
|
||||||
{filteredData.length > pageSize}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={filteredData} columns={columns} pageSize={10} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Modals component */}
|
|
||||||
<ModalTeamDetail
|
|
||||||
isOpen={showDetailModal}
|
|
||||||
onClose={handleCloseDetailModal}
|
|
||||||
team={selectedTeam}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* New Team Modal */}
|
|
||||||
<ModalTeamDetail
|
|
||||||
isOpen={showNewTeamModal}
|
|
||||||
onClose={handleCloseNewTeamModal}
|
|
||||||
team={null} // null indicates creating new team
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Submission Modal */}
|
|
||||||
{selectedSubmissionTeam && (
|
|
||||||
<SubmissionModal
|
|
||||||
isOpen={showSubmissionModal}
|
|
||||||
onClose={handleCloseSubmissionModal}
|
|
||||||
teamId={selectedSubmissionTeam.id}
|
|
||||||
teamName={selectedSubmissionTeam.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HackathonTeamsPage;
|
|
||||||
@@ -1,479 +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';
|
|
||||||
import { CityFilterSelect } from '../../../components/city-filter-select';
|
|
||||||
|
|
||||||
// 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 [cityFilter, setCityFilter] = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// City filter
|
|
||||||
if (cityFilter !== 'all' && user.location !== cityFilter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skills filter
|
|
||||||
if (skillsFilter.length > 0) {
|
|
||||||
const hasMatchingSkill = skillsFilter.some((skill) =>
|
|
||||||
user.skills.includes(skill)
|
|
||||||
);
|
|
||||||
if (!hasMatchingSkill) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}, [statusFilter, cityFilter, 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>
|
|
||||||
|
|
||||||
{/* City Filter */}
|
|
||||||
<CityFilterSelect
|
|
||||||
value={cityFilter}
|
|
||||||
onChange={setCityFilter}
|
|
||||||
className="w-full sm:w-44"
|
|
||||||
placeholder="Search cities..."
|
|
||||||
allOptionLabel="All Cities"
|
|
||||||
/>
|
|
||||||
{cityFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
|
|
||||||
Location: {cityFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setCityFilter('all')}
|
|
||||||
className="text-green-600 hover:text-green-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 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' ||
|
|
||||||
cityFilter !== '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 */}
|
|
||||||
{cityFilter !== 'all' && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
|
|
||||||
City: {cityFilter}
|
|
||||||
<button
|
|
||||||
onClick={() => setCityFilter('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');
|
|
||||||
setCityFilter('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,56 +0,0 @@
|
|||||||
import { FC, ReactElement, useState } from 'react';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
|
||||||
<BackofficeSidebar
|
|
||||||
isOpen={mobileSidebarOpen}
|
|
||||||
onClose={() => setMobileSidebarOpen(false)}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
{/* Sticky top header */}
|
|
||||||
<header
|
|
||||||
className={
|
|
||||||
'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' +
|
|
||||||
(mobileSidebarOpen ? 'z-0' : 'z-30')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Mobile menu button (shown on small screens) */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700"
|
|
||||||
onClick={() => setMobileSidebarOpen(true)}
|
|
||||||
aria-label="Open sidebar"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<h1 className="text-p3 font-semibold text-primary-700">
|
|
||||||
IMPHNEN Backoffice
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalAddPermission {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalAddPermission = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
}: IModalAddPermission) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleAddItem={handleAddItem}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500">
|
|
||||||
Tambah Permissions
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Nama Permission"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
|
||||||
Tambah Permission
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleAddItem?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleAddItem,
|
|
||||||
{
|
|
||||||
success: 'Data permissions berhasil ditambahkan',
|
|
||||||
error: 'Data permissions gagal ditambahkan',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-7 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Permissions
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-center text-neutral-400">
|
|
||||||
Apakah kamu yakin ingin
|
|
||||||
<br /> menambahkan permission ini?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex mb-0 gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Tambahkan
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalAddPermission;
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalDeletePermission {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleDelete?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalDeletePermission = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleDelete,
|
|
||||||
}: IModalDeletePermission) => {
|
|
||||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
|
||||||
success: 'Data permissions berhasil dihapus',
|
|
||||||
error: 'Data permissions gagal dihapus',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Delete Permissions
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini
|
|
||||||
mungkin akan mempengaruhi fungsional sistem
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Batal Hapus
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Hapus Item
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalDeletePermission;
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalUpdatePermission {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleUpdate?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalUpdatePermission = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleUpdate,
|
|
||||||
}: IModalUpdatePermission) => {
|
|
||||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
|
||||||
success: 'Perubahan permissions berhasil dilakukan',
|
|
||||||
error: 'Perubahan permissions gagal dilakukan',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500">
|
|
||||||
Update Permissions
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
|
||||||
<InputField
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Nama Permission"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
type="submit"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Update Permission
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalUpdatePermission;
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
RowSelectionState,
|
|
||||||
useReactTable,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalAddPermission from './_components/modal-add-permission';
|
|
||||||
import ModalUpdatePermission from './_components/modal-update-permission';
|
|
||||||
import ModalDeletePermission from './_components/modal-delete-permission';
|
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface Permission {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: Permission[] = [
|
|
||||||
{ id: 1, name: 'Read' },
|
|
||||||
{ id: 2, name: 'Create' },
|
|
||||||
{ id: 3, name: 'Update' },
|
|
||||||
{ id: 4, name: 'Delete' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
|
||||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
step: currentStep,
|
|
||||||
nextStep,
|
|
||||||
prevStep,
|
|
||||||
resetStep,
|
|
||||||
} = useQueryState('step', {
|
|
||||||
defaultValue: 1,
|
|
||||||
maxValue: 2,
|
|
||||||
minValue: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
const columns: ColumnDef<Permission>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: () => (
|
|
||||||
<div className="flex gap-[8px]">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalUpdateItem(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Update
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalDeleteItem(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined /> Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Permissions</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama permissions"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex gap-3 text-nowrap"
|
|
||||||
onClick={() => {
|
|
||||||
setShowModalAddItem(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusOutlined />
|
|
||||||
Tambah Permissionss
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
data={mockData}
|
|
||||||
columns={columns}
|
|
||||||
pageSize={9}
|
|
||||||
table={table}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<ModalAddPermission
|
|
||||||
currentStep={currentStep}
|
|
||||||
isOpen={showModalAddItem}
|
|
||||||
onClose={() => setShowModalAddItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
<ModalUpdatePermission
|
|
||||||
isOpen={showModalUpdateItem}
|
|
||||||
onClose={() => setShowModalUpdateItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
<ModalDeletePermission
|
|
||||||
isOpen={showModalDeleteItem}
|
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
AuditOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
PaginationState,
|
|
||||||
useReactTable,
|
|
||||||
RowSelectionState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import ModalProcessDelivery from './_components/modal-process-item';
|
|
||||||
|
|
||||||
type OrderValid = 'valid' | 'invalid' | 'unchecked';
|
|
||||||
type Status = 'undelivered' | 'delivered';
|
|
||||||
|
|
||||||
interface Prize {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
orderValid: OrderValid;
|
|
||||||
items: string;
|
|
||||||
address: string;
|
|
||||||
status: Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
'Sertifikat + Laminating',
|
|
||||||
'Lanyard + ID Card',
|
|
||||||
'Pin',
|
|
||||||
'Sticker Isi 3',
|
|
||||||
'Sticker Isi 5',
|
|
||||||
'Gelang Karet',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock data for transactions
|
|
||||||
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: 'Nama Lengkap',
|
|
||||||
orderValid: (i % 3 === 0
|
|
||||||
? 'invalid'
|
|
||||||
: i % 5 === 0
|
|
||||||
? 'unchecked'
|
|
||||||
: 'valid') as OrderValid,
|
|
||||||
items: items[i % items.length],
|
|
||||||
address: 'Jl. Pantai Cibaduyut Indah',
|
|
||||||
status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [showModalProcessDelivery, setShowModalProcessDelivery] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
|
|
||||||
const deliveryOptions = [
|
|
||||||
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
|
|
||||||
{ id: 'option1', value: 'delivered', label: 'Delivered' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns: ColumnDef<Prize>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Lengkap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Order Valid?',
|
|
||||||
accessorKey: 'orderValid',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.orderValid;
|
|
||||||
const statusColors: Record<OrderValid, string> = {
|
|
||||||
valid: 'bg-success-200 text-success-500',
|
|
||||||
invalid: 'bg-danger-200 text-danger-500',
|
|
||||||
unchecked: 'bg-warning-200 text-warning-900',
|
|
||||||
};
|
|
||||||
const statusText: Record<OrderValid, string> = {
|
|
||||||
valid: 'Valid',
|
|
||||||
invalid: 'Invalid',
|
|
||||||
unchecked: 'Unchecked',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Items',
|
|
||||||
accessorKey: 'items',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Alamat Pengiriman',
|
|
||||||
accessorKey: 'address',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<Status, string> = {
|
|
||||||
delivered: 'bg-success-200 text-success-500',
|
|
||||||
undelivered: 'bg-danger-200 text-danger-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<Status, string> = {
|
|
||||||
delivered: 'Delivered',
|
|
||||||
undelivered: 'Undelivered',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModalProcessDelivery(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-full"
|
|
||||||
>
|
|
||||||
<AuditOutlined className="text-[16px]" /> Process
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
|
|
||||||
</header>
|
|
||||||
{/* Account Table Section */}
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
{/* Search and Filter */}
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
{showFilter && (
|
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
|
||||||
<Filter
|
|
||||||
options={deliveryOptions}
|
|
||||||
onClose={() => setShowFilter(false)}
|
|
||||||
onFilterChange={(value) => {
|
|
||||||
console.log('Selected filter:', value);
|
|
||||||
// Filter logic di sini
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Modal Process Delivery */}
|
|
||||||
<ModalProcessDelivery
|
|
||||||
isOpen={showModalProcessDelivery}
|
|
||||||
onClose={() => setShowModalProcessDelivery(false)}
|
|
||||||
handleProcessDelivery={() => {
|
|
||||||
console.log('Action ketika user menekan tombol Proses Pengiriman');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
import { ArrowRightOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { Accordion, Modal, ModalProps } from "@imphnen-frontend-service/ui/molecules";
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
|
||||||
|
|
||||||
export const ModalCreateRoadmap: FC<Omit<ModalProps, 'children'>> = ({ isOpen, onClose }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<div className="overflow-y-auto max-h-[80vh]">
|
|
||||||
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
|
||||||
Create Roadmaps
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-6 mb-12">
|
|
||||||
<div className="col-span-3">
|
|
||||||
<label className={labelClass}>Prompt</label>
|
|
||||||
<Textarea
|
|
||||||
className="min-w-full w-full h-[calc(100%-2rem)]"
|
|
||||||
placeholder="Create a learning roadmap for (subject) at the (level) level. Include topics, estimated duration, and logical progression."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 space-y-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Roadmap Name</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Nama Roadmap" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Roadmap Name</label>
|
|
||||||
<Select className="w-full">
|
|
||||||
<option value="pemula">Pemula</option>
|
|
||||||
<option value="menengah">Menengah</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Model</label>
|
|
||||||
<Select className="w-full">
|
|
||||||
<option value="gpt-3.5-turbo">GPT 3.5 Turbo</option>
|
|
||||||
<option value="gpt-4">GPT 4</option>
|
|
||||||
<option value="gpt-4-32k">GPT 4 32k</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-full">
|
|
||||||
<Button>
|
|
||||||
Generate Roadmap
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 className="text-neutral-600 text-p2 font-semibold mb-8">
|
|
||||||
Roadmap Preview
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2.5">
|
|
||||||
<For data={Array.from({ length: 3 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<Accordion
|
|
||||||
key={index}
|
|
||||||
title={`Day ${index + 1}`}
|
|
||||||
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In convallis tincidunt nisl, id consequat mi malesuada vel. Nulla facilisi. Nam in turpis ligula."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end mt-12">
|
|
||||||
<Button type="button" className="flex items-center gap-5">
|
|
||||||
Submit Roadmap
|
|
||||||
<ArrowRightOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap";
|
|
||||||
|
|
||||||
type LearningStatus = 'active' | 'inactive'
|
|
||||||
|
|
||||||
type RoadmapType = {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
learningLevel: string
|
|
||||||
status: LearningStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i === 0 ? 'Ahmad Wijuana' : 'Anna Wiguana',
|
|
||||||
learningLevel: ['Pemula', 'Menengah'][Math.floor(Math.random() * 2)],
|
|
||||||
status: i % 2 === 0 ? 'active' : 'inactive',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): React.ReactElement {
|
|
||||||
const [openCreateModal, setOpenCreateModal] = useState(false)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<RoadmapType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'id',
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
header: 'Nama Roadmap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'learningLevel',
|
|
||||||
header: 'Tingkat Belajar',
|
|
||||||
accessorKey: 'learningLevel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<LearningStatus, string> = {
|
|
||||||
inactive: 'bg-danger-200 text-danger-700',
|
|
||||||
active: 'bg-success-200 text-success-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<LearningStatus, string> = {
|
|
||||||
inactive: 'Inactive',
|
|
||||||
active: 'Active',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-72") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex gap-[8px]">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Action
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined /> Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Content & Roadmap</h1>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex items-center justify-between mb-9">
|
|
||||||
<h2 className="text-p2 font-semibold text-neutral-600">AI Roadmaps</h2>
|
|
||||||
<Button type="button" variant="primary" className="flex items-center gap-2" onClick={() => setOpenCreateModal(true)}>
|
|
||||||
<PlusOutlined /> Buat Roadmap
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama roadmap"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Tingkat Belajar</option>
|
|
||||||
<option value="pemula">Pemula</option>
|
|
||||||
<option value="menengah">Menengah</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalAddRole {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleAdd?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalAddRole = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
resetStep,
|
|
||||||
handleAdd,
|
|
||||||
}: IModalAddRole) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
onClose();
|
|
||||||
resetStep();
|
|
||||||
}}
|
|
||||||
disableEscapeKeyDown={true}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<StepTwo
|
|
||||||
onClose={onClose}
|
|
||||||
handleAdd={handleAdd}
|
|
||||||
resetStep={resetStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepOneProps {
|
|
||||||
nextStep: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header>
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Roles
|
|
||||||
</h2>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<ControlledInputField
|
|
||||||
control={form.control}
|
|
||||||
label="Nama Role"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan Nama Role"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 items-start overflow-auto">
|
|
||||||
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
|
||||||
Permissions
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-x-6 overflow-x-scroll">
|
|
||||||
{[
|
|
||||||
'Gacha Items',
|
|
||||||
'Gacha Roll',
|
|
||||||
'Roll',
|
|
||||||
'Users',
|
|
||||||
'Gacha Claim',
|
|
||||||
].map((title) => (
|
|
||||||
<div
|
|
||||||
key={title}
|
|
||||||
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
|
||||||
>
|
|
||||||
<span className="text-nowrap text-label1">{title}</span>
|
|
||||||
<div className="flex gap-[8px] items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={`${title}-all`}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
<label htmlFor={`${title}-all`} className="text-nowrap">
|
|
||||||
Check All
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<hr className="border-blue-200" />
|
|
||||||
<div className="flex flex-col items-start gap-4 mb-4">
|
|
||||||
<div className="flex gap-[8px] items-center">
|
|
||||||
<input type="checkbox" id={`${title}-read`} />
|
|
||||||
<label htmlFor={`${title}-read`}>Read</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-[8px] items-center">
|
|
||||||
<input type="checkbox" id={`${title}-create`} />
|
|
||||||
<label htmlFor={`${title}-create`}>Create</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-[8px] items-center">
|
|
||||||
<input type="checkbox" id={`${title}-update`} />
|
|
||||||
<label htmlFor={`${title}-update`}>Update</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-[8px] items-center">
|
|
||||||
<input type="checkbox" id={`${title}-delete`} />
|
|
||||||
<label htmlFor={`${title}-delete`}>Delete</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
|
||||||
Tambah Role
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IStepTwoProps {
|
|
||||||
onClose: () => void;
|
|
||||||
handleAdd?: () => Promise<boolean>;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
|
|
||||||
const { onConfirm, onCancel } = useConfirmItem(
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleAdd,
|
|
||||||
{
|
|
||||||
success: 'Data role berhasil ditambahkan',
|
|
||||||
error: 'Data role gagal ditambahkan',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal.Header className="mb-10 text-center items-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
|
||||||
Tambah Roles
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-center text-neutral-400">
|
|
||||||
Apakah kamu yakin ingin
|
|
||||||
<br /> menambahkan role ini?
|
|
||||||
</p>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex mb-0 gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Tambahkan
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalAddRole;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { useConfirmItem } from '../_hook/use-item';
|
|
||||||
|
|
||||||
interface IModalDeletePermission {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
handleDelete?: () => Promise<boolean>;
|
|
||||||
currentStep?: number;
|
|
||||||
nextStep: () => void;
|
|
||||||
prevStep: () => void;
|
|
||||||
resetStep: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalDeletePermission = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
resetStep,
|
|
||||||
handleDelete,
|
|
||||||
}: IModalDeletePermission) => {
|
|
||||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
|
||||||
success: 'Data roles berhasil dihapus',
|
|
||||||
error: 'Data roles gagal dihapus',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Delete Roles
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus role ini? Menghapus data ini
|
|
||||||
mungkin akan mempengaruhi fungsional sistem
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Batal Hapus
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Hapus Role
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalDeletePermission;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user