Compare commits
331
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d3e2470c8 | ||
|
|
48411d3a08 | ||
|
|
426b71f341 | ||
|
|
06c69cb3c5 | ||
|
|
81999aa044 | ||
|
|
d83a6d9ec7 | ||
|
|
63dfb7299f | ||
|
|
53079fe868 | ||
|
|
2944a2b98a | ||
|
|
79f087e2fc | ||
|
|
d5b267332f | ||
|
|
8cb7b592eb | ||
|
|
b2ed098067 | ||
|
|
8749e83dad | ||
|
|
00b0997a9b | ||
|
|
05bdac6ee0 | ||
|
|
5cbad2a996 | ||
|
|
e78002ff69 | ||
|
|
b48f653a97 | ||
|
|
63da701b1b | ||
|
|
7bef667e96 | ||
|
|
82d1fae3d5 | ||
|
|
49d8419c1c | ||
|
|
4240e8eb51 | ||
|
|
86d9e759a6 | ||
|
|
6b3faf2a21 | ||
|
|
bcfac70e2a | ||
|
|
cb32146daa | ||
|
|
b82b03a8a2 | ||
|
|
0e0ca9720a | ||
|
|
964dd009cc | ||
|
|
a7ec622925 | ||
|
|
931355e64e | ||
|
|
58fbfcd0d8 | ||
|
|
063587aca9 | ||
|
|
7576b2557d | ||
|
|
95bf09c2fa | ||
|
|
0ade3c3e50 | ||
|
|
e945da4b7f | ||
|
|
61b650bb25 | ||
|
|
e61c003e07 | ||
|
|
8fff0df753 | ||
|
|
054f16053d | ||
|
|
0193245551 | ||
|
|
7f5948724b | ||
|
|
2cba806cd5 | ||
|
|
414e4d19b8 | ||
|
|
7268403ca4 | ||
|
|
651fd65ccb | ||
|
|
4e9d110bb0 | ||
|
|
c6719b19e5 | ||
|
|
53ad40f3fd | ||
|
|
8d2aaf5188 | ||
|
|
3f27e8d622 | ||
|
|
629d609792 | ||
|
|
8439c5d5e0 | ||
|
|
1afd7c014c | ||
|
|
419335d6b3 | ||
|
|
aef8d59c9f | ||
|
|
e14f6d4ef7 | ||
|
|
aa3e935fee | ||
|
|
1100d6f417 | ||
|
|
54ebcf1a1e | ||
|
|
9dabbb625d | ||
|
|
3f4461c65c | ||
|
|
f68d97188c | ||
|
|
f882668e7f | ||
|
|
dab4fa4941 | ||
|
|
bbb1bae909 | ||
|
|
3de1748f12 | ||
|
|
a610e180ba | ||
|
|
ce8cb8188c | ||
|
|
3aaf022393 | ||
|
|
ebe27a3f05 | ||
|
|
f846a5a785 | ||
|
|
708cbde596 | ||
|
|
9d37cba65f | ||
|
|
8728d0abd7 | ||
|
|
1f4f4ddaa1 | ||
|
|
4fcc249ff4 | ||
|
|
c2bed98b06 | ||
|
|
ae1864c134 | ||
|
|
3f30fbe89c | ||
|
|
951db0f28a | ||
|
|
ad59d39f55 | ||
|
|
1283689d84 | ||
|
|
7d1370d7f2 | ||
|
|
ab0b56780f | ||
|
|
1a39ac37db | ||
|
|
e5dc784301 | ||
|
|
b2de22b0ef | ||
|
|
ad20eda182 | ||
|
|
da65aa4109 | ||
|
|
1f0798752c | ||
|
|
5f28b9b406 | ||
|
|
9e8fc7c07f | ||
|
|
ca51f8aca2 | ||
|
|
c086b1371c | ||
|
|
398350795d | ||
|
|
5be9ec1edf | ||
|
|
f77967a092 | ||
|
|
fe172cc128 | ||
|
|
38ee9a9549 | ||
|
|
3ad828e759 | ||
|
|
782cce820b | ||
|
|
f5882e3cdc | ||
|
|
afaaff4a7f | ||
|
|
da8b08cd73 | ||
|
|
1dbe704230 | ||
|
|
59c1bb0366 | ||
|
|
c518784810 | ||
|
|
0267de664b | ||
|
|
134ff14946 | ||
|
|
ed3b711c89 | ||
|
|
6b7b1c4d20 | ||
|
|
b4b19e02fa | ||
|
|
e61979cbb6 | ||
|
|
0c7887268c | ||
|
|
76933c0be7 | ||
|
|
abc4c26d5d | ||
|
|
a3d35716a6 | ||
|
|
ac22bd6cab | ||
|
|
27ed5605ce | ||
|
|
2c6389a67a | ||
|
|
a4c75770ae | ||
|
|
d923237969 | ||
|
|
b2a28c6fd3 | ||
|
|
417ff3390b | ||
|
|
0c646ceb85 | ||
|
|
6bbc87479a | ||
|
|
23b6677323 | ||
|
|
45c75b3283 | ||
|
|
764eb34e47 | ||
|
|
fe4e9291fa | ||
|
|
ec9a81d7b5 | ||
|
|
e652c6ed0e | ||
|
|
d431564aac | ||
|
|
350d08a2b4 | ||
|
|
eb5e043357 | ||
|
|
d67e3e7244 | ||
|
|
a7196dd453 | ||
|
|
c83e54fae8 | ||
|
|
35a5761a7c | ||
|
|
b472fbdbbf | ||
|
|
4fcc96ac15 | ||
|
|
2dfcd3adad | ||
|
|
d79788258b | ||
|
|
427d2b69c7 | ||
|
|
7e5e4cc29b | ||
|
|
a1e89b882a | ||
|
|
a74f0181f1 | ||
|
|
a9a1e5450b | ||
|
|
5e65942060 | ||
|
|
d044d0ef50 | ||
|
|
d40a913ccc | ||
|
|
66178ccc18 | ||
|
|
8280ba2a64 | ||
|
|
8259020450 | ||
|
|
692a7a7258 | ||
|
|
1ee04ada14 | ||
|
|
193cb61b45 | ||
|
|
4777a34f23 | ||
|
|
4d3677ebd2 | ||
|
|
f0eb11010d | ||
|
|
e1f13b3872 | ||
|
|
fa30849d03 | ||
|
|
f8d592d811 | ||
|
|
c385aa1996 | ||
|
|
9a97eb3e8e | ||
|
|
70ee5300f7 | ||
|
|
35c9539c08 | ||
|
|
bc5b072ad8 | ||
|
|
66b8b064ba | ||
|
|
63643cc286 | ||
|
|
19bd44a7ca | ||
|
|
b3bdf25b5f | ||
|
|
cdd4fd4703 | ||
|
|
7d1b86acdb | ||
|
|
6e6c361eb8 | ||
|
|
62780c8889 | ||
|
|
1839f4cab0 | ||
|
|
c9f5b9e4a5 | ||
|
|
33365affa2 | ||
|
|
63db7ff6ad | ||
|
|
7f8b53edd4 | ||
|
|
eb0628ac3e | ||
|
|
cbb880a053 | ||
|
|
083a912e91 | ||
|
|
ab744b411d | ||
|
|
8947b4d05e | ||
|
|
7e3eaa6584 | ||
|
|
98a459c7d7 | ||
|
|
9d24a11c05 | ||
|
|
ee60787100 | ||
|
|
301a349776 | ||
|
|
81488ceb6b | ||
|
|
9e99fd61e7 | ||
|
|
d36c693615 | ||
|
|
20125a6f4a | ||
|
|
4bc5006c91 | ||
|
|
71769b71aa | ||
|
|
a2cf67d210 | ||
|
|
d9c4217fd3 | ||
|
|
719e36e46c | ||
|
|
bd052f4866 | ||
|
|
734e8540f2 | ||
|
|
1289555749 | ||
|
|
971fe7160f | ||
|
|
722caeec9d | ||
|
|
7404afe466 | ||
|
|
400daea60f | ||
|
|
40cd47c6be | ||
|
|
529d065a75 | ||
|
|
69d6d86342 | ||
|
|
31736cc403 | ||
|
|
1cb2443b58 | ||
|
|
9e9bfc2793 | ||
|
|
5a99f216cb | ||
|
|
efdbc65be6 | ||
|
|
f5905c0acc | ||
|
|
6348a4998b | ||
|
|
e067450fcb | ||
|
|
a281f279f8 | ||
|
|
a157e11e13 | ||
|
|
b38afd2b44 | ||
|
|
3c301ec2c4 | ||
|
|
65a6bb686c | ||
|
|
f3a19fa460 | ||
|
|
228f92e9ed | ||
|
|
dcd4ef88d6 | ||
|
|
ec13f2c693 | ||
|
|
302ee37ca1 | ||
|
|
b7282f9c99 | ||
|
|
2c347d1cc5 | ||
|
|
791f43986d | ||
|
|
9f25941a4a | ||
|
|
aaf5b4262d | ||
|
|
0f46244d2b | ||
|
|
dd03b3da85 | ||
|
|
9a0fee65a5 | ||
|
|
376e646c10 | ||
|
|
f8cd57a1cf | ||
|
|
5618a8e99b | ||
|
|
598bd22b5a | ||
|
|
246adb57a1 | ||
|
|
75839271ad | ||
|
|
6e78b49731 | ||
|
|
3aa552afd6 | ||
|
|
b389235971 | ||
|
|
86290cc820 | ||
|
|
41712be56a | ||
|
|
240e684b53 | ||
|
|
b1a53df4a9 | ||
|
|
0709c3a2bd | ||
|
|
f2e1cead83 | ||
|
|
1b795f2dfc | ||
|
|
ce115f63e4 | ||
|
|
c907c696f5 | ||
|
|
de76020709 | ||
|
|
1e391a2932 | ||
|
|
a7c67cb77f | ||
|
|
dafdfba490 | ||
|
|
a741fbb8d4 | ||
|
|
0e3dc101d5 | ||
|
|
e805e5fd41 | ||
|
|
5bdf8f5a4d | ||
|
|
07f1fb0208 | ||
|
|
3958ae1098 | ||
|
|
885d156833 | ||
|
|
2746af0b3f | ||
|
|
745a2d2873 | ||
|
|
8887e3b0a9 | ||
|
|
638b64b715 | ||
|
|
d61cb4a215 | ||
|
|
272db82c8c | ||
|
|
9108ddb742 | ||
|
|
628e168342 | ||
|
|
9aff9e2dc8 | ||
|
|
585ad482f2 | ||
|
|
988476b409 | ||
|
|
38e6d4643b | ||
|
|
79cb9d81fc | ||
|
|
64e702df5d | ||
|
|
280afd68ab | ||
|
|
9777b7bea9 | ||
|
|
6bad504d44 | ||
|
|
6b959a2f22 | ||
|
|
33c04707f8 | ||
|
|
f87dc4c782 | ||
|
|
b5428d8552 | ||
|
|
0a846b0512 | ||
|
|
3a950ea867 | ||
|
|
4693284b37 | ||
|
|
f8f796e071 | ||
|
|
fe1928e8ef | ||
|
|
9c7b8c1c63 | ||
|
|
2bd767d20b | ||
|
|
a6fea630a4 | ||
|
|
c05061becf | ||
|
|
a9cc728ade | ||
|
|
2602657201 | ||
|
|
c70e5706bc | ||
|
|
49ca2a9fb0 | ||
|
|
47ebd3ad73 | ||
|
|
d544e43abc | ||
|
|
d7a90f47d0 | ||
|
|
984a31c587 | ||
|
|
a575e534b5 | ||
|
|
989503f930 | ||
|
|
3b979119ed | ||
|
|
ecec423d30 | ||
|
|
3bbdba7015 | ||
|
|
f7a7acaf65 | ||
|
|
809eeee931 | ||
|
|
f39466936f | ||
|
|
5d418830b9 | ||
|
|
d58ad58ef8 | ||
|
|
b62c762cf9 | ||
|
|
4e665cca76 | ||
|
|
74c0ebc5dd | ||
|
|
eadd554a32 | ||
|
|
ecdaa4a82e | ||
|
|
747e75e684 | ||
|
|
76637dd051 | ||
|
|
1594be9d46 | ||
|
|
7eecf2621d | ||
|
|
088a0fc41f | ||
|
|
9178c65c4a | ||
|
|
f93d741cef | ||
|
|
81fa73935f | ||
|
|
a3887519f9 |
@@ -0,0 +1,30 @@
|
|||||||
|
## Issue
|
||||||
|
|
||||||
|
<!-- #GitHubIssueNumber / Issue Link / Describe the problem here (if the issue hasn't been defined) -->
|
||||||
|
|
||||||
|
## What did you do?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Describe what did you do in this code
|
||||||
|
- Example:
|
||||||
|
- [CORRECT] Add auth feature / Modify auth feature.
|
||||||
|
- [WRONG] Create file /controllers/auth.ts, /models/user.ts, modify /routes/web.ts
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Screenshot / Video
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Attach pictures of this work / video explanation about this work
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Note for Deployment
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Tell to DevOps what should he do to deploy this change
|
||||||
|
- example:
|
||||||
|
- Please run migration when deploy this
|
||||||
|
- Please upload these files into folder xxx
|
||||||
|
- Please create folder `xxx` and give permission `777`
|
||||||
|
- Please run this SQL after deploy ```sql INSERT INTO xxx (...) ```
|
||||||
|
- etc...
|
||||||
|
-->
|
||||||
@@ -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
|
||||||
+36
-1
@@ -4,7 +4,7 @@
|
|||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
out-tsc
|
out-tsc
|
||||||
|
docs/FigmaImage/
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
@@ -45,3 +45,38 @@ vite.config.*.timestamp*
|
|||||||
vitest.config.*.timestamp*
|
vitest.config.*.timestamp*
|
||||||
|
|
||||||
storybook-static
|
storybook-static
|
||||||
|
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.prod
|
||||||
|
.env.develop
|
||||||
|
.env.staging
|
||||||
|
|
||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
.cursor/rules/nx-rules.mdc
|
||||||
|
.github/instructions/nx.instructions.md
|
||||||
|
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
.direnv
|
||||||
|
result
|
||||||
|
|
||||||
|
.claude/worktrees
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Screenshots and Test Reports
|
||||||
|
**/screenshots
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# path to a directory with all packages
|
|
||||||
storage: ../tmp/local-registry/storage
|
|
||||||
|
|
||||||
# a list of other known repositories we can talk to
|
|
||||||
uplinks:
|
|
||||||
npmjs:
|
|
||||||
url: https://registry.npmjs.org/
|
|
||||||
maxage: 60m
|
|
||||||
|
|
||||||
packages:
|
|
||||||
'**':
|
|
||||||
# give all users (including non-authenticated users) full access
|
|
||||||
# because it is a local registry
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
unpublish: $all
|
|
||||||
|
|
||||||
# if package is not available locally, proxy requests to npm registry
|
|
||||||
proxy: npmjs
|
|
||||||
|
|
||||||
# log settings
|
|
||||||
log:
|
|
||||||
type: stdout
|
|
||||||
format: pretty
|
|
||||||
level: warn
|
|
||||||
|
|
||||||
publish:
|
|
||||||
allow_offline: true # set offline to true to allow publish offline
|
|
||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"nrwl.angular-console",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# IMPHNEN Frontend Service
|
||||||
|
|
||||||
|
Nx monorepo for IMPHNEN (Ingin Menjadi Programmer Handal Namun Enggan Ngoding) — Indonesia's largest programmer community.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Monorepo**: Nx 22.6
|
||||||
|
- **Framework**: Next.js 16 (landing), Vite + React 19 (all other apps)
|
||||||
|
- **Routing**: react-router v7 (Vite apps), Next.js App Router (landing)
|
||||||
|
- **Styling**: Tailwind CSS v4, class-variance-authority (CVA)
|
||||||
|
- **State**: Zustand, TanStack React Query
|
||||||
|
- **Forms**: react-hook-form + zod
|
||||||
|
- **Build/Deploy**: Nix flakes, Cachix binary cache, NixOS modules
|
||||||
|
- **CI/CD**: GitHub Actions with `nx affected` + matrix strategy
|
||||||
|
- **Node**: v22
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Apps
|
||||||
|
| App | Framework | Description |
|
||||||
|
|-----|-----------|-------------|
|
||||||
|
| `landing` | Next.js 16 | Public website |
|
||||||
|
| `backoffice` | Vite + React | Admin dashboard |
|
||||||
|
| `hackathon` | Vite + React | Hackathon platform |
|
||||||
|
| `dimentorin` | Vite + React | Mentoring platform |
|
||||||
|
| `gacha` | Vite + React | Merch gacha system |
|
||||||
|
| `qrcampaign` | Vite + React | QR campaign tool |
|
||||||
|
| `infra` | Vite + React | Infrastructure dashboard |
|
||||||
|
|
||||||
|
### Shared Libraries
|
||||||
|
| Lib | Purpose | Depends on |
|
||||||
|
|-----|---------|------------|
|
||||||
|
| `utils` | Pure utilities only: `cn`, `For`, `Show`, `useQueryState`, `useModalLogin`, react-query helpers, react-router file-based routing | nothing |
|
||||||
|
| `service` | Business logic: API clients, auth hooks, storage (SessionToken/SessionUser), constants (PERMISSIONS, cities) | `utils` |
|
||||||
|
| `ui` | UI components: atoms (Button, Input, Card, Dialog, Drawer, Form, Label), molecules, organisms (Navbar, Sidebar, Datatable) | `utils`, `service` |
|
||||||
|
|
||||||
|
**Dependency rule**: `ui` → `service` → `utils` (never the reverse)
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
nx dev <app> # Start dev server
|
||||||
|
nx build <app> # Build single app
|
||||||
|
nx run-many -t build --all # Build everything
|
||||||
|
|
||||||
|
# Test
|
||||||
|
nx test <lib> # Run unit tests (vitest)
|
||||||
|
nx e2e <app>-e2e # Run e2e tests (playwright)
|
||||||
|
nx lint <project> # Lint
|
||||||
|
|
||||||
|
# Build all affected
|
||||||
|
nx affected -t build # Only build what changed
|
||||||
|
nx affected -t test # Only test what changed
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
nix build .#<app> # Build Nix package for an app
|
||||||
|
nix develop # Enter dev shell (node 22, bun, git, jq)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nix / Deployment
|
||||||
|
|
||||||
|
All Nix config is in a single `flake.nix`:
|
||||||
|
- `mkViteApp`: Builds Vite apps as Nix packages
|
||||||
|
- `mkLandingApp`: Builds the Next.js landing app
|
||||||
|
- `mkLandingModule`: NixOS module (systemd service for Next.js)
|
||||||
|
- `mkStaticAppModule`: NixOS module (nginx for static Vite apps)
|
||||||
|
- `npmDepsHash`: Must be updated when `package-lock.json` changes. Use `lib.fakeHash` to get the new hash from a failed build.
|
||||||
|
|
||||||
|
## CI/CD Pipeline (.github/workflows/nix-build.yml)
|
||||||
|
|
||||||
|
1. **detect**: Uses `nx affected` to find changed apps
|
||||||
|
2. **build**: Matrix strategy builds only affected apps with Nix, pushes to Cachix
|
||||||
|
3. **deploy**: Clones `imphnen-infrastructure`, updates `flake.lock`, pushes, then runs `clan machines update hetzner` to deploy to the Hetzner server
|
||||||
|
|
||||||
|
Required GitHub secrets: `CACHIX_AUTH_TOKEN`, `INFRA_DEPLOY_KEY` (SSH key for both GitHub and server access)
|
||||||
|
|
||||||
|
The server (167.235.70.37) pulls pre-built packages from Cachix during `nixos-rebuild`.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- Vite apps: `VITE_API_URL`, `VITE_GITHUB_CLIENT_ID`
|
||||||
|
- Next.js (landing): `NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_GITHUB_CLIENT_ID`
|
||||||
|
- The `getBaseURL()` function in `libs/service/src/api/index.ts` handles both environments
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- Atomic design: atoms → molecules → organisms
|
||||||
|
- Components have their own folder with `component.tsx`, `index.ts`, `spec.tsx`, `stories.tsx`
|
||||||
|
- Use `cn()` from `@imphnen-frontend-service/utils` for className merging
|
||||||
|
- Use `'use client'` directive on any component using React hooks (for Next.js compatibility)
|
||||||
|
- Button variants via CVA: `primary`, `secondary`, `text`, `bordered`, `success`, `danger`
|
||||||
|
- Landing app uses `container` class — Tailwind v4 requires explicit `margin-inline: auto` (defined in globals.css)
|
||||||
@@ -1,101 +1,120 @@
|
|||||||
# ImphnenFrontendService
|
# IMPHNEN Frontend Service
|
||||||
|
|
||||||
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
<p align="center">
|
||||||
|
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||||
|
</p>
|
||||||
|
|
||||||
✨ Your new, shiny [Nx workspace](https://nx.dev) is ready ✨.
|
Monorepo for all frontend services of [IMPHNEN](https://imphnen.dev) (Ingin Menjadi Programmer Handal Namun Enggan Ngoding) — Indonesia's largest programmer community.
|
||||||
|
|
||||||
[Learn more about this workspace setup and its capabilities](https://nx.dev/getting-started/tutorials/react-monorepo-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
|
## Apps
|
||||||
|
|
||||||
## Run tasks
|
| 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) |
|
||||||
|
|
||||||
To run the dev server for your app, use:
|
## 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
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx serve dimentorin
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git
|
||||||
|
cd imphnen-frontend-service
|
||||||
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
To create a production bundle:
|
Or with Nix:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx build dimentorin
|
nix develop # enters dev shell with node 22, bun, git, jq
|
||||||
```
|
```
|
||||||
|
|
||||||
To see all available targets to run for a project, run:
|
### Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` in the app directory:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx show project dimentorin
|
cp apps/<app>/.env.example apps/<app>/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
|
### Development
|
||||||
|
|
||||||
[More about running tasks in the docs »](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
## Add new projects
|
|
||||||
|
|
||||||
While you could add new projects to your workspace manually, you might want to leverage [Nx plugins](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) and their [code generation](https://nx.dev/features/generate-code?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) feature.
|
|
||||||
|
|
||||||
Use the plugin's generator to create new projects.
|
|
||||||
|
|
||||||
To generate a new application, use:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx g @nx/react:app demo
|
nx dev <app> # e.g. nx dev landing, nx dev backoffice
|
||||||
```
|
```
|
||||||
|
|
||||||
To generate a new library, use:
|
### Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx g @nx/react:lib mylib
|
nx build <app> # build single app
|
||||||
|
nx run-many -t build --all # build everything
|
||||||
|
nx affected -t build # build only what changed
|
||||||
```
|
```
|
||||||
|
|
||||||
You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx list <plugin-name>` to learn about more specific capabilities of a particular plugin. Alternatively, [install Nx Console](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) to browse plugins and generators in your IDE.
|
### Nix Build
|
||||||
|
|
||||||
[Learn more about Nx plugins »](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry »](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
## Set up CI!
|
|
||||||
|
|
||||||
### Step 1
|
|
||||||
|
|
||||||
To connect to Nx Cloud, run the following command:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx connect
|
nix build .#<app> # e.g. nix build .#landing, .#dimentorin
|
||||||
```
|
```
|
||||||
|
|
||||||
Connecting to Nx Cloud ensures a [fast and scalable CI](https://nx.dev/ci/intro/why-nx-cloud?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) pipeline. It includes features such as:
|
All Nix config lives in `flake.nix`. When `package-lock.json` changes, update `npmDepsHash` using `lib.fakeHash`.
|
||||||
|
|
||||||
- [Remote caching](https://nx.dev/ci/features/remote-cache?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
### Testing
|
||||||
- [Task distribution across multiple machines](https://nx.dev/ci/features/distribute-task-execution?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
- [Automated e2e test splitting](https://nx.dev/ci/features/split-e2e-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
- [Task flakiness detection and rerunning](https://nx.dev/ci/features/flaky-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
Use the following command to configure a CI workflow for your workspace:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx nx g ci-workflow
|
nx test <project> # unit tests (vitest)
|
||||||
|
nx e2e <app>-e2e # e2e tests (playwright)
|
||||||
|
nx lint <project> # eslint
|
||||||
```
|
```
|
||||||
|
|
||||||
[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
### Storybook
|
||||||
|
|
||||||
## Install Nx Console
|
```sh
|
||||||
|
nx storybook ui # run storybook for ui lib
|
||||||
|
nx build-storybook ui # build static storybook
|
||||||
|
```
|
||||||
|
|
||||||
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
|
## CI/CD
|
||||||
|
|
||||||
[Install Nx Console »](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
GitHub Actions pipeline (`.github/workflows/nix-build.yml`):
|
||||||
|
|
||||||
## Useful links
|
1. **detect** — uses `nx affected` to find changed apps
|
||||||
|
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)
|
||||||
|
|
||||||
Learn more:
|
## Tech Stack
|
||||||
|
|
||||||
- [Learn more about this workspace setup](https://nx.dev/getting-started/tutorials/react-monorepo-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
- **Monorepo**: Nx 22.6
|
||||||
- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
- **Frontend**: React 19, TypeScript
|
||||||
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
- **Styling**: Tailwind CSS v4, CVA
|
||||||
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
- **State**: Zustand, TanStack React Query
|
||||||
|
- **Forms**: react-hook-form + zod
|
||||||
|
- **Build**: Nix flakes, Cachix
|
||||||
|
- **CI**: GitHub Actions
|
||||||
|
|
||||||
And join the Nx community:
|
## Contributing
|
||||||
- [Discord](https://go.nx.dev/community)
|
|
||||||
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
|
1. Fork and clone the repository
|
||||||
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
|
2. Create a branch: `git checkout -b feat/feature-name`
|
||||||
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
3. Make changes, commit, and push
|
||||||
|
4. Open a pull request to the `develop` branch
|
||||||
|
|
||||||
|
Issues and feedback welcome via [GitHub Issues](https://github.com/IMPHNEN/imphnen-frontend-service/issues).
|
||||||
|
|||||||
+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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=
|
||||||
|
VITE_GITHUB_CLIENT_ID=
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
<!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>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
|
||||||
<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="/favicon.ico" />
|
<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>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -1,18 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
|
||||||
<BackofficeSidebar />
|
|
||||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] mx-auto">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
} 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';
|
|
||||||
|
|
||||||
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 Indonesia',
|
|
||||||
}));
|
|
||||||
|
|
||||||
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();
|
|
||||||
// handleEdit(row.id);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Edit
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-md 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>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
disabled
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
|
||||||
<BackofficeSidebar />
|
|
||||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] mx-auto">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { ArrowDownOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { FC, ReactElement } from 'react';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Dashboard Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-md 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]">
|
|
||||||
{/* Summary Section */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
|
||||||
Summary
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{/* Summary Cards */}
|
|
||||||
{[1, 2, 3, 4].map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
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">
|
|
||||||
<ArrowDownOutlined 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">
|
|
||||||
Total Participants
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Gacha Items 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">
|
|
||||||
<span>Tambah Item</span>
|
|
||||||
<PlusOutlined className="text-[16px]" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gacha Items List */}
|
|
||||||
<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 max-h-[80px] overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col py-4 px-6 gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-p3 text-primary-500 font-medium">
|
|
||||||
Lanyard IMPHNEN
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1">
|
|
||||||
<span>Prize {item}</span>
|
|
||||||
<span>Chance Rate: (0.1%)</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"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="sm"
|
|
||||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-red-500"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
|
|
||||||
<img
|
|
||||||
src="gacha-clip.png"
|
|
||||||
alt="Lanyard IMPHNEN"
|
|
||||||
className="h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right-side illustration */}
|
|
||||||
<img
|
|
||||||
src="gacha.png"
|
|
||||||
alt=""
|
|
||||||
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<main className="bg-primary-50 min-h-screen">
|
|
||||||
<Outlet />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { InputForm } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
|
||||||
<div className="bg-white border border-primary-200 shadow-lg p-[60px] text-center flex flex-col justify-items-stretch gap-8 rounded-2xl">
|
|
||||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
|
||||||
<h1 className="text-primary-500 text-p1 font-semibold">
|
|
||||||
Welcome to IMPHNEN Backoffice
|
|
||||||
</h1>
|
|
||||||
<InputForm
|
|
||||||
label="Email"
|
|
||||||
placeholder="Masukkan Email"
|
|
||||||
type="email"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<InputForm
|
|
||||||
label="Password"
|
|
||||||
placeholder="Masukkan password"
|
|
||||||
type="password"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<Button onClick={() => navigate('/dashboard')}>Login</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
|
||||||
<BackofficeSidebar />
|
|
||||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] mx-auto">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import {
|
|
||||||
FilterOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
AuditOutlined,
|
|
||||||
} 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';
|
|
||||||
|
|
||||||
type TransactionStatus = 'valid' | 'invalid' | 'unchecked';
|
|
||||||
|
|
||||||
interface Transaction {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
transactionNumber: string;
|
|
||||||
status: TransactionStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data for transactions
|
|
||||||
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
|
||||||
transactionNumber: '25D2133Y9AFYBD',
|
|
||||||
status: (i % 3 === 0
|
|
||||||
? 'invalid'
|
|
||||||
: i % 5 === 0
|
|
||||||
? 'unchecked'
|
|
||||||
: 'valid') as TransactionStatus,
|
|
||||||
}));
|
|
||||||
|
|
||||||
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: 'Nomor Transaksi',
|
|
||||||
accessorKey: 'transactionNumber',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Order Valid?',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<TransactionStatus, string> = {
|
|
||||||
valid: 'bg-success-500 text-white',
|
|
||||||
invalid: 'bg-danger-500 text-white',
|
|
||||||
unchecked: 'bg-yellow-400 text-black',
|
|
||||||
};
|
|
||||||
const statusText: Record<TransactionStatus, string> = {
|
|
||||||
valid: 'Valid',
|
|
||||||
invalid: 'Invalid',
|
|
||||||
unchecked: 'Unchecked',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-1 px-3 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// handleUpdate(row.id);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-full"
|
|
||||||
>
|
|
||||||
<AuditOutlined className="text-[16px]" /> Update
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockTransactions,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockTransactions.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white py-4 px-8 rounded-md shadow p-4">
|
|
||||||
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Account Table Section */}
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
{/* Search and Filter */}
|
|
||||||
<div className="flex justify-between items-center gap-8 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<FilterOutlined />
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<DataTable data={mockTransactions} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { FC, useState, useRef, useEffect } from 'react';
|
||||||
|
import { FilterOutlined } from '@ant-design/icons';
|
||||||
|
import INDONESIAN_CITIES from '../constants/cities';
|
||||||
|
|
||||||
|
interface CityFilterSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
allOptionLabel?: string;
|
||||||
|
filterIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CityFilterSelect: FC<CityFilterSelectProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className = '',
|
||||||
|
placeholder = 'Search cities...',
|
||||||
|
allOptionLabel = 'All Cities',
|
||||||
|
filterIcon = true,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const filteredCities = INDONESIAN_CITIES.filter((city) =>
|
||||||
|
city.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectCity = (city: string) => {
|
||||||
|
onChange(city);
|
||||||
|
setSearchQuery('');
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputClick = () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
onChange('all');
|
||||||
|
setSearchQuery('');
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayValue = value === 'all' ? allOptionLabel : value;
|
||||||
|
const showClearButton = value !== 'all' && !isOpen;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className}`} ref={dropdownRef}>
|
||||||
|
<div className="relative">
|
||||||
|
{filterIcon && (
|
||||||
|
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={isOpen ? searchQuery : displayValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
if (!isOpen) setIsOpen(true);
|
||||||
|
}}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
onFocus={handleInputClick}
|
||||||
|
placeholder={isOpen ? placeholder : displayValue}
|
||||||
|
className={`border border-neutral-200 rounded-lg pr-10 py-2.5 text-sm w-full focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer ${
|
||||||
|
filterIcon ? ' pl-10' : 'pl-3'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{showClearButton && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClearSelection();
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 text-xs cursor-pointer z-20"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute z-50 w-full mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
onClick={() => handleSelectCity('all')}
|
||||||
|
className={`px-3 py-2 cursor-pointer hover:bg-neutral-50 border-b border-neutral-100 ${
|
||||||
|
value === 'all'
|
||||||
|
? 'bg-primary-50 text-primary-700 font-medium'
|
||||||
|
: 'text-neutral-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{allOptionLabel}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredCities.length > 0 ? (
|
||||||
|
<div className="py-1">
|
||||||
|
{filteredCities.slice(0, 100).map((city) => (
|
||||||
|
<div
|
||||||
|
key={city}
|
||||||
|
onClick={() => handleSelectCity(city)}
|
||||||
|
className={`px-3 py-2 cursor-pointer hover:bg-neutral-50 text-sm ${
|
||||||
|
value === city
|
||||||
|
? 'bg-primary-50 text-primary-700 font-medium'
|
||||||
|
: 'text-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{city}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filteredCities.length > 100 && (
|
||||||
|
<div className="px-3 py-2 text-xs text-neutral-500 border-t border-neutral-100">
|
||||||
|
Showing first 100 results. Continue typing to refine...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : searchQuery ? (
|
||||||
|
<div className="px-3 py-2 text-neutral-500 text-sm">
|
||||||
|
No cities found matching "{searchQuery}"
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import type { Row, Table } from '@tanstack/react-table';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
Checkbox,
|
||||||
|
} from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
|
||||||
|
/** Renders a header checkbox that toggles all rows (with indeterminate support). */
|
||||||
|
export function SelectAllCheckbox<T>({ table }: { table: Table<T> }) {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsAllRowsSelected()
|
||||||
|
? true
|
||||||
|
: table.getIsSomeRowsSelected()
|
||||||
|
? 'indeterminate'
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
table.toggleAllRowsSelected(!!v && v !== 'indeterminate')
|
||||||
|
}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-row checkbox. */
|
||||||
|
export function RowSelectCheckbox<T>({ row }: { row: Row<T> }) {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(v) => row.toggleSelected(!!v)}
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteConfirmProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DeleteConfirmDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
title = 'Hapus data ini?',
|
||||||
|
description = 'Tindakan ini tidak dapat dibatalkan. Data akan dihapus permanen.',
|
||||||
|
}: DeleteConfirmProps) {
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Batal</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={onConfirm}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC, ReactElement, useState } from 'react';
|
||||||
|
import { useLocation, useNavigate } from '@tanstack/react-router';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Users,
|
||||||
|
UsersRound,
|
||||||
|
ClipboardCheck,
|
||||||
|
BookOpen,
|
||||||
|
MessageSquare,
|
||||||
|
MessageCircle,
|
||||||
|
Calendar,
|
||||||
|
CalendarClock,
|
||||||
|
Settings,
|
||||||
|
BarChart3,
|
||||||
|
RefreshCcw,
|
||||||
|
Inbox,
|
||||||
|
UserCog,
|
||||||
|
UserPlus,
|
||||||
|
ShieldCheck,
|
||||||
|
KeyRound,
|
||||||
|
User,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
} from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
type MenuLink = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuGroup = {
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
children: MenuLink[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuItem = MenuLink | MenuGroup;
|
||||||
|
|
||||||
|
const MENUS: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Hackathon',
|
||||||
|
icon: BarChart3,
|
||||||
|
children: [
|
||||||
|
{ label: 'Dashboard', href: '/hackathon-dashboard', icon: LayoutDashboard },
|
||||||
|
{ label: 'Users', href: '/hackathon-users', icon: Users },
|
||||||
|
{ label: 'Teams', href: '/hackathon-teams', icon: UsersRound },
|
||||||
|
{ label: 'Submissions', href: '/hackathon-submissions', icon: ClipboardCheck },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dimentorin',
|
||||||
|
icon: BookOpen,
|
||||||
|
children: [
|
||||||
|
{ label: 'Dashboard', href: '/dashboard-dimentorin', icon: LayoutDashboard },
|
||||||
|
{ label: 'Users', href: '/users-dimentorin', icon: UserCog },
|
||||||
|
{ label: 'Session', href: '/session-dimentorin', icon: CalendarClock },
|
||||||
|
{ label: 'Content & Roadmap', href: '/roadmap-dimentorin', icon: BookOpen },
|
||||||
|
{ label: 'Feedback & Review', href: '/feedback-review-dimentorin', icon: MessageSquare },
|
||||||
|
{ label: 'Settings', href: '/settings-dimentorin', icon: Settings },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gacha',
|
||||||
|
icon: RefreshCcw,
|
||||||
|
children: [
|
||||||
|
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
|
{ label: 'Gacha Roll', href: '/gacha-roll', icon: RefreshCcw },
|
||||||
|
{ label: 'Validasi Transaksi', href: '/transactions', icon: ClipboardCheck },
|
||||||
|
{ label: 'Data Pengiriman', href: '/prizes', icon: Inbox },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CMS',
|
||||||
|
icon: BookOpen,
|
||||||
|
children: [
|
||||||
|
{ label: 'Events', href: '/cms-events', icon: Calendar },
|
||||||
|
{ label: 'Testimonials', href: '/cms-testimonials', icon: MessageCircle },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const FLAT_MENUS: MenuLink[] = [
|
||||||
|
{ label: 'Permissions', href: '/permissions', icon: ShieldCheck },
|
||||||
|
{ label: 'Roles', href: '/roles', icon: KeyRound },
|
||||||
|
{ label: 'Data Akun', href: '/accounts', icon: User },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isMenuGroup = (item: MenuItem): item is MenuGroup =>
|
||||||
|
(item as MenuGroup).children !== undefined;
|
||||||
|
|
||||||
|
export const BackofficeSidebar: FC = (): ReactElement => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return location.pathname.includes(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>(() => {
|
||||||
|
const initial: Record<string, boolean> = {};
|
||||||
|
MENUS.forEach((menu) => {
|
||||||
|
if (
|
||||||
|
isMenuGroup(menu) &&
|
||||||
|
menu.children.some((child) => isActive(child.href))
|
||||||
|
) {
|
||||||
|
initial[menu.label] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleGroup = (label: string) =>
|
||||||
|
setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar collapsible="offcanvas" variant="inset">
|
||||||
|
<SidebarHeader>
|
||||||
|
<div className="flex items-center justify-center px-2 py-3">
|
||||||
|
<img
|
||||||
|
src="/logos/simple.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="h-10 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{MENUS.map((menu) => {
|
||||||
|
const GroupIcon = menu.icon;
|
||||||
|
const open = !!openGroups[menu.label];
|
||||||
|
const groupHasActive =
|
||||||
|
isMenuGroup(menu) &&
|
||||||
|
menu.children.some((c) => isActive(c.href));
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={menu.label}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
onClick={() => toggleGroup(menu.label)}
|
||||||
|
isActive={groupHasActive && !open}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<GroupIcon className="size-4" />
|
||||||
|
<span>{menu.label}</span>
|
||||||
|
</span>
|
||||||
|
{open ? (
|
||||||
|
<ChevronDown className="size-3.5 opacity-60" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="size-3.5 opacity-60" />
|
||||||
|
)}
|
||||||
|
</SidebarMenuButton>
|
||||||
|
{isMenuGroup(menu) && open && (
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{menu.children.map((child) => {
|
||||||
|
const ChildIcon = child.icon;
|
||||||
|
return (
|
||||||
|
<SidebarMenuSubItem key={child.href}>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
isActive={isActive(child.href)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
navigate({ to: child.href })
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-left',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChildIcon className="size-4" />
|
||||||
|
<span>{child.label}</span>
|
||||||
|
</button>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
)}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>System</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{FLAT_MENUS.map((menu) => {
|
||||||
|
const Icon = menu.icon;
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={menu.href}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
isActive={isActive(menu.href)}
|
||||||
|
onClick={() => navigate({ to: menu.href })}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
<span>{menu.label}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
const INDONESIAN_CITIES: string[] = [
|
||||||
|
'Aceh Selatan',
|
||||||
|
'Aceh Tenggara',
|
||||||
|
'Aceh Timur',
|
||||||
|
'Aceh Tengah',
|
||||||
|
'Aceh Barat',
|
||||||
|
'Aceh Besar',
|
||||||
|
'Pidie',
|
||||||
|
'Aceh Utara',
|
||||||
|
'Simeulue',
|
||||||
|
'Aceh Singkil',
|
||||||
|
'Bireuen',
|
||||||
|
'Aceh Barat Daya',
|
||||||
|
'Gayo Lues',
|
||||||
|
'Aceh Jaya',
|
||||||
|
'Nagan Raya',
|
||||||
|
'Aceh Tamiang',
|
||||||
|
'Bener Meriah',
|
||||||
|
'Pidie Jaya',
|
||||||
|
'Kota Banda Aceh',
|
||||||
|
'Kota Sabang',
|
||||||
|
'Kota Lhokseumawe',
|
||||||
|
'Kota Langsa',
|
||||||
|
'Kota Subulussalam',
|
||||||
|
'Tapanuli Tengah',
|
||||||
|
'Tapanuli Utara',
|
||||||
|
'Tapanuli Selatan',
|
||||||
|
'Nias',
|
||||||
|
'Langkat',
|
||||||
|
'Karo',
|
||||||
|
'Deli Serdang',
|
||||||
|
'Simalungun',
|
||||||
|
'Asahan',
|
||||||
|
'Labuhanbatu',
|
||||||
|
'Dairi',
|
||||||
|
'Toba',
|
||||||
|
'Mandailing Natal',
|
||||||
|
'Nias Selatan',
|
||||||
|
'Pakpak Bharat',
|
||||||
|
'Humbang Hasundutan',
|
||||||
|
'Samosir',
|
||||||
|
'Serdang Bedagai',
|
||||||
|
'Batu Bara',
|
||||||
|
'Padang Lawas Utara',
|
||||||
|
'Padang Lawas',
|
||||||
|
'Labuhanbatu Selatan',
|
||||||
|
'Labuhanbatu Utara',
|
||||||
|
'Nias Utara',
|
||||||
|
'Nias Barat',
|
||||||
|
'Kota Medan',
|
||||||
|
'Kota Pematangsiantar',
|
||||||
|
'Kota Sibolga',
|
||||||
|
'Kota Tanjung Balai',
|
||||||
|
'Kota Binjai',
|
||||||
|
'Kota Tebing Tinggi',
|
||||||
|
'Kota Padangsidimpuan',
|
||||||
|
'Kota Gunungsitoli',
|
||||||
|
'Pesisir Selatan',
|
||||||
|
'Solok',
|
||||||
|
'Sijunjung',
|
||||||
|
'Tanah Datar',
|
||||||
|
'Padang Pariaman',
|
||||||
|
'Agam',
|
||||||
|
'Lima Puluh Kota',
|
||||||
|
'Pasaman',
|
||||||
|
'Kepulauan Mentawai',
|
||||||
|
'Dharmasraya',
|
||||||
|
'Solok Selatan',
|
||||||
|
'Pasaman Barat',
|
||||||
|
'Kota Padang',
|
||||||
|
'Kota Solok',
|
||||||
|
'Kota Sawahlunto',
|
||||||
|
'Kota Padang Panjang',
|
||||||
|
'Kota Bukittinggi',
|
||||||
|
'Kota Payakumbuh',
|
||||||
|
'Kota Pariaman',
|
||||||
|
'Kampar',
|
||||||
|
'Indragiri Hulu',
|
||||||
|
'Bengkalis',
|
||||||
|
'Indragiri Hilir',
|
||||||
|
'Pelalawan',
|
||||||
|
'Rokan Hulu',
|
||||||
|
'Rokan Hilir',
|
||||||
|
'Siak',
|
||||||
|
'Kuantan Singingi',
|
||||||
|
'Kepulauan Meranti',
|
||||||
|
'Kota Pekanbaru',
|
||||||
|
'Kota Dumai',
|
||||||
|
'Kerinci',
|
||||||
|
'Merangin',
|
||||||
|
'Sarolangun',
|
||||||
|
'Batanghari',
|
||||||
|
'Muaro Jambi',
|
||||||
|
'Tanjung Jabung Barat',
|
||||||
|
'Tanjung Jabung Timur',
|
||||||
|
'Bungo',
|
||||||
|
'Tebo',
|
||||||
|
'Kota Jambi',
|
||||||
|
'Kota Sungai Penuh',
|
||||||
|
'Ogan Komering Ulu',
|
||||||
|
'Ogan Komering Ilir',
|
||||||
|
'Muara Enim',
|
||||||
|
'Lahat',
|
||||||
|
'Musi Rawas',
|
||||||
|
'Musi Banyuasin',
|
||||||
|
'Banyuasin',
|
||||||
|
'Ogan Komering Ulu Timur',
|
||||||
|
'Ogan Komering Ulu Selatan',
|
||||||
|
'Ogan Ilir',
|
||||||
|
'Empat Lawang',
|
||||||
|
'Penukal Abab Lematang Ilir',
|
||||||
|
'Musi Rawas Utara',
|
||||||
|
'Kota Palembang',
|
||||||
|
'Kota Pagar Alam',
|
||||||
|
'Kota Lubuk Linggau',
|
||||||
|
'Kota Prabumulih',
|
||||||
|
'Bengkulu Selatan',
|
||||||
|
'Rejang Lebong',
|
||||||
|
'Bengkulu Utara',
|
||||||
|
'Kaur',
|
||||||
|
'Seluma',
|
||||||
|
'Muko Muko',
|
||||||
|
'Lebong',
|
||||||
|
'Kepahiang',
|
||||||
|
'Bengkulu Tengah',
|
||||||
|
'Kota Bengkulu',
|
||||||
|
'Lampung Selatan',
|
||||||
|
'Lampung Tengah',
|
||||||
|
'Lampung Utara',
|
||||||
|
'Lampung Barat',
|
||||||
|
'Tulang Bawang',
|
||||||
|
'Tanggamus',
|
||||||
|
'Lampung Timur',
|
||||||
|
'Way Kanan',
|
||||||
|
'Pesawaran',
|
||||||
|
'Pringsewu',
|
||||||
|
'Mesuji',
|
||||||
|
'Tulang Bawang Barat',
|
||||||
|
'Pesisir Barat',
|
||||||
|
'Kota Bandar Lampung',
|
||||||
|
'Kota Metro',
|
||||||
|
'Bangka',
|
||||||
|
'Belitung',
|
||||||
|
'Bangka Selatan',
|
||||||
|
'Bangka Tengah',
|
||||||
|
'Bangka Barat',
|
||||||
|
'Belitung Timur',
|
||||||
|
'Kota Pangkal Pinang',
|
||||||
|
'Bintan',
|
||||||
|
'Karimun',
|
||||||
|
'Natuna',
|
||||||
|
'Lingga',
|
||||||
|
'Kepulauan Anambas',
|
||||||
|
'Kota Batam',
|
||||||
|
'Kota Tanjung Pinang',
|
||||||
|
'Kepulauan Seribu',
|
||||||
|
'Kota Jakarta Pusat',
|
||||||
|
'Kota Jakarta Utara',
|
||||||
|
'Kota Jakarta Barat',
|
||||||
|
'Kota Jakarta Selatan',
|
||||||
|
'Kota Jakarta Timur',
|
||||||
|
'Bogor',
|
||||||
|
'Sukabumi',
|
||||||
|
'Cianjur',
|
||||||
|
'Bandung',
|
||||||
|
'Garut',
|
||||||
|
'Tasikmalaya',
|
||||||
|
'Ciamis',
|
||||||
|
'Kuningan',
|
||||||
|
'Cirebon',
|
||||||
|
'Majalengka',
|
||||||
|
'Sumedang',
|
||||||
|
'Indramayu',
|
||||||
|
'Subang',
|
||||||
|
'Purwakarta',
|
||||||
|
'Karawang',
|
||||||
|
'Bekasi',
|
||||||
|
'Bandung Barat',
|
||||||
|
'Pangandaran',
|
||||||
|
'Kota Bogor',
|
||||||
|
'Kota Sukabumi',
|
||||||
|
'Kota Bandung',
|
||||||
|
'Kota Cirebon',
|
||||||
|
'Kota Bekasi',
|
||||||
|
'Kota Depok',
|
||||||
|
'Kota Cimahi',
|
||||||
|
'Kota Tasikmalaya',
|
||||||
|
'Kota Banjar',
|
||||||
|
'Cilacap',
|
||||||
|
'Banyumas',
|
||||||
|
'Purbalingga',
|
||||||
|
'Banjarnegara',
|
||||||
|
'Kebumen',
|
||||||
|
'Purworejo',
|
||||||
|
'Wonosobo',
|
||||||
|
'Magelang',
|
||||||
|
'Boyolali',
|
||||||
|
'Klaten',
|
||||||
|
'Sukoharjo',
|
||||||
|
'Wonogiri',
|
||||||
|
'Karanganyar',
|
||||||
|
'Sragen',
|
||||||
|
'Grobogan',
|
||||||
|
'Blora',
|
||||||
|
'Rembang',
|
||||||
|
'Pati',
|
||||||
|
'Kudus',
|
||||||
|
'Jepara',
|
||||||
|
'Demak',
|
||||||
|
'Semarang',
|
||||||
|
'Temanggung',
|
||||||
|
'Kendal',
|
||||||
|
'Batang',
|
||||||
|
'Pekalongan',
|
||||||
|
'Pemalang',
|
||||||
|
'Tegal',
|
||||||
|
'Brebes',
|
||||||
|
'Kota Magelang',
|
||||||
|
'Kota Surakarta',
|
||||||
|
'Kota Salatiga',
|
||||||
|
'Kota Semarang',
|
||||||
|
'Kota Pekalongan',
|
||||||
|
'Kota Tegal',
|
||||||
|
'Kulon Progo',
|
||||||
|
'Bantul',
|
||||||
|
'Gunungkidul',
|
||||||
|
'Sleman',
|
||||||
|
'Kota Yogyakarta',
|
||||||
|
'Pacitan',
|
||||||
|
'Ponorogo',
|
||||||
|
'Trenggalek',
|
||||||
|
'Tulungagung',
|
||||||
|
'Blitar',
|
||||||
|
'Kediri',
|
||||||
|
'Malang',
|
||||||
|
'Lumajang',
|
||||||
|
'Jember',
|
||||||
|
'Banyuwangi',
|
||||||
|
'Bondowoso',
|
||||||
|
'Situbondo',
|
||||||
|
'Probolinggo',
|
||||||
|
'Pasuruan',
|
||||||
|
'Sidoarjo',
|
||||||
|
'Mojokerto',
|
||||||
|
'Jombang',
|
||||||
|
'Nganjuk',
|
||||||
|
'Madiun',
|
||||||
|
'Magetan',
|
||||||
|
'Ngawi',
|
||||||
|
'Bojonegoro',
|
||||||
|
'Tuban',
|
||||||
|
'Lamongan',
|
||||||
|
'Gresik',
|
||||||
|
'Bangkalan',
|
||||||
|
'Sampang',
|
||||||
|
'Pamekasan',
|
||||||
|
'Sumenep',
|
||||||
|
'Kota Kediri',
|
||||||
|
'Kota Blitar',
|
||||||
|
'Kota Malang',
|
||||||
|
'Kota Probolinggo',
|
||||||
|
'Kota Pasuruan',
|
||||||
|
'Kota Mojokerto',
|
||||||
|
'Kota Madiun',
|
||||||
|
'Kota Surabaya',
|
||||||
|
'Kota Batu',
|
||||||
|
'Pandeglang',
|
||||||
|
'Lebak',
|
||||||
|
'Tangerang',
|
||||||
|
'Serang',
|
||||||
|
'Kota Tangerang',
|
||||||
|
'Kota Cilegon',
|
||||||
|
'Kota Serang',
|
||||||
|
'Kota Tangerang Selatan',
|
||||||
|
'Jembrana',
|
||||||
|
'Tabanan',
|
||||||
|
'Badung',
|
||||||
|
'Gianyar',
|
||||||
|
'Klungkung',
|
||||||
|
'Bangli',
|
||||||
|
'Karangasem',
|
||||||
|
'Buleleng',
|
||||||
|
'Kota Denpasar',
|
||||||
|
'Lombok Barat',
|
||||||
|
'Lombok Tengah',
|
||||||
|
'Lombok Timur',
|
||||||
|
'Sumbawa',
|
||||||
|
'Dompu',
|
||||||
|
'Bima',
|
||||||
|
'Sumbawa Barat',
|
||||||
|
'Lombok Utara',
|
||||||
|
'Kota Mataram',
|
||||||
|
'Kota Bima',
|
||||||
|
'Kupang',
|
||||||
|
'Timor Tengah Selatan',
|
||||||
|
'Timor Tengah Utara',
|
||||||
|
'Belu',
|
||||||
|
'Alor',
|
||||||
|
'Flores Timur',
|
||||||
|
'Sikka',
|
||||||
|
'Ende',
|
||||||
|
'Ngada',
|
||||||
|
'Manggarai',
|
||||||
|
'Sumba Timur',
|
||||||
|
'Sumba Barat',
|
||||||
|
'Lembata',
|
||||||
|
'Rote Ndao',
|
||||||
|
'Manggarai Barat',
|
||||||
|
'Nagekeo',
|
||||||
|
'Sumba Tengah',
|
||||||
|
'Sumba Barat Daya',
|
||||||
|
'Manggarai Timur',
|
||||||
|
'Sabu Raijua',
|
||||||
|
'Malaka',
|
||||||
|
'Kota Kupang',
|
||||||
|
'Sambas',
|
||||||
|
'Mempawah',
|
||||||
|
'Sanggau',
|
||||||
|
'Ketapang',
|
||||||
|
'Sintang',
|
||||||
|
'Kapuas Hulu',
|
||||||
|
'Bengkayang',
|
||||||
|
'Landak',
|
||||||
|
'Sekadau',
|
||||||
|
'Melawi',
|
||||||
|
'Kayong Utara',
|
||||||
|
'Kubu Raya',
|
||||||
|
'Kota Pontianak',
|
||||||
|
'Kota Singkawang',
|
||||||
|
'Kotawaringin Barat',
|
||||||
|
'Kotawaringin Timur',
|
||||||
|
'Kapuas',
|
||||||
|
'Barito Selatan',
|
||||||
|
'Barito Utara',
|
||||||
|
'Katingan',
|
||||||
|
'Seruyan',
|
||||||
|
'Sukamara',
|
||||||
|
'Lamandau',
|
||||||
|
'Gunung Mas',
|
||||||
|
'Pulang Pisau',
|
||||||
|
'Murung Raya',
|
||||||
|
'Barito Timur',
|
||||||
|
'Kota Palangkaraya',
|
||||||
|
'Tanah Laut',
|
||||||
|
'Kotabaru',
|
||||||
|
'Banjar',
|
||||||
|
'Barito Kuala',
|
||||||
|
'Tapin',
|
||||||
|
'Hulu Sungai Selatan',
|
||||||
|
'Hulu Sungai Tengah',
|
||||||
|
'Hulu Sungai Utara',
|
||||||
|
'Tabalong',
|
||||||
|
'Tanah Bumbu',
|
||||||
|
'Balangan',
|
||||||
|
'Kota Banjarmasin',
|
||||||
|
'Kota Banjarbaru',
|
||||||
|
'Paser',
|
||||||
|
'Kutai Kartanegara',
|
||||||
|
'Berau',
|
||||||
|
'Kutai Barat',
|
||||||
|
'Kutai Timur',
|
||||||
|
'Penajam Paser Utara',
|
||||||
|
'Mahakam Ulu',
|
||||||
|
'Kota Balikpapan',
|
||||||
|
'Kota Samarinda',
|
||||||
|
'Kota Bontang',
|
||||||
|
'Bulungan',
|
||||||
|
'Malinau',
|
||||||
|
'Nunukan',
|
||||||
|
'Tana Tidung',
|
||||||
|
'Kota Tarakan',
|
||||||
|
'Bolaang Mongondow',
|
||||||
|
'Minahasa',
|
||||||
|
'Kepulauan Sangihe',
|
||||||
|
'Kepulauan Talaud',
|
||||||
|
'Minahasa Selatan',
|
||||||
|
'Minahasa Utara',
|
||||||
|
'Minahasa Tenggara',
|
||||||
|
'Bolaang Mongondow Utara',
|
||||||
|
'Kepulauan Siau Tagulandang Biaro (Sitaro)',
|
||||||
|
'Bolaang Mongondow Timur',
|
||||||
|
'Bolaang Mongondow Selatan',
|
||||||
|
'Kota Manado',
|
||||||
|
'Kota Bitung',
|
||||||
|
'Kota Tomohon',
|
||||||
|
'Kota Kotamobagu',
|
||||||
|
'Banggai',
|
||||||
|
'Poso',
|
||||||
|
'Donggala',
|
||||||
|
'Toli Toli',
|
||||||
|
'Buol',
|
||||||
|
'Morowali',
|
||||||
|
'Banggai Kepulauan',
|
||||||
|
'Parigi Moutong',
|
||||||
|
'Tojo Una Una',
|
||||||
|
'Sigi',
|
||||||
|
'Banggai Laut',
|
||||||
|
'Morowali Utara',
|
||||||
|
'Kota Palu',
|
||||||
|
'Kepulauan Selayar',
|
||||||
|
'Bulukumba',
|
||||||
|
'Bantaeng',
|
||||||
|
'Jeneponto',
|
||||||
|
'Takalar',
|
||||||
|
'Gowa',
|
||||||
|
'Sinjai',
|
||||||
|
'Bone',
|
||||||
|
'Maros',
|
||||||
|
'Pangkajene Kepulauan',
|
||||||
|
'Barru',
|
||||||
|
'Soppeng',
|
||||||
|
'Wajo',
|
||||||
|
'Sidenreng Rappang',
|
||||||
|
'Pinrang',
|
||||||
|
'Enrekang',
|
||||||
|
'Luwu',
|
||||||
|
'Tana Toraja',
|
||||||
|
'Luwu Utara',
|
||||||
|
'Luwu Timur',
|
||||||
|
'Toraja Utara',
|
||||||
|
'Kota Makassar',
|
||||||
|
'Kota Pare Pare',
|
||||||
|
'Kota Palopo',
|
||||||
|
'Kolaka',
|
||||||
|
'Konawe',
|
||||||
|
'Muna',
|
||||||
|
'Buton',
|
||||||
|
'Konawe Selatan',
|
||||||
|
'Bombana',
|
||||||
|
'Wakatobi',
|
||||||
|
'Kolaka Utara',
|
||||||
|
'Konawe Utara',
|
||||||
|
'Buton Utara',
|
||||||
|
'Kolaka Timur',
|
||||||
|
'Konawe Kepulauan',
|
||||||
|
'Muna Barat',
|
||||||
|
'Buton Tengah',
|
||||||
|
'Buton Selatan',
|
||||||
|
'Kota Kendari',
|
||||||
|
'Kota Bau Bau',
|
||||||
|
'Gorontalo',
|
||||||
|
'Boalemo',
|
||||||
|
'Bone Bolango',
|
||||||
|
'Pahuwato',
|
||||||
|
'Gorontalo Utara',
|
||||||
|
'Kota Gorontalo',
|
||||||
|
'Pasangkayu (Mamuju Utara)',
|
||||||
|
'Mamuju',
|
||||||
|
'Mamasa',
|
||||||
|
'Polewali Mandar',
|
||||||
|
'Majene',
|
||||||
|
'Mamuju Tengah',
|
||||||
|
'Maluku Tengah',
|
||||||
|
'Maluku Tenggara',
|
||||||
|
'Kepulauan Tanimbar (Maluku Tenggara Barat)',
|
||||||
|
'Buru',
|
||||||
|
'Seram Bagian Timur',
|
||||||
|
'Seram Bagian Barat',
|
||||||
|
'Kepulauan Aru',
|
||||||
|
'Maluku Barat Daya',
|
||||||
|
'Buru Selatan',
|
||||||
|
'Kota Ambon',
|
||||||
|
'Kota Tual',
|
||||||
|
'Halmahera Barat',
|
||||||
|
'Halmahera Tengah',
|
||||||
|
'Halmahera Utara',
|
||||||
|
'Halmahera Selatan',
|
||||||
|
'Kepulauan Sula',
|
||||||
|
'Halmahera Timur',
|
||||||
|
'Pulau Morotai',
|
||||||
|
'Pulau Taliabu',
|
||||||
|
'Kota Ternate',
|
||||||
|
'Kota Tidore Kepulauan',
|
||||||
|
'Jayapura',
|
||||||
|
'Kepulauan Yapen',
|
||||||
|
'Biak Numfor',
|
||||||
|
'Sarmi',
|
||||||
|
'Keerom',
|
||||||
|
'Waropen',
|
||||||
|
'Supiori',
|
||||||
|
'Mamberamo Raya',
|
||||||
|
'Kota Jayapura',
|
||||||
|
'Manokwari',
|
||||||
|
'Fak Fak',
|
||||||
|
'Teluk Bintuni',
|
||||||
|
'Teluk Wondama',
|
||||||
|
'Kaimana',
|
||||||
|
'Manokwari Selatan',
|
||||||
|
'Pegunungan Arfak',
|
||||||
|
'Merauke',
|
||||||
|
'Boven Digoel',
|
||||||
|
'Mappi',
|
||||||
|
'Asmat',
|
||||||
|
'Nabire',
|
||||||
|
'Puncak Jaya',
|
||||||
|
'Paniai',
|
||||||
|
'Mimika',
|
||||||
|
'Puncak',
|
||||||
|
'Dogiyai',
|
||||||
|
'Intan Jaya',
|
||||||
|
'Deiyai',
|
||||||
|
'Jayawijaya',
|
||||||
|
'Pegunungan Bintang',
|
||||||
|
'Yahukimo',
|
||||||
|
'Tolikara',
|
||||||
|
'Mamberamo Tengah',
|
||||||
|
'Yalimo',
|
||||||
|
'Lanny Jaya',
|
||||||
|
'Nduga',
|
||||||
|
'Sorong',
|
||||||
|
'Sorong Selatan',
|
||||||
|
'Raja Ampat',
|
||||||
|
'Tambrauw',
|
||||||
|
'Maybrat',
|
||||||
|
'Kota Sorong',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default INDONESIAN_CITIES;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface UseQueryStateOptions {
|
||||||
|
defaultValue: number;
|
||||||
|
maxValue?: number;
|
||||||
|
minValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useQueryState = (key: string, options: UseQueryStateOptions) => {
|
||||||
|
const { defaultValue, maxValue = Infinity, minValue = 1 } = options;
|
||||||
|
|
||||||
|
const getQueryParam = (param: string): string | null => {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
return searchParams.get(param);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setQueryParam = (param: string, value: string) => {
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
searchParams.set(param, value);
|
||||||
|
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
|
||||||
|
window.history.replaceState(null, '', newUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValue = () => {
|
||||||
|
const queryValue = getQueryParam(key);
|
||||||
|
const parsedValue = queryValue ? parseInt(queryValue, 10) : defaultValue;
|
||||||
|
return Math.max(minValue, Math.min(maxValue, parsedValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [value, setValue] = useState<number>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
const queryValue = getQueryParam(key);
|
||||||
|
const newValue = queryValue ? parseInt(queryValue, 10) : defaultValue;
|
||||||
|
setValue(Math.max(minValue, Math.min(maxValue, newValue)));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
|
}, [key, defaultValue, minValue, maxValue]);
|
||||||
|
|
||||||
|
const updateValue = useCallback(
|
||||||
|
(newValue: number) => {
|
||||||
|
const constrainedValue = Math.max(minValue, Math.min(maxValue, newValue));
|
||||||
|
setValue(constrainedValue);
|
||||||
|
setQueryParam(key, constrainedValue.toString());
|
||||||
|
},
|
||||||
|
[key, minValue, maxValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextStep = useCallback(() => {
|
||||||
|
updateValue(value + 1);
|
||||||
|
}, [value, updateValue]);
|
||||||
|
|
||||||
|
const prevStep = useCallback(() => {
|
||||||
|
updateValue(value - 1);
|
||||||
|
}, [value, updateValue]);
|
||||||
|
|
||||||
|
const resetStep = useCallback(() => {
|
||||||
|
updateValue(defaultValue);
|
||||||
|
}, [defaultValue, updateValue]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: value,
|
||||||
|
nextStep,
|
||||||
|
prevStep,
|
||||||
|
resetStep,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
@source "../../../libs/ui/**/*.{ts,tsx}";
|
@source "../../../libs/ui/**/*.{ts,tsx}";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
@@ -17,14 +18,14 @@
|
|||||||
|
|
||||||
--color-neutral-50: #f6f6f6;
|
--color-neutral-50: #f6f6f6;
|
||||||
--color-neutral-100: #e7e7e7;
|
--color-neutral-100: #e7e7e7;
|
||||||
--color-neutral-200: #d0d0d0;
|
--color-neutral-200: #d1d1d1;
|
||||||
--color-neutral-300: #b7b7b7;
|
--color-neutral-300: #b0b0b0;
|
||||||
--color-neutral-400: #9f9f9f;
|
--color-neutral-400: #888888;
|
||||||
--color-neutral-500: #7e7e7e;
|
--color-neutral-500: #6d6d6d;
|
||||||
--color-neutral-600: #5d5d5d;
|
--color-neutral-600: #5d5d5d;
|
||||||
--color-neutral-700: #4a4a4a;
|
--color-neutral-700: #4f4f4f;
|
||||||
--color-neutral-800: #3a3a3a;
|
--color-neutral-800: #454545;
|
||||||
--color-neutral-900: #2f2f2f;
|
--color-neutral-900: #3d3d3d;
|
||||||
--color-neutral-950: #2b2b2b;
|
--color-neutral-950: #2b2b2b;
|
||||||
|
|
||||||
--color-success-100: #e0fbd8;
|
--color-success-100: #e0fbd8;
|
||||||
@@ -96,6 +97,45 @@
|
|||||||
/* ~10px */
|
/* ~10px */
|
||||||
--text-label3: 0.677rem;
|
--text-label3: 0.677rem;
|
||||||
/* ~8px */
|
/* ~8px */
|
||||||
|
|
||||||
|
/* shadcn/ui semantic tokens — aliased to existing palette (keep colors consistent) */
|
||||||
|
--color-background: #ffffff;
|
||||||
|
--color-foreground: #3d3d3d;
|
||||||
|
--color-card: #ffffff;
|
||||||
|
--color-card-foreground: #3d3d3d;
|
||||||
|
--color-popover: #ffffff;
|
||||||
|
--color-popover-foreground: #3d3d3d;
|
||||||
|
--color-primary: #23a1eb;
|
||||||
|
--color-primary-foreground: #ffffff;
|
||||||
|
--color-secondary: #e7e7e7;
|
||||||
|
--color-secondary-foreground: #3d3d3d;
|
||||||
|
--color-muted: #f6f6f6;
|
||||||
|
--color-muted-foreground: #6d6d6d;
|
||||||
|
--color-accent: #e1f0fd;
|
||||||
|
--color-accent-foreground: #085f9c;
|
||||||
|
--color-destructive: #ff5242;
|
||||||
|
--color-destructive-foreground: #ffffff;
|
||||||
|
--color-border: #d1d1d1;
|
||||||
|
--color-input: #d1d1d1;
|
||||||
|
--color-ring: #3eb0f2;
|
||||||
|
|
||||||
|
--color-sidebar: #ffffff;
|
||||||
|
--color-sidebar-foreground: #4f4f4f;
|
||||||
|
--color-sidebar-primary: #23a1eb;
|
||||||
|
--color-sidebar-primary-foreground: #ffffff;
|
||||||
|
--color-sidebar-accent: #f0f8ff;
|
||||||
|
--color-sidebar-accent-foreground: #085f9c;
|
||||||
|
--color-sidebar-border: #e7e7e7;
|
||||||
|
--color-sidebar-ring: #3eb0f2;
|
||||||
|
|
||||||
|
--color-chart-1: #23a1eb;
|
||||||
|
--color-chart-2: #35ba43;
|
||||||
|
--color-chart-3: #04acf3;
|
||||||
|
--color-chart-4: #ffed27;
|
||||||
|
--color-chart-5: #ff5242;
|
||||||
|
|
||||||
|
--radius: 8px;
|
||||||
|
--font-sans: 'Bai Jamjuree', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -130,7 +170,7 @@
|
|||||||
html {
|
html {
|
||||||
font-family: 'Bai Jamjuree', sans-serif;
|
font-family: 'Bai Jamjuree', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,25 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client'
|
||||||
import { middleware } from './middleware';
|
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||||
import { StrictMode } from 'react';
|
import { QueryProvider } from '@imphnen-frontend-service/utils'
|
||||||
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
|
import { Toaster } from 'sonner'
|
||||||
import {
|
import { routeTree } from './routeTree.gen'
|
||||||
add404PageToRoutesChildren,
|
import './index.css'
|
||||||
addErrorElementToRoutes,
|
|
||||||
convertPagesToRoute,
|
|
||||||
QueryProvider,
|
|
||||||
} from '@imphnen-frontend-service/utils';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
const router = createRouter({ routeTree })
|
||||||
const errorFiles = import.meta.glob('./app/**/*error.tsx');
|
|
||||||
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
|
|
||||||
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
|
|
||||||
|
|
||||||
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
|
declare module '@tanstack/react-router' {
|
||||||
addErrorElementToRoutes(errorFiles, routes);
|
interface Register {
|
||||||
add404PageToRoutesChildren(notFoundFiles, routes);
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const rootElement = document.getElementById('root')
|
||||||
{
|
|
||||||
...routes,
|
|
||||||
loader: middleware,
|
|
||||||
shouldRevalidate: () => true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
if (!rootElement) throw new Error('Failed to find the root element')
|
||||||
|
|
||||||
if (!rootElement) throw new Error('Failed to find the root element');
|
|
||||||
|
|
||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<QueryProvider>
|
||||||
<QueryProvider>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
<Toaster position="top-right" />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</StrictMode>
|
)
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { LoaderFunctionArgs } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
const pathname = url.pathname;
|
|
||||||
|
|
||||||
if (pathname) return null;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,923 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as PublicRouteImport } from './routes/_public'
|
||||||
|
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
||||||
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as AuthenticatedUsersDimentorinRouteImport } from './routes/_authenticated/users-dimentorin'
|
||||||
|
import { Route as AuthenticatedTransactionsRouteImport } from './routes/_authenticated/transactions'
|
||||||
|
import { Route as AuthenticatedSettingsDimentorinRouteImport } from './routes/_authenticated/settings-dimentorin'
|
||||||
|
import { Route as AuthenticatedSessionDimentorinRouteImport } from './routes/_authenticated/session-dimentorin'
|
||||||
|
import { Route as AuthenticatedRolesRouteImport } from './routes/_authenticated/roles'
|
||||||
|
import { Route as AuthenticatedRoadmapDimentorinRouteImport } from './routes/_authenticated/roadmap-dimentorin'
|
||||||
|
import { Route as AuthenticatedPrizesRouteImport } from './routes/_authenticated/prizes'
|
||||||
|
import { Route as AuthenticatedPermissionsRouteImport } from './routes/_authenticated/permissions'
|
||||||
|
import { Route as AuthenticatedHackathonUsersRouteImport } from './routes/_authenticated/hackathon-users'
|
||||||
|
import { Route as AuthenticatedHackathonTeamsRouteImport } from './routes/_authenticated/hackathon-teams'
|
||||||
|
import { Route as AuthenticatedHackathonSubmissionsRouteImport } from './routes/_authenticated/hackathon-submissions'
|
||||||
|
import { Route as AuthenticatedHackathonDashboardRouteImport } from './routes/_authenticated/hackathon-dashboard'
|
||||||
|
import { Route as AuthenticatedGachaRollRouteImport } from './routes/_authenticated/gacha-roll'
|
||||||
|
import { Route as AuthenticatedFeedbackReviewDimentorinRouteImport } from './routes/_authenticated/feedback-review-dimentorin'
|
||||||
|
import { Route as AuthenticatedDashboardDimentorinRouteImport } from './routes/_authenticated/dashboard-dimentorin'
|
||||||
|
import { Route as AuthenticatedDashboardRouteImport } from './routes/_authenticated/dashboard'
|
||||||
|
import { Route as AuthenticatedCmsTestimonialsRouteImport } from './routes/_authenticated/cms-testimonials'
|
||||||
|
import { Route as AuthenticatedCmsEventsRouteImport } from './routes/_authenticated/cms-events'
|
||||||
|
import { Route as AuthenticatedAccountsRouteImport } from './routes/_authenticated/accounts'
|
||||||
|
import { Route as PublicAuthLoginRouteImport } from './routes/_public/auth/login'
|
||||||
|
import { Route as AuthenticatedUsersDimentorinIdRouteImport } from './routes/_authenticated/users-dimentorin_/$id'
|
||||||
|
import { Route as AuthenticatedSessionDimentorinIdRouteImport } from './routes/_authenticated/session-dimentorin_/$id'
|
||||||
|
import { Route as AuthenticatedRolesCreateRouteImport } from './routes/_authenticated/roles_/create'
|
||||||
|
import { Route as AuthenticatedRolesIdRouteImport } from './routes/_authenticated/roles_/$id'
|
||||||
|
import { Route as AuthenticatedRoadmapDimentorinCreateRouteImport } from './routes/_authenticated/roadmap-dimentorin_/create'
|
||||||
|
import { Route as AuthenticatedRoadmapDimentorinIdRouteImport } from './routes/_authenticated/roadmap-dimentorin_/$id'
|
||||||
|
import { Route as AuthenticatedPermissionsCreateRouteImport } from './routes/_authenticated/permissions_/create'
|
||||||
|
import { Route as AuthenticatedPermissionsIdRouteImport } from './routes/_authenticated/permissions_/$id'
|
||||||
|
import { Route as AuthenticatedGachaRollCreateRouteImport } from './routes/_authenticated/gacha-roll_/create'
|
||||||
|
import { Route as AuthenticatedGachaRollIdRouteImport } from './routes/_authenticated/gacha-roll_/$id'
|
||||||
|
import { Route as AuthenticatedDashboardCreateRouteImport } from './routes/_authenticated/dashboard_/create'
|
||||||
|
import { Route as AuthenticatedDashboardIdRouteImport } from './routes/_authenticated/dashboard_/$id'
|
||||||
|
import { Route as AuthenticatedCmsTestimonialsCreateRouteImport } from './routes/_authenticated/cms-testimonials_/create'
|
||||||
|
import { Route as AuthenticatedCmsTestimonialsIdRouteImport } from './routes/_authenticated/cms-testimonials_/$id'
|
||||||
|
import { Route as AuthenticatedCmsEventsCreateRouteImport } from './routes/_authenticated/cms-events_/create'
|
||||||
|
import { Route as AuthenticatedCmsEventsIdRouteImport } from './routes/_authenticated/cms-events_/$id'
|
||||||
|
import { Route as AuthenticatedAccountsIdRouteImport } from './routes/_authenticated/accounts_/$id'
|
||||||
|
|
||||||
|
const PublicRoute = PublicRouteImport.update({
|
||||||
|
id: '/_public',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRoute = AuthenticatedRouteImport.update({
|
||||||
|
id: '/_authenticated',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedUsersDimentorinRoute =
|
||||||
|
AuthenticatedUsersDimentorinRouteImport.update({
|
||||||
|
id: '/users-dimentorin',
|
||||||
|
path: '/users-dimentorin',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedTransactionsRoute =
|
||||||
|
AuthenticatedTransactionsRouteImport.update({
|
||||||
|
id: '/transactions',
|
||||||
|
path: '/transactions',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedSettingsDimentorinRoute =
|
||||||
|
AuthenticatedSettingsDimentorinRouteImport.update({
|
||||||
|
id: '/settings-dimentorin',
|
||||||
|
path: '/settings-dimentorin',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedSessionDimentorinRoute =
|
||||||
|
AuthenticatedSessionDimentorinRouteImport.update({
|
||||||
|
id: '/session-dimentorin',
|
||||||
|
path: '/session-dimentorin',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRolesRoute = AuthenticatedRolesRouteImport.update({
|
||||||
|
id: '/roles',
|
||||||
|
path: '/roles',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRoadmapDimentorinRoute =
|
||||||
|
AuthenticatedRoadmapDimentorinRouteImport.update({
|
||||||
|
id: '/roadmap-dimentorin',
|
||||||
|
path: '/roadmap-dimentorin',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedPrizesRoute = AuthenticatedPrizesRouteImport.update({
|
||||||
|
id: '/prizes',
|
||||||
|
path: '/prizes',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedPermissionsRoute =
|
||||||
|
AuthenticatedPermissionsRouteImport.update({
|
||||||
|
id: '/permissions',
|
||||||
|
path: '/permissions',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedHackathonUsersRoute =
|
||||||
|
AuthenticatedHackathonUsersRouteImport.update({
|
||||||
|
id: '/hackathon-users',
|
||||||
|
path: '/hackathon-users',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedHackathonTeamsRoute =
|
||||||
|
AuthenticatedHackathonTeamsRouteImport.update({
|
||||||
|
id: '/hackathon-teams',
|
||||||
|
path: '/hackathon-teams',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedHackathonSubmissionsRoute =
|
||||||
|
AuthenticatedHackathonSubmissionsRouteImport.update({
|
||||||
|
id: '/hackathon-submissions',
|
||||||
|
path: '/hackathon-submissions',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedHackathonDashboardRoute =
|
||||||
|
AuthenticatedHackathonDashboardRouteImport.update({
|
||||||
|
id: '/hackathon-dashboard',
|
||||||
|
path: '/hackathon-dashboard',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedGachaRollRoute = AuthenticatedGachaRollRouteImport.update({
|
||||||
|
id: '/gacha-roll',
|
||||||
|
path: '/gacha-roll',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedFeedbackReviewDimentorinRoute =
|
||||||
|
AuthenticatedFeedbackReviewDimentorinRouteImport.update({
|
||||||
|
id: '/feedback-review-dimentorin',
|
||||||
|
path: '/feedback-review-dimentorin',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedDashboardDimentorinRoute =
|
||||||
|
AuthenticatedDashboardDimentorinRouteImport.update({
|
||||||
|
id: '/dashboard-dimentorin',
|
||||||
|
path: '/dashboard-dimentorin',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedDashboardRoute = AuthenticatedDashboardRouteImport.update({
|
||||||
|
id: '/dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedCmsTestimonialsRoute =
|
||||||
|
AuthenticatedCmsTestimonialsRouteImport.update({
|
||||||
|
id: '/cms-testimonials',
|
||||||
|
path: '/cms-testimonials',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedCmsEventsRoute = AuthenticatedCmsEventsRouteImport.update({
|
||||||
|
id: '/cms-events',
|
||||||
|
path: '/cms-events',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedAccountsRoute = AuthenticatedAccountsRouteImport.update({
|
||||||
|
id: '/accounts',
|
||||||
|
path: '/accounts',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const PublicAuthLoginRoute = PublicAuthLoginRouteImport.update({
|
||||||
|
id: '/auth/login',
|
||||||
|
path: '/auth/login',
|
||||||
|
getParentRoute: () => PublicRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedUsersDimentorinIdRoute =
|
||||||
|
AuthenticatedUsersDimentorinIdRouteImport.update({
|
||||||
|
id: '/users-dimentorin_/$id',
|
||||||
|
path: '/users-dimentorin/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedSessionDimentorinIdRoute =
|
||||||
|
AuthenticatedSessionDimentorinIdRouteImport.update({
|
||||||
|
id: '/session-dimentorin_/$id',
|
||||||
|
path: '/session-dimentorin/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRolesCreateRoute =
|
||||||
|
AuthenticatedRolesCreateRouteImport.update({
|
||||||
|
id: '/roles_/create',
|
||||||
|
path: '/roles/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRolesIdRoute = AuthenticatedRolesIdRouteImport.update({
|
||||||
|
id: '/roles_/$id',
|
||||||
|
path: '/roles/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRoadmapDimentorinCreateRoute =
|
||||||
|
AuthenticatedRoadmapDimentorinCreateRouteImport.update({
|
||||||
|
id: '/roadmap-dimentorin_/create',
|
||||||
|
path: '/roadmap-dimentorin/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedRoadmapDimentorinIdRoute =
|
||||||
|
AuthenticatedRoadmapDimentorinIdRouteImport.update({
|
||||||
|
id: '/roadmap-dimentorin_/$id',
|
||||||
|
path: '/roadmap-dimentorin/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedPermissionsCreateRoute =
|
||||||
|
AuthenticatedPermissionsCreateRouteImport.update({
|
||||||
|
id: '/permissions_/create',
|
||||||
|
path: '/permissions/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedPermissionsIdRoute =
|
||||||
|
AuthenticatedPermissionsIdRouteImport.update({
|
||||||
|
id: '/permissions_/$id',
|
||||||
|
path: '/permissions/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedGachaRollCreateRoute =
|
||||||
|
AuthenticatedGachaRollCreateRouteImport.update({
|
||||||
|
id: '/gacha-roll_/create',
|
||||||
|
path: '/gacha-roll/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedGachaRollIdRoute =
|
||||||
|
AuthenticatedGachaRollIdRouteImport.update({
|
||||||
|
id: '/gacha-roll_/$id',
|
||||||
|
path: '/gacha-roll/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedDashboardCreateRoute =
|
||||||
|
AuthenticatedDashboardCreateRouteImport.update({
|
||||||
|
id: '/dashboard_/create',
|
||||||
|
path: '/dashboard/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedDashboardIdRoute =
|
||||||
|
AuthenticatedDashboardIdRouteImport.update({
|
||||||
|
id: '/dashboard_/$id',
|
||||||
|
path: '/dashboard/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedCmsTestimonialsCreateRoute =
|
||||||
|
AuthenticatedCmsTestimonialsCreateRouteImport.update({
|
||||||
|
id: '/cms-testimonials_/create',
|
||||||
|
path: '/cms-testimonials/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedCmsTestimonialsIdRoute =
|
||||||
|
AuthenticatedCmsTestimonialsIdRouteImport.update({
|
||||||
|
id: '/cms-testimonials_/$id',
|
||||||
|
path: '/cms-testimonials/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedCmsEventsCreateRoute =
|
||||||
|
AuthenticatedCmsEventsCreateRouteImport.update({
|
||||||
|
id: '/cms-events_/create',
|
||||||
|
path: '/cms-events/create',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedCmsEventsIdRoute =
|
||||||
|
AuthenticatedCmsEventsIdRouteImport.update({
|
||||||
|
id: '/cms-events_/$id',
|
||||||
|
path: '/cms-events/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthenticatedAccountsIdRoute = AuthenticatedAccountsIdRouteImport.update({
|
||||||
|
id: '/accounts_/$id',
|
||||||
|
path: '/accounts/$id',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/accounts': typeof AuthenticatedAccountsRoute
|
||||||
|
'/cms-events': typeof AuthenticatedCmsEventsRoute
|
||||||
|
'/cms-testimonials': typeof AuthenticatedCmsTestimonialsRoute
|
||||||
|
'/dashboard': typeof AuthenticatedDashboardRoute
|
||||||
|
'/dashboard-dimentorin': typeof AuthenticatedDashboardDimentorinRoute
|
||||||
|
'/feedback-review-dimentorin': typeof AuthenticatedFeedbackReviewDimentorinRoute
|
||||||
|
'/gacha-roll': typeof AuthenticatedGachaRollRoute
|
||||||
|
'/hackathon-dashboard': typeof AuthenticatedHackathonDashboardRoute
|
||||||
|
'/hackathon-submissions': typeof AuthenticatedHackathonSubmissionsRoute
|
||||||
|
'/hackathon-teams': typeof AuthenticatedHackathonTeamsRoute
|
||||||
|
'/hackathon-users': typeof AuthenticatedHackathonUsersRoute
|
||||||
|
'/permissions': typeof AuthenticatedPermissionsRoute
|
||||||
|
'/prizes': typeof AuthenticatedPrizesRoute
|
||||||
|
'/roadmap-dimentorin': typeof AuthenticatedRoadmapDimentorinRoute
|
||||||
|
'/roles': typeof AuthenticatedRolesRoute
|
||||||
|
'/session-dimentorin': typeof AuthenticatedSessionDimentorinRoute
|
||||||
|
'/settings-dimentorin': typeof AuthenticatedSettingsDimentorinRoute
|
||||||
|
'/transactions': typeof AuthenticatedTransactionsRoute
|
||||||
|
'/users-dimentorin': typeof AuthenticatedUsersDimentorinRoute
|
||||||
|
'/accounts/$id': typeof AuthenticatedAccountsIdRoute
|
||||||
|
'/cms-events/$id': typeof AuthenticatedCmsEventsIdRoute
|
||||||
|
'/cms-events/create': typeof AuthenticatedCmsEventsCreateRoute
|
||||||
|
'/cms-testimonials/$id': typeof AuthenticatedCmsTestimonialsIdRoute
|
||||||
|
'/cms-testimonials/create': typeof AuthenticatedCmsTestimonialsCreateRoute
|
||||||
|
'/dashboard/$id': typeof AuthenticatedDashboardIdRoute
|
||||||
|
'/dashboard/create': typeof AuthenticatedDashboardCreateRoute
|
||||||
|
'/gacha-roll/$id': typeof AuthenticatedGachaRollIdRoute
|
||||||
|
'/gacha-roll/create': typeof AuthenticatedGachaRollCreateRoute
|
||||||
|
'/permissions/$id': typeof AuthenticatedPermissionsIdRoute
|
||||||
|
'/permissions/create': typeof AuthenticatedPermissionsCreateRoute
|
||||||
|
'/roadmap-dimentorin/$id': typeof AuthenticatedRoadmapDimentorinIdRoute
|
||||||
|
'/roadmap-dimentorin/create': typeof AuthenticatedRoadmapDimentorinCreateRoute
|
||||||
|
'/roles/$id': typeof AuthenticatedRolesIdRoute
|
||||||
|
'/roles/create': typeof AuthenticatedRolesCreateRoute
|
||||||
|
'/session-dimentorin/$id': typeof AuthenticatedSessionDimentorinIdRoute
|
||||||
|
'/users-dimentorin/$id': typeof AuthenticatedUsersDimentorinIdRoute
|
||||||
|
'/auth/login': typeof PublicAuthLoginRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/accounts': typeof AuthenticatedAccountsRoute
|
||||||
|
'/cms-events': typeof AuthenticatedCmsEventsRoute
|
||||||
|
'/cms-testimonials': typeof AuthenticatedCmsTestimonialsRoute
|
||||||
|
'/dashboard': typeof AuthenticatedDashboardRoute
|
||||||
|
'/dashboard-dimentorin': typeof AuthenticatedDashboardDimentorinRoute
|
||||||
|
'/feedback-review-dimentorin': typeof AuthenticatedFeedbackReviewDimentorinRoute
|
||||||
|
'/gacha-roll': typeof AuthenticatedGachaRollRoute
|
||||||
|
'/hackathon-dashboard': typeof AuthenticatedHackathonDashboardRoute
|
||||||
|
'/hackathon-submissions': typeof AuthenticatedHackathonSubmissionsRoute
|
||||||
|
'/hackathon-teams': typeof AuthenticatedHackathonTeamsRoute
|
||||||
|
'/hackathon-users': typeof AuthenticatedHackathonUsersRoute
|
||||||
|
'/permissions': typeof AuthenticatedPermissionsRoute
|
||||||
|
'/prizes': typeof AuthenticatedPrizesRoute
|
||||||
|
'/roadmap-dimentorin': typeof AuthenticatedRoadmapDimentorinRoute
|
||||||
|
'/roles': typeof AuthenticatedRolesRoute
|
||||||
|
'/session-dimentorin': typeof AuthenticatedSessionDimentorinRoute
|
||||||
|
'/settings-dimentorin': typeof AuthenticatedSettingsDimentorinRoute
|
||||||
|
'/transactions': typeof AuthenticatedTransactionsRoute
|
||||||
|
'/users-dimentorin': typeof AuthenticatedUsersDimentorinRoute
|
||||||
|
'/accounts/$id': typeof AuthenticatedAccountsIdRoute
|
||||||
|
'/cms-events/$id': typeof AuthenticatedCmsEventsIdRoute
|
||||||
|
'/cms-events/create': typeof AuthenticatedCmsEventsCreateRoute
|
||||||
|
'/cms-testimonials/$id': typeof AuthenticatedCmsTestimonialsIdRoute
|
||||||
|
'/cms-testimonials/create': typeof AuthenticatedCmsTestimonialsCreateRoute
|
||||||
|
'/dashboard/$id': typeof AuthenticatedDashboardIdRoute
|
||||||
|
'/dashboard/create': typeof AuthenticatedDashboardCreateRoute
|
||||||
|
'/gacha-roll/$id': typeof AuthenticatedGachaRollIdRoute
|
||||||
|
'/gacha-roll/create': typeof AuthenticatedGachaRollCreateRoute
|
||||||
|
'/permissions/$id': typeof AuthenticatedPermissionsIdRoute
|
||||||
|
'/permissions/create': typeof AuthenticatedPermissionsCreateRoute
|
||||||
|
'/roadmap-dimentorin/$id': typeof AuthenticatedRoadmapDimentorinIdRoute
|
||||||
|
'/roadmap-dimentorin/create': typeof AuthenticatedRoadmapDimentorinCreateRoute
|
||||||
|
'/roles/$id': typeof AuthenticatedRolesIdRoute
|
||||||
|
'/roles/create': typeof AuthenticatedRolesCreateRoute
|
||||||
|
'/session-dimentorin/$id': typeof AuthenticatedSessionDimentorinIdRoute
|
||||||
|
'/users-dimentorin/$id': typeof AuthenticatedUsersDimentorinIdRoute
|
||||||
|
'/auth/login': typeof PublicAuthLoginRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||||
|
'/_public': typeof PublicRouteWithChildren
|
||||||
|
'/_authenticated/accounts': typeof AuthenticatedAccountsRoute
|
||||||
|
'/_authenticated/cms-events': typeof AuthenticatedCmsEventsRoute
|
||||||
|
'/_authenticated/cms-testimonials': typeof AuthenticatedCmsTestimonialsRoute
|
||||||
|
'/_authenticated/dashboard': typeof AuthenticatedDashboardRoute
|
||||||
|
'/_authenticated/dashboard-dimentorin': typeof AuthenticatedDashboardDimentorinRoute
|
||||||
|
'/_authenticated/feedback-review-dimentorin': typeof AuthenticatedFeedbackReviewDimentorinRoute
|
||||||
|
'/_authenticated/gacha-roll': typeof AuthenticatedGachaRollRoute
|
||||||
|
'/_authenticated/hackathon-dashboard': typeof AuthenticatedHackathonDashboardRoute
|
||||||
|
'/_authenticated/hackathon-submissions': typeof AuthenticatedHackathonSubmissionsRoute
|
||||||
|
'/_authenticated/hackathon-teams': typeof AuthenticatedHackathonTeamsRoute
|
||||||
|
'/_authenticated/hackathon-users': typeof AuthenticatedHackathonUsersRoute
|
||||||
|
'/_authenticated/permissions': typeof AuthenticatedPermissionsRoute
|
||||||
|
'/_authenticated/prizes': typeof AuthenticatedPrizesRoute
|
||||||
|
'/_authenticated/roadmap-dimentorin': typeof AuthenticatedRoadmapDimentorinRoute
|
||||||
|
'/_authenticated/roles': typeof AuthenticatedRolesRoute
|
||||||
|
'/_authenticated/session-dimentorin': typeof AuthenticatedSessionDimentorinRoute
|
||||||
|
'/_authenticated/settings-dimentorin': typeof AuthenticatedSettingsDimentorinRoute
|
||||||
|
'/_authenticated/transactions': typeof AuthenticatedTransactionsRoute
|
||||||
|
'/_authenticated/users-dimentorin': typeof AuthenticatedUsersDimentorinRoute
|
||||||
|
'/_authenticated/accounts_/$id': typeof AuthenticatedAccountsIdRoute
|
||||||
|
'/_authenticated/cms-events_/$id': typeof AuthenticatedCmsEventsIdRoute
|
||||||
|
'/_authenticated/cms-events_/create': typeof AuthenticatedCmsEventsCreateRoute
|
||||||
|
'/_authenticated/cms-testimonials_/$id': typeof AuthenticatedCmsTestimonialsIdRoute
|
||||||
|
'/_authenticated/cms-testimonials_/create': typeof AuthenticatedCmsTestimonialsCreateRoute
|
||||||
|
'/_authenticated/dashboard_/$id': typeof AuthenticatedDashboardIdRoute
|
||||||
|
'/_authenticated/dashboard_/create': typeof AuthenticatedDashboardCreateRoute
|
||||||
|
'/_authenticated/gacha-roll_/$id': typeof AuthenticatedGachaRollIdRoute
|
||||||
|
'/_authenticated/gacha-roll_/create': typeof AuthenticatedGachaRollCreateRoute
|
||||||
|
'/_authenticated/permissions_/$id': typeof AuthenticatedPermissionsIdRoute
|
||||||
|
'/_authenticated/permissions_/create': typeof AuthenticatedPermissionsCreateRoute
|
||||||
|
'/_authenticated/roadmap-dimentorin_/$id': typeof AuthenticatedRoadmapDimentorinIdRoute
|
||||||
|
'/_authenticated/roadmap-dimentorin_/create': typeof AuthenticatedRoadmapDimentorinCreateRoute
|
||||||
|
'/_authenticated/roles_/$id': typeof AuthenticatedRolesIdRoute
|
||||||
|
'/_authenticated/roles_/create': typeof AuthenticatedRolesCreateRoute
|
||||||
|
'/_authenticated/session-dimentorin_/$id': typeof AuthenticatedSessionDimentorinIdRoute
|
||||||
|
'/_authenticated/users-dimentorin_/$id': typeof AuthenticatedUsersDimentorinIdRoute
|
||||||
|
'/_public/auth/login': typeof PublicAuthLoginRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/accounts'
|
||||||
|
| '/cms-events'
|
||||||
|
| '/cms-testimonials'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/dashboard-dimentorin'
|
||||||
|
| '/feedback-review-dimentorin'
|
||||||
|
| '/gacha-roll'
|
||||||
|
| '/hackathon-dashboard'
|
||||||
|
| '/hackathon-submissions'
|
||||||
|
| '/hackathon-teams'
|
||||||
|
| '/hackathon-users'
|
||||||
|
| '/permissions'
|
||||||
|
| '/prizes'
|
||||||
|
| '/roadmap-dimentorin'
|
||||||
|
| '/roles'
|
||||||
|
| '/session-dimentorin'
|
||||||
|
| '/settings-dimentorin'
|
||||||
|
| '/transactions'
|
||||||
|
| '/users-dimentorin'
|
||||||
|
| '/accounts/$id'
|
||||||
|
| '/cms-events/$id'
|
||||||
|
| '/cms-events/create'
|
||||||
|
| '/cms-testimonials/$id'
|
||||||
|
| '/cms-testimonials/create'
|
||||||
|
| '/dashboard/$id'
|
||||||
|
| '/dashboard/create'
|
||||||
|
| '/gacha-roll/$id'
|
||||||
|
| '/gacha-roll/create'
|
||||||
|
| '/permissions/$id'
|
||||||
|
| '/permissions/create'
|
||||||
|
| '/roadmap-dimentorin/$id'
|
||||||
|
| '/roadmap-dimentorin/create'
|
||||||
|
| '/roles/$id'
|
||||||
|
| '/roles/create'
|
||||||
|
| '/session-dimentorin/$id'
|
||||||
|
| '/users-dimentorin/$id'
|
||||||
|
| '/auth/login'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/accounts'
|
||||||
|
| '/cms-events'
|
||||||
|
| '/cms-testimonials'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/dashboard-dimentorin'
|
||||||
|
| '/feedback-review-dimentorin'
|
||||||
|
| '/gacha-roll'
|
||||||
|
| '/hackathon-dashboard'
|
||||||
|
| '/hackathon-submissions'
|
||||||
|
| '/hackathon-teams'
|
||||||
|
| '/hackathon-users'
|
||||||
|
| '/permissions'
|
||||||
|
| '/prizes'
|
||||||
|
| '/roadmap-dimentorin'
|
||||||
|
| '/roles'
|
||||||
|
| '/session-dimentorin'
|
||||||
|
| '/settings-dimentorin'
|
||||||
|
| '/transactions'
|
||||||
|
| '/users-dimentorin'
|
||||||
|
| '/accounts/$id'
|
||||||
|
| '/cms-events/$id'
|
||||||
|
| '/cms-events/create'
|
||||||
|
| '/cms-testimonials/$id'
|
||||||
|
| '/cms-testimonials/create'
|
||||||
|
| '/dashboard/$id'
|
||||||
|
| '/dashboard/create'
|
||||||
|
| '/gacha-roll/$id'
|
||||||
|
| '/gacha-roll/create'
|
||||||
|
| '/permissions/$id'
|
||||||
|
| '/permissions/create'
|
||||||
|
| '/roadmap-dimentorin/$id'
|
||||||
|
| '/roadmap-dimentorin/create'
|
||||||
|
| '/roles/$id'
|
||||||
|
| '/roles/create'
|
||||||
|
| '/session-dimentorin/$id'
|
||||||
|
| '/users-dimentorin/$id'
|
||||||
|
| '/auth/login'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/_authenticated'
|
||||||
|
| '/_public'
|
||||||
|
| '/_authenticated/accounts'
|
||||||
|
| '/_authenticated/cms-events'
|
||||||
|
| '/_authenticated/cms-testimonials'
|
||||||
|
| '/_authenticated/dashboard'
|
||||||
|
| '/_authenticated/dashboard-dimentorin'
|
||||||
|
| '/_authenticated/feedback-review-dimentorin'
|
||||||
|
| '/_authenticated/gacha-roll'
|
||||||
|
| '/_authenticated/hackathon-dashboard'
|
||||||
|
| '/_authenticated/hackathon-submissions'
|
||||||
|
| '/_authenticated/hackathon-teams'
|
||||||
|
| '/_authenticated/hackathon-users'
|
||||||
|
| '/_authenticated/permissions'
|
||||||
|
| '/_authenticated/prizes'
|
||||||
|
| '/_authenticated/roadmap-dimentorin'
|
||||||
|
| '/_authenticated/roles'
|
||||||
|
| '/_authenticated/session-dimentorin'
|
||||||
|
| '/_authenticated/settings-dimentorin'
|
||||||
|
| '/_authenticated/transactions'
|
||||||
|
| '/_authenticated/users-dimentorin'
|
||||||
|
| '/_authenticated/accounts_/$id'
|
||||||
|
| '/_authenticated/cms-events_/$id'
|
||||||
|
| '/_authenticated/cms-events_/create'
|
||||||
|
| '/_authenticated/cms-testimonials_/$id'
|
||||||
|
| '/_authenticated/cms-testimonials_/create'
|
||||||
|
| '/_authenticated/dashboard_/$id'
|
||||||
|
| '/_authenticated/dashboard_/create'
|
||||||
|
| '/_authenticated/gacha-roll_/$id'
|
||||||
|
| '/_authenticated/gacha-roll_/create'
|
||||||
|
| '/_authenticated/permissions_/$id'
|
||||||
|
| '/_authenticated/permissions_/create'
|
||||||
|
| '/_authenticated/roadmap-dimentorin_/$id'
|
||||||
|
| '/_authenticated/roadmap-dimentorin_/create'
|
||||||
|
| '/_authenticated/roles_/$id'
|
||||||
|
| '/_authenticated/roles_/create'
|
||||||
|
| '/_authenticated/session-dimentorin_/$id'
|
||||||
|
| '/_authenticated/users-dimentorin_/$id'
|
||||||
|
| '/_public/auth/login'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
||||||
|
PublicRoute: typeof PublicRouteWithChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/_public': {
|
||||||
|
id: '/_public'
|
||||||
|
path: ''
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof PublicRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_authenticated': {
|
||||||
|
id: '/_authenticated'
|
||||||
|
path: ''
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_authenticated/users-dimentorin': {
|
||||||
|
id: '/_authenticated/users-dimentorin'
|
||||||
|
path: '/users-dimentorin'
|
||||||
|
fullPath: '/users-dimentorin'
|
||||||
|
preLoaderRoute: typeof AuthenticatedUsersDimentorinRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/transactions': {
|
||||||
|
id: '/_authenticated/transactions'
|
||||||
|
path: '/transactions'
|
||||||
|
fullPath: '/transactions'
|
||||||
|
preLoaderRoute: typeof AuthenticatedTransactionsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/settings-dimentorin': {
|
||||||
|
id: '/_authenticated/settings-dimentorin'
|
||||||
|
path: '/settings-dimentorin'
|
||||||
|
fullPath: '/settings-dimentorin'
|
||||||
|
preLoaderRoute: typeof AuthenticatedSettingsDimentorinRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/session-dimentorin': {
|
||||||
|
id: '/_authenticated/session-dimentorin'
|
||||||
|
path: '/session-dimentorin'
|
||||||
|
fullPath: '/session-dimentorin'
|
||||||
|
preLoaderRoute: typeof AuthenticatedSessionDimentorinRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/roles': {
|
||||||
|
id: '/_authenticated/roles'
|
||||||
|
path: '/roles'
|
||||||
|
fullPath: '/roles'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRolesRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/roadmap-dimentorin': {
|
||||||
|
id: '/_authenticated/roadmap-dimentorin'
|
||||||
|
path: '/roadmap-dimentorin'
|
||||||
|
fullPath: '/roadmap-dimentorin'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRoadmapDimentorinRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/prizes': {
|
||||||
|
id: '/_authenticated/prizes'
|
||||||
|
path: '/prizes'
|
||||||
|
fullPath: '/prizes'
|
||||||
|
preLoaderRoute: typeof AuthenticatedPrizesRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/permissions': {
|
||||||
|
id: '/_authenticated/permissions'
|
||||||
|
path: '/permissions'
|
||||||
|
fullPath: '/permissions'
|
||||||
|
preLoaderRoute: typeof AuthenticatedPermissionsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/hackathon-users': {
|
||||||
|
id: '/_authenticated/hackathon-users'
|
||||||
|
path: '/hackathon-users'
|
||||||
|
fullPath: '/hackathon-users'
|
||||||
|
preLoaderRoute: typeof AuthenticatedHackathonUsersRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/hackathon-teams': {
|
||||||
|
id: '/_authenticated/hackathon-teams'
|
||||||
|
path: '/hackathon-teams'
|
||||||
|
fullPath: '/hackathon-teams'
|
||||||
|
preLoaderRoute: typeof AuthenticatedHackathonTeamsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/hackathon-submissions': {
|
||||||
|
id: '/_authenticated/hackathon-submissions'
|
||||||
|
path: '/hackathon-submissions'
|
||||||
|
fullPath: '/hackathon-submissions'
|
||||||
|
preLoaderRoute: typeof AuthenticatedHackathonSubmissionsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/hackathon-dashboard': {
|
||||||
|
id: '/_authenticated/hackathon-dashboard'
|
||||||
|
path: '/hackathon-dashboard'
|
||||||
|
fullPath: '/hackathon-dashboard'
|
||||||
|
preLoaderRoute: typeof AuthenticatedHackathonDashboardRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/gacha-roll': {
|
||||||
|
id: '/_authenticated/gacha-roll'
|
||||||
|
path: '/gacha-roll'
|
||||||
|
fullPath: '/gacha-roll'
|
||||||
|
preLoaderRoute: typeof AuthenticatedGachaRollRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/feedback-review-dimentorin': {
|
||||||
|
id: '/_authenticated/feedback-review-dimentorin'
|
||||||
|
path: '/feedback-review-dimentorin'
|
||||||
|
fullPath: '/feedback-review-dimentorin'
|
||||||
|
preLoaderRoute: typeof AuthenticatedFeedbackReviewDimentorinRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/dashboard-dimentorin': {
|
||||||
|
id: '/_authenticated/dashboard-dimentorin'
|
||||||
|
path: '/dashboard-dimentorin'
|
||||||
|
fullPath: '/dashboard-dimentorin'
|
||||||
|
preLoaderRoute: typeof AuthenticatedDashboardDimentorinRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/dashboard': {
|
||||||
|
id: '/_authenticated/dashboard'
|
||||||
|
path: '/dashboard'
|
||||||
|
fullPath: '/dashboard'
|
||||||
|
preLoaderRoute: typeof AuthenticatedDashboardRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/cms-testimonials': {
|
||||||
|
id: '/_authenticated/cms-testimonials'
|
||||||
|
path: '/cms-testimonials'
|
||||||
|
fullPath: '/cms-testimonials'
|
||||||
|
preLoaderRoute: typeof AuthenticatedCmsTestimonialsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/cms-events': {
|
||||||
|
id: '/_authenticated/cms-events'
|
||||||
|
path: '/cms-events'
|
||||||
|
fullPath: '/cms-events'
|
||||||
|
preLoaderRoute: typeof AuthenticatedCmsEventsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/accounts': {
|
||||||
|
id: '/_authenticated/accounts'
|
||||||
|
path: '/accounts'
|
||||||
|
fullPath: '/accounts'
|
||||||
|
preLoaderRoute: typeof AuthenticatedAccountsRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_public/auth/login': {
|
||||||
|
id: '/_public/auth/login'
|
||||||
|
path: '/auth/login'
|
||||||
|
fullPath: '/auth/login'
|
||||||
|
preLoaderRoute: typeof PublicAuthLoginRouteImport
|
||||||
|
parentRoute: typeof PublicRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/users-dimentorin_/$id': {
|
||||||
|
id: '/_authenticated/users-dimentorin_/$id'
|
||||||
|
path: '/users-dimentorin/$id'
|
||||||
|
fullPath: '/users-dimentorin/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedUsersDimentorinIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/session-dimentorin_/$id': {
|
||||||
|
id: '/_authenticated/session-dimentorin_/$id'
|
||||||
|
path: '/session-dimentorin/$id'
|
||||||
|
fullPath: '/session-dimentorin/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedSessionDimentorinIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/roles_/create': {
|
||||||
|
id: '/_authenticated/roles_/create'
|
||||||
|
path: '/roles/create'
|
||||||
|
fullPath: '/roles/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRolesCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/roles_/$id': {
|
||||||
|
id: '/_authenticated/roles_/$id'
|
||||||
|
path: '/roles/$id'
|
||||||
|
fullPath: '/roles/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRolesIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/roadmap-dimentorin_/create': {
|
||||||
|
id: '/_authenticated/roadmap-dimentorin_/create'
|
||||||
|
path: '/roadmap-dimentorin/create'
|
||||||
|
fullPath: '/roadmap-dimentorin/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRoadmapDimentorinCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/roadmap-dimentorin_/$id': {
|
||||||
|
id: '/_authenticated/roadmap-dimentorin_/$id'
|
||||||
|
path: '/roadmap-dimentorin/$id'
|
||||||
|
fullPath: '/roadmap-dimentorin/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedRoadmapDimentorinIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/permissions_/create': {
|
||||||
|
id: '/_authenticated/permissions_/create'
|
||||||
|
path: '/permissions/create'
|
||||||
|
fullPath: '/permissions/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedPermissionsCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/permissions_/$id': {
|
||||||
|
id: '/_authenticated/permissions_/$id'
|
||||||
|
path: '/permissions/$id'
|
||||||
|
fullPath: '/permissions/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedPermissionsIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/gacha-roll_/create': {
|
||||||
|
id: '/_authenticated/gacha-roll_/create'
|
||||||
|
path: '/gacha-roll/create'
|
||||||
|
fullPath: '/gacha-roll/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedGachaRollCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/gacha-roll_/$id': {
|
||||||
|
id: '/_authenticated/gacha-roll_/$id'
|
||||||
|
path: '/gacha-roll/$id'
|
||||||
|
fullPath: '/gacha-roll/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedGachaRollIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/dashboard_/create': {
|
||||||
|
id: '/_authenticated/dashboard_/create'
|
||||||
|
path: '/dashboard/create'
|
||||||
|
fullPath: '/dashboard/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedDashboardCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/dashboard_/$id': {
|
||||||
|
id: '/_authenticated/dashboard_/$id'
|
||||||
|
path: '/dashboard/$id'
|
||||||
|
fullPath: '/dashboard/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedDashboardIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/cms-testimonials_/create': {
|
||||||
|
id: '/_authenticated/cms-testimonials_/create'
|
||||||
|
path: '/cms-testimonials/create'
|
||||||
|
fullPath: '/cms-testimonials/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedCmsTestimonialsCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/cms-testimonials_/$id': {
|
||||||
|
id: '/_authenticated/cms-testimonials_/$id'
|
||||||
|
path: '/cms-testimonials/$id'
|
||||||
|
fullPath: '/cms-testimonials/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedCmsTestimonialsIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/cms-events_/create': {
|
||||||
|
id: '/_authenticated/cms-events_/create'
|
||||||
|
path: '/cms-events/create'
|
||||||
|
fullPath: '/cms-events/create'
|
||||||
|
preLoaderRoute: typeof AuthenticatedCmsEventsCreateRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/cms-events_/$id': {
|
||||||
|
id: '/_authenticated/cms-events_/$id'
|
||||||
|
path: '/cms-events/$id'
|
||||||
|
fullPath: '/cms-events/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedCmsEventsIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
'/_authenticated/accounts_/$id': {
|
||||||
|
id: '/_authenticated/accounts_/$id'
|
||||||
|
path: '/accounts/$id'
|
||||||
|
fullPath: '/accounts/$id'
|
||||||
|
preLoaderRoute: typeof AuthenticatedAccountsIdRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthenticatedRouteChildren {
|
||||||
|
AuthenticatedAccountsRoute: typeof AuthenticatedAccountsRoute
|
||||||
|
AuthenticatedCmsEventsRoute: typeof AuthenticatedCmsEventsRoute
|
||||||
|
AuthenticatedCmsTestimonialsRoute: typeof AuthenticatedCmsTestimonialsRoute
|
||||||
|
AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute
|
||||||
|
AuthenticatedDashboardDimentorinRoute: typeof AuthenticatedDashboardDimentorinRoute
|
||||||
|
AuthenticatedFeedbackReviewDimentorinRoute: typeof AuthenticatedFeedbackReviewDimentorinRoute
|
||||||
|
AuthenticatedGachaRollRoute: typeof AuthenticatedGachaRollRoute
|
||||||
|
AuthenticatedHackathonDashboardRoute: typeof AuthenticatedHackathonDashboardRoute
|
||||||
|
AuthenticatedHackathonSubmissionsRoute: typeof AuthenticatedHackathonSubmissionsRoute
|
||||||
|
AuthenticatedHackathonTeamsRoute: typeof AuthenticatedHackathonTeamsRoute
|
||||||
|
AuthenticatedHackathonUsersRoute: typeof AuthenticatedHackathonUsersRoute
|
||||||
|
AuthenticatedPermissionsRoute: typeof AuthenticatedPermissionsRoute
|
||||||
|
AuthenticatedPrizesRoute: typeof AuthenticatedPrizesRoute
|
||||||
|
AuthenticatedRoadmapDimentorinRoute: typeof AuthenticatedRoadmapDimentorinRoute
|
||||||
|
AuthenticatedRolesRoute: typeof AuthenticatedRolesRoute
|
||||||
|
AuthenticatedSessionDimentorinRoute: typeof AuthenticatedSessionDimentorinRoute
|
||||||
|
AuthenticatedSettingsDimentorinRoute: typeof AuthenticatedSettingsDimentorinRoute
|
||||||
|
AuthenticatedTransactionsRoute: typeof AuthenticatedTransactionsRoute
|
||||||
|
AuthenticatedUsersDimentorinRoute: typeof AuthenticatedUsersDimentorinRoute
|
||||||
|
AuthenticatedAccountsIdRoute: typeof AuthenticatedAccountsIdRoute
|
||||||
|
AuthenticatedCmsEventsIdRoute: typeof AuthenticatedCmsEventsIdRoute
|
||||||
|
AuthenticatedCmsEventsCreateRoute: typeof AuthenticatedCmsEventsCreateRoute
|
||||||
|
AuthenticatedCmsTestimonialsIdRoute: typeof AuthenticatedCmsTestimonialsIdRoute
|
||||||
|
AuthenticatedCmsTestimonialsCreateRoute: typeof AuthenticatedCmsTestimonialsCreateRoute
|
||||||
|
AuthenticatedDashboardIdRoute: typeof AuthenticatedDashboardIdRoute
|
||||||
|
AuthenticatedDashboardCreateRoute: typeof AuthenticatedDashboardCreateRoute
|
||||||
|
AuthenticatedGachaRollIdRoute: typeof AuthenticatedGachaRollIdRoute
|
||||||
|
AuthenticatedGachaRollCreateRoute: typeof AuthenticatedGachaRollCreateRoute
|
||||||
|
AuthenticatedPermissionsIdRoute: typeof AuthenticatedPermissionsIdRoute
|
||||||
|
AuthenticatedPermissionsCreateRoute: typeof AuthenticatedPermissionsCreateRoute
|
||||||
|
AuthenticatedRoadmapDimentorinIdRoute: typeof AuthenticatedRoadmapDimentorinIdRoute
|
||||||
|
AuthenticatedRoadmapDimentorinCreateRoute: typeof AuthenticatedRoadmapDimentorinCreateRoute
|
||||||
|
AuthenticatedRolesIdRoute: typeof AuthenticatedRolesIdRoute
|
||||||
|
AuthenticatedRolesCreateRoute: typeof AuthenticatedRolesCreateRoute
|
||||||
|
AuthenticatedSessionDimentorinIdRoute: typeof AuthenticatedSessionDimentorinIdRoute
|
||||||
|
AuthenticatedUsersDimentorinIdRoute: typeof AuthenticatedUsersDimentorinIdRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||||
|
AuthenticatedAccountsRoute: AuthenticatedAccountsRoute,
|
||||||
|
AuthenticatedCmsEventsRoute: AuthenticatedCmsEventsRoute,
|
||||||
|
AuthenticatedCmsTestimonialsRoute: AuthenticatedCmsTestimonialsRoute,
|
||||||
|
AuthenticatedDashboardRoute: AuthenticatedDashboardRoute,
|
||||||
|
AuthenticatedDashboardDimentorinRoute: AuthenticatedDashboardDimentorinRoute,
|
||||||
|
AuthenticatedFeedbackReviewDimentorinRoute:
|
||||||
|
AuthenticatedFeedbackReviewDimentorinRoute,
|
||||||
|
AuthenticatedGachaRollRoute: AuthenticatedGachaRollRoute,
|
||||||
|
AuthenticatedHackathonDashboardRoute: AuthenticatedHackathonDashboardRoute,
|
||||||
|
AuthenticatedHackathonSubmissionsRoute:
|
||||||
|
AuthenticatedHackathonSubmissionsRoute,
|
||||||
|
AuthenticatedHackathonTeamsRoute: AuthenticatedHackathonTeamsRoute,
|
||||||
|
AuthenticatedHackathonUsersRoute: AuthenticatedHackathonUsersRoute,
|
||||||
|
AuthenticatedPermissionsRoute: AuthenticatedPermissionsRoute,
|
||||||
|
AuthenticatedPrizesRoute: AuthenticatedPrizesRoute,
|
||||||
|
AuthenticatedRoadmapDimentorinRoute: AuthenticatedRoadmapDimentorinRoute,
|
||||||
|
AuthenticatedRolesRoute: AuthenticatedRolesRoute,
|
||||||
|
AuthenticatedSessionDimentorinRoute: AuthenticatedSessionDimentorinRoute,
|
||||||
|
AuthenticatedSettingsDimentorinRoute: AuthenticatedSettingsDimentorinRoute,
|
||||||
|
AuthenticatedTransactionsRoute: AuthenticatedTransactionsRoute,
|
||||||
|
AuthenticatedUsersDimentorinRoute: AuthenticatedUsersDimentorinRoute,
|
||||||
|
AuthenticatedAccountsIdRoute: AuthenticatedAccountsIdRoute,
|
||||||
|
AuthenticatedCmsEventsIdRoute: AuthenticatedCmsEventsIdRoute,
|
||||||
|
AuthenticatedCmsEventsCreateRoute: AuthenticatedCmsEventsCreateRoute,
|
||||||
|
AuthenticatedCmsTestimonialsIdRoute: AuthenticatedCmsTestimonialsIdRoute,
|
||||||
|
AuthenticatedCmsTestimonialsCreateRoute:
|
||||||
|
AuthenticatedCmsTestimonialsCreateRoute,
|
||||||
|
AuthenticatedDashboardIdRoute: AuthenticatedDashboardIdRoute,
|
||||||
|
AuthenticatedDashboardCreateRoute: AuthenticatedDashboardCreateRoute,
|
||||||
|
AuthenticatedGachaRollIdRoute: AuthenticatedGachaRollIdRoute,
|
||||||
|
AuthenticatedGachaRollCreateRoute: AuthenticatedGachaRollCreateRoute,
|
||||||
|
AuthenticatedPermissionsIdRoute: AuthenticatedPermissionsIdRoute,
|
||||||
|
AuthenticatedPermissionsCreateRoute: AuthenticatedPermissionsCreateRoute,
|
||||||
|
AuthenticatedRoadmapDimentorinIdRoute: AuthenticatedRoadmapDimentorinIdRoute,
|
||||||
|
AuthenticatedRoadmapDimentorinCreateRoute:
|
||||||
|
AuthenticatedRoadmapDimentorinCreateRoute,
|
||||||
|
AuthenticatedRolesIdRoute: AuthenticatedRolesIdRoute,
|
||||||
|
AuthenticatedRolesCreateRoute: AuthenticatedRolesCreateRoute,
|
||||||
|
AuthenticatedSessionDimentorinIdRoute: AuthenticatedSessionDimentorinIdRoute,
|
||||||
|
AuthenticatedUsersDimentorinIdRoute: AuthenticatedUsersDimentorinIdRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||||
|
AuthenticatedRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface PublicRouteChildren {
|
||||||
|
PublicAuthLoginRoute: typeof PublicAuthLoginRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PublicRouteChildren: PublicRouteChildren = {
|
||||||
|
PublicAuthLoginRoute: PublicAuthLoginRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PublicRouteWithChildren =
|
||||||
|
PublicRoute._addFileChildren(PublicRouteChildren)
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
||||||
|
PublicRoute: PublicRouteWithChildren,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: () => <Outlet />,
|
||||||
|
})
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
|
||||||
|
import { SessionToken } from '@imphnen-frontend-service/service';
|
||||||
|
import {
|
||||||
|
SidebarInset,
|
||||||
|
SidebarProvider,
|
||||||
|
} from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { BackofficeSidebar } from '../components/sidebar';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/_authenticated')({
|
||||||
|
beforeLoad: () => {
|
||||||
|
const session = SessionToken.get();
|
||||||
|
if (!session?.token?.access_token) {
|
||||||
|
throw redirect({ to: '/auth/login' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: AuthenticatedLayout,
|
||||||
|
});
|
||||||
|
|
||||||
|
function AuthenticatedLayout() {
|
||||||
|
return (
|
||||||
|
<SidebarProvider>
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<SidebarInset>
|
||||||
|
<Outlet />
|
||||||
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
||||||
|
|
||||||
|
type ChartProps = {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: ChartProps[] = [
|
||||||
|
{ name: "Active", value: 49, color: "#23A1EB" },
|
||||||
|
{ name: "Done", value: 24, color: "#81CBF8" },
|
||||||
|
{ name: "Canceled", value: 27, color: "#BCE1FB" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
const renderCustomizedLabel = (props: any) => {
|
||||||
|
const cx = props.cx ?? 0
|
||||||
|
const cy = props.cy ?? 0
|
||||||
|
const midAngle = props.midAngle ?? 0
|
||||||
|
const innerRadius = props.innerRadius ?? 0
|
||||||
|
const outerRadius = props.outerRadius ?? 0
|
||||||
|
const percent = props.percent ?? 0
|
||||||
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||||
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||||
|
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
||||||
|
{`${(percent * 100).toFixed(0)}%`}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessionStatusChart = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
|
<PieChart width={500} height={320}>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={40}
|
||||||
|
outerRadius={100}
|
||||||
|
labelLine={false}
|
||||||
|
label={renderCustomizedLabel}
|
||||||
|
>
|
||||||
|
{chartData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
||||||
|
|
||||||
|
type ChartProps = {
|
||||||
|
name: string
|
||||||
|
activeUser: number
|
||||||
|
activeSession: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: ChartProps[] = [
|
||||||
|
{ name: "2014", activeUser: 0, activeSession: 0 },
|
||||||
|
{ name: "2015", activeUser: 15, activeSession: 25 },
|
||||||
|
{ name: "2016", activeUser: 30, activeSession: 40 },
|
||||||
|
{ name: "2017", activeUser: 45, activeSession: 55 },
|
||||||
|
{ name: "2018", activeUser: 60, activeSession: 70 },
|
||||||
|
{ name: "2019", activeUser: 75, activeSession: 85 },
|
||||||
|
{ name: "2020", activeUser: 90, activeSession: 95 },
|
||||||
|
{ name: "2021", activeUser: 85, activeSession: 80 },
|
||||||
|
{ name: "2022", activeUser: 95, activeSession: 90 },
|
||||||
|
{ name: "2023", activeUser: 100, activeSession: 100 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const UserGrowthChart = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
|
<LineChart data={chartData} width={500} height={320} margin={{ left: -32 }}>
|
||||||
|
<CartesianGrid />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis tickCount={10} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line dataKey="activeUser" stroke="#23A1EB" />
|
||||||
|
<Line dataKey="activeSession" stroke="#0877C1" />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
ProjectOutlined,
|
||||||
|
FileImageOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { TAdminSubmissionItem } from '@imphnen-frontend-service/service';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
interface SubmissionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
submission: TAdminSubmissionItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
submission,
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'submitted':
|
||||||
|
return 'bg-success-50 border-success-200 text-success-800';
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-orange-50 border-orange-200 text-orange-800';
|
||||||
|
case 'approved':
|
||||||
|
return 'bg-blue-50 border-blue-200 text-blue-800';
|
||||||
|
case 'rejected':
|
||||||
|
return 'bg-error-50 border-error-200 text-error-800';
|
||||||
|
default:
|
||||||
|
return 'bg-neutral-50 border-neutral-200 text-neutral-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
|
||||||
|
<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">
|
||||||
|
{submission.project_name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
Team ID: {submission.team_id} • Submitted{' '}
|
||||||
|
{new Date(submission.submitted_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-neutral-400 hover:text-neutral-600 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<CloseOutlined className="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-4 border rounded-lg',
|
||||||
|
getStatusColor(submission.status)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-3 h-3 rounded-full',
|
||||||
|
submission.status === 'submitted' && 'bg-success-500',
|
||||||
|
submission.status === 'pending' && 'bg-orange-500',
|
||||||
|
submission.status === 'approved' && 'bg-blue-500',
|
||||||
|
submission.status === 'rejected' && 'bg-error-500'
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Status:{' '}
|
||||||
|
{submission.status.charAt(0).toUpperCase() +
|
||||||
|
submission.status.slice(1)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs">Submitted by: {submission.submitted_by}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-neutral-700 mb-2">
|
||||||
|
Project Description
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-neutral-600 leading-relaxed">
|
||||||
|
{submission.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-neutral-700">
|
||||||
|
Project Links
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
|
||||||
|
<LinkOutlined className="text-primary-500 mt-1" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-medium text-neutral-600 mb-1">
|
||||||
|
Repository
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={submission.repository_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary-600 hover:text-primary-700 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{submission.repository_url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{submission.demo_url && (
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
|
||||||
|
<LinkOutlined className="text-primary-500 mt-1" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-medium text-neutral-600 mb-1">
|
||||||
|
Live Demo
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={submission.demo_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary-600 hover:text-primary-700 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{submission.demo_url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{submission.presentation_url && (
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
|
||||||
|
<LinkOutlined className="text-primary-500 mt-1" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-medium text-neutral-600 mb-1">
|
||||||
|
Presentation
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={submission.presentation_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary-600 hover:text-primary-700 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{submission.presentation_url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{submission.screenshots && submission.screenshots.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-neutral-700 flex items-center gap-2">
|
||||||
|
<FileImageOutlined className="text-primary-500" />
|
||||||
|
Screenshots ({submission.screenshots.length})
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{submission.screenshots.map((screenshot, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={screenshot}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block rounded-lg overflow-hidden border border-neutral-200 hover:border-primary-300 transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={screenshot}
|
||||||
|
alt={`Screenshot ${index + 1}`}
|
||||||
|
className="w-full h-40 object-cover"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-neutral-200">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-neutral-500 mb-1">Created</p>
|
||||||
|
<p className="text-sm text-neutral-900">
|
||||||
|
{new Date(submission.created_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-neutral-500 mb-1">Last Updated</p>
|
||||||
|
<p className="text-sm text-neutral-900">
|
||||||
|
{new Date(submission.updated_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-neutral-200 bg-neutral-50">
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubmissionModal;
|
||||||
+501
@@ -0,0 +1,501 @@
|
|||||||
|
import { FC, useState, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { CityFilterSelect } from '../../../../components/city-filter-select';
|
||||||
|
import TeamBannerPlaceholder from './team-banner-placeholder';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { TAdminTeamItem } from '@imphnen-frontend-service/service';
|
||||||
|
import {
|
||||||
|
TeamOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
CrownOutlined,
|
||||||
|
ExclamationOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
CameraOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
EyeInvisibleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
type TeamType = TAdminTeamItem;
|
||||||
|
|
||||||
|
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 [showLogoMenu, setShowLogoMenu] = useState(false);
|
||||||
|
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const bannerInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (team) {
|
||||||
|
setFormData({ ...team });
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
city: '',
|
||||||
|
banner: null,
|
||||||
|
logo: null,
|
||||||
|
visibility: 'public',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
leader_id: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, team]);
|
||||||
|
|
||||||
|
const hasChanges = useMemo(() => {
|
||||||
|
if (!formData || !team) return !!formData;
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const isFormValid = useMemo(() => {
|
||||||
|
if (!formData) return false;
|
||||||
|
return (
|
||||||
|
formData.name.trim() !== '' &&
|
||||||
|
formData.city.trim() !== '' &&
|
||||||
|
formData.description.trim() !== ''
|
||||||
|
);
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
|
const canSave = hasChanges && isFormValid;
|
||||||
|
|
||||||
|
if (!isOpen || !formData) return null;
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof TeamType, value: string | null) => {
|
||||||
|
setFormData((prev) => (prev ? { ...prev, [field]: value } : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
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) return;
|
||||||
|
|
||||||
|
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) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6" onClick={() => setShowLogoMenu(false)}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={logoInputRef}
|
||||||
|
onChange={handleLogoUpload}
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={bannerInputRef}
|
||||||
|
onChange={handleBannerUpload}
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-neutral-700">
|
||||||
|
Team Banner{' '}
|
||||||
|
<span className="text-xs text-neutral-500">
|
||||||
|
(3:1 aspect ratio recommended)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<TeamBannerPlaceholder
|
||||||
|
banner={formData.banner || undefined}
|
||||||
|
teamName={formData.name || 'Team Name'}
|
||||||
|
className="rounded-lg border border-neutral-200 transition-all group-hover:border-primary-300"
|
||||||
|
/>
|
||||||
|
<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();
|
||||||
|
handleInputChange('banner', null);
|
||||||
|
}}
|
||||||
|
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
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-12 gap-4 items-start">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
{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();
|
||||||
|
logoInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
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();
|
||||||
|
handleInputChange('logo', null);
|
||||||
|
setShowLogoMenu(false);
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-neutral-700">
|
||||||
|
Description <span className="text-danger-500">*</span>
|
||||||
|
</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"
|
||||||
|
rows={3}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
className="text-primary-600"
|
||||||
|
/>
|
||||||
|
<EyeInvisibleOutlined className="text-neutral-600" />
|
||||||
|
<span className="text-sm">Private</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{team && (
|
||||||
|
<div className="space-y-4 border-t border-neutral-200 pt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-neutral-700">
|
||||||
|
Team Leader ID
|
||||||
|
</label>
|
||||||
|
<div className="p-3 bg-neutral-50 rounded-lg flex items-center gap-3">
|
||||||
|
<CrownOutlined className="text-yellow-600 text-lg" />
|
||||||
|
<span className="text-sm text-neutral-700 font-mono">
|
||||||
|
{team.leader_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{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 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalTeamDetail;
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { TeamOutlined } from '@ant-design/icons';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
interface TeamBannerPlaceholderProps {
|
||||||
|
banner?: string;
|
||||||
|
teamName: string;
|
||||||
|
className?: string;
|
||||||
|
showPlaceholder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
|
||||||
|
banner,
|
||||||
|
teamName,
|
||||||
|
className = '',
|
||||||
|
showPlaceholder = true,
|
||||||
|
}) => {
|
||||||
|
const aspectRatioClass = 'aspect-[3/1]';
|
||||||
|
|
||||||
|
if (!banner && !showPlaceholder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (banner) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full bg-gray-100 overflow-hidden relative',
|
||||||
|
aspectRatioClass,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={banner}
|
||||||
|
alt={`${teamName} banner`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
const placeholder = target.nextElementSibling as HTMLElement;
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute inset-0 bg-linear-to-r from-gray-100 to-gray-200 flex items-center justify-center',
|
||||||
|
'hidden'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<TeamOutlined className="text-4xl text-gray-400 mb-2" />
|
||||||
|
<p className="text-sm text-gray-500 font-medium">{teamName}</p>
|
||||||
|
<p className="text-xs text-gray-400">Team Banner</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full bg-linear-to-r from-gray-100 to-gray-200 flex items-center justify-center',
|
||||||
|
aspectRatioClass,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<TeamOutlined className="text-4xl text-gray-400 mb-2" />
|
||||||
|
<p className="text-sm text-gray-500 font-medium">{teamName}</p>
|
||||||
|
<p className="text-xs text-gray-400">No Banner</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamBannerPlaceholder;
|
||||||
+626
@@ -0,0 +1,626 @@
|
|||||||
|
import { FC, useState, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
EnvironmentOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
ExclamationOutlined,
|
||||||
|
CameraOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
interface UserType {
|
||||||
|
id: string;
|
||||||
|
avatar?: string | null;
|
||||||
|
fullname: string;
|
||||||
|
bio?: string;
|
||||||
|
location: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
skills: string[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
user: UserType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||||
|
const [formData, setFormData] = useState<UserType | null>(null);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [showAvatarMenu, setShowAvatarMenu] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (user) {
|
||||||
|
setFormData({ ...user });
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
id: '',
|
||||||
|
fullname: '',
|
||||||
|
bio: '',
|
||||||
|
location: '',
|
||||||
|
is_active: true,
|
||||||
|
skills: [],
|
||||||
|
avatar: undefined,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, user]);
|
||||||
|
|
||||||
|
const hasChanges = useMemo(() => {
|
||||||
|
if (!formData) return false;
|
||||||
|
if (!user) return true;
|
||||||
|
return (
|
||||||
|
formData.fullname !== user.fullname ||
|
||||||
|
formData.location !== user.location ||
|
||||||
|
formData.is_active !== user.is_active ||
|
||||||
|
formData.avatar !== user.avatar ||
|
||||||
|
JSON.stringify(formData.skills) !== JSON.stringify(user.skills) ||
|
||||||
|
formData.bio !== user.bio
|
||||||
|
);
|
||||||
|
}, [formData, user]);
|
||||||
|
|
||||||
|
const isFormValid = useMemo(() => {
|
||||||
|
if (!formData) return false;
|
||||||
|
return formData.fullname?.trim() !== '' && formData.location?.trim() !== '';
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
|
const canSave = hasChanges && isFormValid;
|
||||||
|
|
||||||
|
if (!isOpen || !formData) return null;
|
||||||
|
|
||||||
|
const handleInputChange = (
|
||||||
|
field: keyof UserType,
|
||||||
|
value: string | boolean | string[] | undefined
|
||||||
|
) => {
|
||||||
|
setFormData((prev) => (prev ? { ...prev, [field]: value } : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkillsChange = (skills: string[]) => {
|
||||||
|
setFormData((prev) => (prev ? { ...prev, skills } : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
console.log('Update user data:', formData);
|
||||||
|
} else {
|
||||||
|
console.log('Create new user:', formData);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (user) {
|
||||||
|
setFormData({ ...user });
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAccount = () => {
|
||||||
|
if (!user) return;
|
||||||
|
console.log('Delete user:', user.id);
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarUpload = (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 avatarUrl = e.target?.result as string;
|
||||||
|
handleInputChange('avatar', avatarUrl);
|
||||||
|
setShowAvatarMenu(false);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveAvatar = () => {
|
||||||
|
handleInputChange('avatar', undefined);
|
||||||
|
setShowAvatarMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerFileUpload = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableSkills = [
|
||||||
|
'Frontend Developer',
|
||||||
|
'Backend Developer',
|
||||||
|
'Full Stack Developer',
|
||||||
|
'DevOps Engineer',
|
||||||
|
'UI/UX Designer',
|
||||||
|
'Product Manager',
|
||||||
|
'Data Scientist',
|
||||||
|
'Mobile Developer',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50"
|
||||||
|
onClick={(e) => {
|
||||||
|
setShowAvatarMenu(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleAvatarUpload}
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div className="border-b border-neutral-200 px-8 py-6 flex justify-between items-start">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="relative group ">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden border-2 border-transparent group-hover:border-primary-300 transition-colors">
|
||||||
|
{formData.avatar ? (
|
||||||
|
<img
|
||||||
|
src={formData.avatar}
|
||||||
|
alt={formData.fullname}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<UserOutlined className="text-neutral-500 text-2xl" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAvatarMenu(!showAvatarMenu)}
|
||||||
|
className="absolute inset-0 bg-neutral-400 cursor-pointer rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CameraOutlined className="text-white text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showAvatarMenu && (
|
||||||
|
<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={triggerFileUpload}
|
||||||
|
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.avatar ? 'Change Photo' : 'Upload Photo'}
|
||||||
|
</button>
|
||||||
|
{formData.avatar && (
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveAvatar}
|
||||||
|
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 Photo
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h2 className="text-2xl font-bold text-neutral-900">
|
||||||
|
{user ? 'Edit User Profile' : 'Create New User'}
|
||||||
|
</h2>
|
||||||
|
{user && (
|
||||||
|
<span className="px-3 py-1 bg-info-100 text-info-700 text-xs font-medium rounded-2xl">
|
||||||
|
Hover avatar to change
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-neutral-500">
|
||||||
|
{user
|
||||||
|
? `Make changes to ${
|
||||||
|
formData.fullname || 'this user'
|
||||||
|
}'s profile information`
|
||||||
|
: 'Fill in the information below to create a new user account'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="p-2 hover:bg-neutral-100 rounded-lg transition-colors cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAvatarMenu(false);
|
||||||
|
handleCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseOutlined className="text-neutral-400 text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8" onClick={() => setShowAvatarMenu(false)}>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||||
|
Basic Information
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<UserOutlined className="text-neutral-400" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm text-neutral-500 block mb-1">
|
||||||
|
Full Name <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.fullname}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange('fullname', e.target.value)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
'w-full border rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none',
|
||||||
|
!formData.fullname ||
|
||||||
|
formData.fullname.trim() === ''
|
||||||
|
? 'border-red-300 bg-red-50'
|
||||||
|
: 'border-neutral-300'
|
||||||
|
)}
|
||||||
|
placeholder="Enter full name"
|
||||||
|
/>
|
||||||
|
{(!formData.fullname ||
|
||||||
|
formData.fullname.trim() === '') && (
|
||||||
|
<p className="text-red-500 text-xs mt-1">
|
||||||
|
Full name is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<EnvironmentOutlined className="text-neutral-400" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm text-neutral-500 block mb-1">
|
||||||
|
Location <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.location || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange('location', e.target.value)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
'w-full border rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none bg-white',
|
||||||
|
!formData.location ||
|
||||||
|
formData.location.trim() === ''
|
||||||
|
? 'border-red-300 bg-red-50'
|
||||||
|
: 'border-neutral-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<option value="">Select location</option>
|
||||||
|
<option value="Jakarta">Jakarta</option>
|
||||||
|
<option value="Bandung">Bandung</option>
|
||||||
|
<option value="Surabaya">Surabaya</option>
|
||||||
|
<option value="Medan">Medan</option>
|
||||||
|
<option value="Yogyakarta">Yogyakarta</option>
|
||||||
|
</select>
|
||||||
|
{(!formData.location ||
|
||||||
|
formData.location.trim() === '') && (
|
||||||
|
<p className="text-red-500 text-xs mt-1">
|
||||||
|
Location is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CalendarOutlined className="text-neutral-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
Joined Date
|
||||||
|
</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
{new Date(formData.created_at).toLocaleDateString(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-neutral-900 mb-3">
|
||||||
|
Bio{' '}
|
||||||
|
<span className="text-neutral-400 text-sm font-normal">
|
||||||
|
(Optional)
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<textarea
|
||||||
|
value={formData.bio || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange('bio', e.target.value || undefined)
|
||||||
|
}
|
||||||
|
placeholder="Tell us about yourself..."
|
||||||
|
rows={4}
|
||||||
|
className="w-full border border-neutral-300 rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||||
|
Account Status
|
||||||
|
</h3>
|
||||||
|
<div className="flex bg-neutral-100 p-1 rounded-lg">
|
||||||
|
<button
|
||||||
|
onClick={() => handleInputChange('is_active', true)}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 cursor-pointer',
|
||||||
|
formData.is_active
|
||||||
|
? 'bg-white text-success-700 shadow-sm ring-1 ring-success-200'
|
||||||
|
: 'text-neutral-600 hover:text-neutral-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
formData.is_active
|
||||||
|
? 'bg-success-500'
|
||||||
|
: 'bg-neutral-400'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleInputChange('is_active', false)}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 cursor-pointer',
|
||||||
|
!formData.is_active
|
||||||
|
? 'bg-white text-neutral-700 shadow-sm ring-1 ring-neutral-200'
|
||||||
|
: 'text-neutral-600 hover:text-neutral-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
!formData.is_active
|
||||||
|
? 'bg-neutral-500'
|
||||||
|
: 'bg-neutral-400'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
Inactive
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-500 mt-2">
|
||||||
|
{formData.is_active
|
||||||
|
? 'User can access their account and participate in activities'
|
||||||
|
: 'User account is suspended and cannot access services'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||||
|
Skills & Expertise{' '}
|
||||||
|
<span className="text-neutral-400 text-sm font-normal">
|
||||||
|
(Optional)
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap gap-2 min-h-10 p-3 border border-neutral-300 rounded-lg bg-neutral-50">
|
||||||
|
{formData.skills.length > 0 ? (
|
||||||
|
formData.skills.map((skill, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-2xl text-sm font-medium bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleSkillsChange(
|
||||||
|
formData.skills.filter((_, i) => i !== index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="text-blue-600 hover:text-blue-800 ml-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
No skills added yet
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value=""
|
||||||
|
onChange={(e) => {
|
||||||
|
if (
|
||||||
|
e.target.value &&
|
||||||
|
!formData.skills.includes(e.target.value)
|
||||||
|
) {
|
||||||
|
handleSkillsChange([
|
||||||
|
...formData.skills,
|
||||||
|
e.target.value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full border border-neutral-300 rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Add a skill...</option>
|
||||||
|
{availableSkills
|
||||||
|
.filter((skill) => !formData.skills.includes(skill))
|
||||||
|
.map((skill) => (
|
||||||
|
<option key={skill} value={skill}>
|
||||||
|
{skill}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||||
|
Account Details
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3 bg-neutral-50 p-4 rounded-lg">
|
||||||
|
<div className="flex justify-between items-center py-1">
|
||||||
|
<span className="text-neutral-600 text-sm">
|
||||||
|
User ID
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-sm text-neutral-800">
|
||||||
|
{formData.id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-1">
|
||||||
|
<span className="text-neutral-600 text-sm">
|
||||||
|
Last Updated
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-neutral-800">
|
||||||
|
{new Date(formData.updated_at).toLocaleDateString(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-neutral-200 px-8 py-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-sm text-neutral-500">
|
||||||
|
{canSave
|
||||||
|
? 'Ready to save changes'
|
||||||
|
: hasChanges
|
||||||
|
? 'Please fill required fields'
|
||||||
|
: 'No changes made'}
|
||||||
|
</div>
|
||||||
|
{user && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
className="text-red-600 hover:text-red-700 text-sm font-medium transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Delete Account
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!canSave}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-6',
|
||||||
|
!canSave && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SaveOutlined className="text-sm" />
|
||||||
|
{user ? 'Save Changes' : 'Create User'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDeleteConfirm && (
|
||||||
|
<div className="fixed inset-0 z-60">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50"
|
||||||
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-2xl w-full max-w-md">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
|
||||||
|
<ExclamationOutlined className="text-red-600 text-lg" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-neutral-900">
|
||||||
|
Delete Account
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
This action cannot be undone
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-neutral-700 mb-6">
|
||||||
|
Are you sure you want to permanently delete{' '}
|
||||||
|
<strong>{formData.fullname}</strong>'s account? This will
|
||||||
|
remove all their data and cannot be reversed.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
|
className="px-4"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDeleteAccount}
|
||||||
|
className="px-4 bg-red-600 hover:bg-red-700 border-red-600"
|
||||||
|
>
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalUserDetail;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
|
||||||
|
interface IModalProcessDelivery {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleProcessDelivery?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalProcessDelivery = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
handleProcessDelivery,
|
||||||
|
}: IModalProcessDelivery) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
disableEscapeKeyDown={true}
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||||
|
Delivery Process
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Lakukan pengiriman hadiah gacha untuk pengguna di bawah ini, jika
|
||||||
|
sudah ubah status menjadi “Delivered”.
|
||||||
|
</p>
|
||||||
|
</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="Ahmad Wiyana"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Item yang didapatkan"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Nama Item"
|
||||||
|
value="Lanyard + ID Card"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Alamat Pengiriman"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Alamat Pengiriman"
|
||||||
|
value="Jl. Pantai Cibaduyut Indah"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Status"
|
||||||
|
type="text"
|
||||||
|
placeholder="Isi Status Pengiriman"
|
||||||
|
value="Lanyard + ID Card"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleProcessDelivery && handleProcessDelivery()}
|
||||||
|
>
|
||||||
|
Proses Pengiriman
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalProcessDelivery;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button, Input, NativeSelect as Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const GeneralSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">General Settings</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ToggleInput label="Mode Maintenance" />
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Platform Settings</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Platform</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Bahasa</label>
|
||||||
|
<Select defaultValue="id" className="w-full">
|
||||||
|
<option value="id">Indonesia</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Logo Platform</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Favicon</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Legal Settings</h2>
|
||||||
|
<div className="grid gap-x-8 gap-y-5 mb-20">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>URL Syarat & Ketentuan</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>URL Kebijakan Privasi</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
import { Button, Textarea, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export const NotificationSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Notification Settings</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
|
<ToggleInput label="Nyalakan Notifikasi Email" />
|
||||||
|
<ToggleInput label="Beritahu mentor ketika ada request" />
|
||||||
|
<ToggleInput label="Beritahu mentee untuk update sesi " />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="text-neutral-800 font-medium inline-block mb-2 text-p3">
|
||||||
|
API Integrasi Notifikasi (URL)
|
||||||
|
</label>
|
||||||
|
<Textarea placeholder="Masukkan url API notifikasi yang akan digunakan" className="w-full h-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Input, NativeSelect as Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const PaymentSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Payment</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Integrasi Payment Gateway</label>
|
||||||
|
<Textarea className="min-w-full w-full h-20" placeholder="Durasi dalam menit" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Mata Uang</label>
|
||||||
|
<Select defaultValue="idr">
|
||||||
|
<option value="idr">Rupiah (IDR)</option>
|
||||||
|
<option value="usd">Dollar (USD)</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Harga sesi mentoring <span className="text-neutral-600">(default)</span></label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Masukkan harga sesi mentoring" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Tarif Komisi untuk Platform</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Masukkan persentase komisi" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Invoice</h2>
|
||||||
|
<div className="mb-32">
|
||||||
|
<label className={labelClass}>Masukkan Format Invoice</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" placeholder="InvoiceDimentorin.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const SecuritySettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Security Settings</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-8 mb-20">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Durasi Session Timeout</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Durasi dalam menit" />
|
||||||
|
<p className="text-[10px] text-neutral-800">Auto logout setelah X menit tidak aktif</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Blokir Setelah Upaya Gagal</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="x kali perobaan login" />
|
||||||
|
<p className="text-[10px] text-neutral-800">Misal: 5 kali salah login = lock akun</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user