This commit is contained in:
bipproduction
2025-11-22 10:23:29 +08:00
parent 98257d5f77
commit 1b3031ae3c
30 changed files with 554 additions and 260 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
# dependencies (bun install)
node_modules
# generated
generated
# output
out
dist

View File

@@ -5,31 +5,33 @@
"name": "bun-react-template",
"dependencies": {
"@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.1",
"@elysiajs/eden": "^1.4.5",
"@elysiajs/jwt": "^1.4.0",
"@elysiajs/swagger": "^1.3.1",
"@mantine/core": "^8.3.3",
"@mantine/hooks": "^8.3.3",
"@mantine/notifications": "^8.3.3",
"@prisma/client": "^6.7.0",
"@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@mantine/modals": "^8.3.8",
"@mantine/notifications": "^8.3.8",
"@prisma/client": "^6.19.0",
"@tabler/icons-react": "^3.35.0",
"@types/jwt-decode": "^3.1.0",
"add": "^2.0.6",
"elysia": "^1.4.9",
"dotenv": "^17.2.3",
"elysia": "^1.4.16",
"jwt-decode": "^4.0.0",
"react": "^19",
"react-dom": "^19",
"react-router-dom": "^7.9.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"swr": "^2.3.6",
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prisma": "^6.7.0",
"prisma": "^6.19.0",
},
},
},
@@ -40,7 +42,7 @@
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],
"@elysiajs/eden": ["@elysiajs/eden@1.4.5", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g=="],
"@elysiajs/jwt": ["@elysiajs/jwt@1.4.0", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Z0PvZhQxdDeKZ8HslXzDoXXD83NKExNPmoiAPki3nI2Xvh5wtUrBH+zWOD17yP14IbRo8fxGj3L25MRCAPsgPA=="],
@@ -56,27 +58,29 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@mantine/core": ["@mantine/core@8.3.6", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.6", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A=="],
"@mantine/core": ["@mantine/core@8.3.8", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.8", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-UM3Za7Yl0FzbZ2zPgHwNyCpLgtSqkAi8ku13+gRS/6JB0FjwSkMwibERUqQIpwqAHdR5KNmIohjuqHu8guJowg=="],
"@mantine/hooks": ["@mantine/hooks@8.3.6", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw=="],
"@mantine/hooks": ["@mantine/hooks@8.3.8", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-2YCUk5IWz+Ebi7VpbdscUz1MwulyaVPKr236ugMfpK0PFwsun4aBaLCAc8UeMGP0LtoSkuFvnsCPR4U6rhNfeQ=="],
"@mantine/notifications": ["@mantine/notifications@8.3.6", "", { "dependencies": { "@mantine/store": "8.3.6", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.6", "@mantine/hooks": "8.3.6", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-d3A96lyrFOVXtrwASEXALfzooKnnA60T2LclMXFF/4k27Ay5Hwza4D+ylqgxf0RkPfF9J6LhBXk72OjL5RH5Kg=="],
"@mantine/modals": ["@mantine/modals@8.3.8", "", { "peerDependencies": { "@mantine/core": "8.3.8", "@mantine/hooks": "8.3.8", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-hcYXchS1Zrdwz5xRnEsFTPv6o/kNQbl/Ey0LBXvZCMn//2aq70IHTlEbtUUM2FMQNz3i/wzcpOqvhUU9mGZVJw=="],
"@mantine/store": ["@mantine/store@8.3.6", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-fo86wF6nL8RPukY8cseAFQKk+bRVv3Ga/WmHJMYRsCbNleZOEZMXXUf/OVhmr1D3t+xzCzAlJe/sQ8MIS+c+pA=="],
"@mantine/notifications": ["@mantine/notifications@8.3.8", "", { "dependencies": { "@mantine/store": "8.3.8", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.8", "@mantine/hooks": "8.3.8", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-AS3UGnHO8UGzLpxe4cUIVpwpCoGKplWhMGm6E2hJoHnO4Wg0h3HlsR7drFEnDOZhaOMyD6MD9tAeWZ2/7rnvrw=="],
"@prisma/client": ["@prisma/client@6.18.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA=="],
"@mantine/store": ["@mantine/store@8.3.8", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-B6LEed839OR2t9pnC7Bl3zhMyYzUvJZ46YaOpH9zCqLiFX+u4FKC+UCNzqkz2a+I+olrNlONLnrCA0NDTCjz9A=="],
"@prisma/config": ["@prisma/config@6.18.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ=="],
"@prisma/client": ["@prisma/client@6.19.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g=="],
"@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="],
"@prisma/config": ["@prisma/config@6.19.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg=="],
"@prisma/engines": ["@prisma/engines@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/fetch-engine": "6.18.0", "@prisma/get-platform": "6.18.0" } }, "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA=="],
"@prisma/debug": ["@prisma/debug@6.19.0", "", {}, "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA=="],
"@prisma/engines-version": ["@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "", {}, "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ=="],
"@prisma/engines": ["@prisma/engines@6.19.0", "", { "dependencies": { "@prisma/debug": "6.19.0", "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", "@prisma/fetch-engine": "6.19.0", "@prisma/get-platform": "6.19.0" } }, "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/get-platform": "6.18.0" } }, "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A=="],
"@prisma/engines-version": ["@prisma/engines-version@6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", "", {}, "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ=="],
"@prisma/get-platform": ["@prisma/get-platform@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0" } }, "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@6.19.0", "", { "dependencies": { "@prisma/debug": "6.19.0", "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", "@prisma/get-platform": "6.19.0" } }, "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ=="],
"@prisma/get-platform": ["@prisma/get-platform@6.19.0", "", { "dependencies": { "@prisma/debug": "6.19.0" } }, "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
@@ -96,21 +100,21 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/jwt-decode": ["@types/jwt-decode@3.1.0", "", { "dependencies": { "jwt-decode": "*" } }, "sha512-tthwik7TKkou3mVnBnvVuHnHElbjtdbM63pdBCbZTirCt3WAdM73Y79mOri7+ljsS99ZVwUFZHLMxJuJnv/z1w=="],
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="],
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"add": ["add@2.0.6", "", {}, "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
@@ -130,7 +134,7 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -146,15 +150,15 @@
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
"elysia": ["elysia@1.4.15", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg=="],
"elysia": ["elysia@1.4.16", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-KZtKN160/bdWVKg2hEgyoNXY8jRRquc+m6PboyisaLZL891I+Ufb7Ja6lDAD7vMQur8sLEWIcidZOzj5lWw9UA=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
"exact-mirror": ["exact-mirror@0.2.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="],
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
@@ -226,7 +230,7 @@
"postcss-simple-vars": ["postcss-simple-vars@7.0.1", "", { "peerDependencies": { "postcss": "^8.2.1" } }, "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A=="],
"prisma": ["prisma@6.18.0", "", { "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g=="],
"prisma": ["prisma@6.19.0", "", { "dependencies": { "@prisma/config": "6.19.0", "@prisma/engines": "6.19.0" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
@@ -246,9 +250,9 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.9.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A=="],
"react-router": ["react-router@7.9.6", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA=="],
"react-router-dom": ["react-router-dom@7.9.5", "", { "dependencies": { "react-router": "7.9.5" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw=="],
"react-router-dom": ["react-router-dom@7.9.6", "", { "dependencies": { "react-router": "7.9.6" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
@@ -306,8 +310,12 @@
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
"c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"dom-helpers/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],

View File

@@ -1,4 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }

View File

@@ -1,4 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }

View File

@@ -1,6 +1,7 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
@@ -35,12 +36,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.18.0
* Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
* Prisma Client JS version: 6.19.0
* Query Engine version: 2ba551f319ab1df4bc874a89965d8b3641056773
*/
Prisma.prismaVersion = {
client: "6.18.0",
engine: "34b5a692b7bd79939a9a2c3ef97d816e749cda2f"
client: "6.19.0",
engine: "2ba551f319ab1df4bc874a89965d8b3641056773"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@@ -165,8 +166,8 @@ const config = {
"schemaEnvPath": "../../.env"
},
"relativePath": "../../prisma",
"clientVersion": "6.18.0",
"engineVersion": "34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
"clientVersion": "6.19.0",
"engineVersion": "2ba551f319ab1df4bc874a89965d8b3641056773",
"datasourceNames": [
"db"
],

View File

@@ -1,6 +1,7 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
@@ -20,12 +21,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.18.0
* Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
* Prisma Client JS version: 6.19.0
* Query Engine version: 2ba551f319ab1df4bc874a89965d8b3641056773
*/
Prisma.prismaVersion = {
client: "6.18.0",
engine: "34b5a692b7bd79939a9a2c3ef97d816e749cda2f"
client: "6.19.0",
engine: "2ba551f319ab1df4bc874a89965d8b3641056773"
}
Prisma.PrismaClientKnownRequestError = () => {

View File

@@ -219,8 +219,8 @@ export namespace Prisma {
export import Exact = $Public.Exact
/**
* Prisma Client JS version: 6.18.0
* Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
* Prisma Client JS version: 6.19.0
* Query Engine version: 2ba551f319ab1df4bc874a89965d8b3641056773
*/
export type PrismaVersion = {
client: string

View File

@@ -1,6 +1,7 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
@@ -35,12 +36,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.18.0
* Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
* Prisma Client JS version: 6.19.0
* Query Engine version: 2ba551f319ab1df4bc874a89965d8b3641056773
*/
Prisma.prismaVersion = {
client: "6.18.0",
engine: "34b5a692b7bd79939a9a2c3ef97d816e749cda2f"
client: "6.19.0",
engine: "2ba551f319ab1df4bc874a89965d8b3641056773"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@@ -166,8 +167,8 @@ const config = {
"schemaEnvPath": "../../.env"
},
"relativePath": "../../prisma",
"clientVersion": "6.18.0",
"engineVersion": "34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
"clientVersion": "6.19.0",
"engineVersion": "2ba551f319ab1df4bc874a89965d8b3641056773",
"datasourceNames": [
"db"
],

View File

@@ -151,7 +151,7 @@
},
"./*": "./*"
},
"version": "6.18.0",
"version": "6.19.0",
"sideEffects": false,
"imports": {
"#wasm-engine-loader": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm?module')

View File

@@ -1,4 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm')

View File

@@ -1,6 +1,7 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
@@ -35,12 +36,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.18.0
* Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
* Prisma Client JS version: 6.19.0
* Query Engine version: 2ba551f319ab1df4bc874a89965d8b3641056773
*/
Prisma.prismaVersion = {
client: "6.18.0",
engine: "34b5a692b7bd79939a9a2c3ef97d816e749cda2f"
client: "6.19.0",
engine: "2ba551f319ab1df4bc874a89965d8b3641056773"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@@ -165,8 +166,8 @@ const config = {
"schemaEnvPath": "../../.env"
},
"relativePath": "../../prisma",
"clientVersion": "6.18.0",
"engineVersion": "34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
"clientVersion": "6.19.0",
"engineVersion": "2ba551f319ab1df4bc874a89965d8b3641056773",
"datasourceNames": [
"db"
],

View File

@@ -11,30 +11,32 @@
},
"dependencies": {
"@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.4",
"@elysiajs/eden": "^1.4.5",
"@elysiajs/jwt": "^1.4.0",
"@elysiajs/swagger": "^1.3.1",
"@mantine/core": "^8.3.6",
"@mantine/hooks": "^8.3.6",
"@mantine/notifications": "^8.3.6",
"@prisma/client": "^6.18.0",
"@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@mantine/modals": "^8.3.8",
"@mantine/notifications": "^8.3.8",
"@prisma/client": "^6.19.0",
"@tabler/icons-react": "^3.35.0",
"@types/jwt-decode": "^3.1.0",
"add": "^2.0.6",
"elysia": "^1.4.15",
"dotenv": "^17.2.3",
"elysia": "^1.4.16",
"jwt-decode": "^4.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.5",
"react-router-dom": "^7.9.6",
"swr": "^2.3.6"
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prisma": "^6.18.0"
"prisma": "^6.19.0"
}
}

View File

@@ -2,14 +2,16 @@
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import { Notifications } from '@mantine/notifications';
import { ModalsProvider } from '@mantine/modals';
import { MantineProvider } from '@mantine/core';
import AppRoutes from './AppRoutes';
export function App() {
return <MantineProvider>
<Notifications />
<ModalsProvider>
<AppRoutes />
</ModalsProvider>
</MantineProvider>;
}

View File

@@ -1,9 +1,35 @@
import clientRoutes from "@/clientRoutes";
import { Button, Card, Container, Group, Stack, Title } from "@mantine/core";
export default function Home() {
return (
<div>
<h1>Home</h1>
</div>
<Container size={420} py={80}>
<Card shadow="sm" padding="xl" radius="md">
<Stack gap="md">
<Title order={2} ta="center">
Home
</Title>
<Group grow>
<Button
size="sm"
component="a"
href={clientRoutes["/dashboard"]}
>
Dashboard
</Button>
<Button
size="sm"
component="a"
href={clientRoutes["/login"]}
variant="light"
>
Login
</Button>
</Group>
</Stack>
</Card>
</Container>
);
}

View File

@@ -1,46 +1,71 @@
import { Button, Container, Group, Stack, Text, TextInput } from "@mantine/core";
import { Button, Card, Container, Group, PasswordInput, Stack, Text, TextInput, Title } from "@mantine/core";
import { useState } from "react";
import apiFetch from "../lib/apiFetch";
export default function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true)
setLoading(true);
try {
const response = await apiFetch.auth.login.post({
email,
password,
})
});
if (response.data?.token) {
localStorage.setItem('token', response.data.token)
window.location.href = '/dashboard'
return
localStorage.setItem('token', response.data.token);
window.location.href = '/dashboard';
return;
}
if (response.error) {
alert(JSON.stringify(response.error))
alert(JSON.stringify(response.error));
}
} catch (error) {
console.error(error)
console.error(error);
} finally {
setLoading(false)
}
setLoading(false);
}
};
return (
<Container>
<Stack>
<Text>Login</Text>
<TextInput placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
<TextInput placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
<Group justify="right">
<Button onClick={handleSubmit} disabled={loading}>Login</Button>
<Container size={420} py={80}>
<Card shadow="sm" radius="md" padding="xl">
<Stack gap="md">
<Title order={2} ta="center">
Login
</Title>
<TextInput
label="Email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<PasswordInput
label="Password"
placeholder="********"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<Group justify="flex-end" mt="sm">
<Button
onClick={handleSubmit}
loading={loading}
fullWidth
>
Login
</Button>
</Group>
</Stack>
</Card>
</Container>
)
);
}

View File

@@ -1,110 +1,222 @@
import { Button, Card, Container, Group, Stack, Table, Text, TextInput } from "@mantine/core";
import {
Button,
Card,
Container,
Group,
Stack,
Table,
Text,
TextInput,
Title,
Divider,
Loader,
} from "@mantine/core";
import { useEffect, useState } from "react";
import apiFetch from "@/lib/apiFetch";
import { showNotification } from "@mantine/notifications";
import useSwr from "swr";
import { modals } from "@mantine/modals";
export default function ApiKeyPage() {
return (
<Container size="md" w={"100%"}>
<Stack>
<Text>API Key</Text>
<Container size="md" w="100%" py="lg">
<Stack gap="lg">
<Title order={2}>API Key Management</Title>
<CreateApiKey />
<ListApiKey />
</Stack>
</Container>
);
}
function CreateApiKey() {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [expiredAt, setExpiredAt] = useState('');
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [expiredAt, setExpiredAt] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const handleSubmit = async () => {
try {
setLoading(true);
const res = await apiFetch.api.apikey.create.post({ name, description, expiredAt });
if (res.status === 200) {
setName('');
setDescription('');
setExpiredAt('');
if (!name || !description || !expiredAt) {
showNotification({
title: 'Success',
message: 'API key created successfully',
color: 'green',
})
title: "Error",
message: "All fields are required",
color: "red",
});
return;
}
const res = await apiFetch.api.apikey.create.post({
name,
description,
expiredAt,
});
if (res.status === 200) {
setName("");
setDescription("");
setExpiredAt("");
showNotification({
title: "Success",
message: "API key created successfully",
color: "green",
});
}
setLoading(false);
} catch (error) {
showNotification({
title: "Error",
message: "Failed to create API key " + JSON.stringify(error),
color: "red",
});
setLoading(false);
} finally {
setLoading(false);
}
return (
<Card >
<Stack>
<Text>API Create</Text>
<TextInput label="Name" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
<TextInput label="Description" placeholder="Description" value={description} onChange={(e) => setDescription(e.target.value)} />
<TextInput label="Expired At" placeholder="Expired At" type="date" value={expiredAt} onChange={(e) => setExpiredAt(e.target.value)} />
<Group>
<Button variant="outline" onClick={() => { setName(''); setDescription(''); setExpiredAt(''); }}>Cancel</Button>
<Button onClick={handleSubmit} type="submit" loading={loading}>Save</Button>
</Group>
};
<ListApiKey />
return (
<Card shadow="sm" radius="md" padding="lg">
<Stack gap="md">
<Title order={4}>Create API Key</Title>
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<TextInput
label="Expired At"
placeholder="Expired At"
type="date"
value={expiredAt}
onChange={(e) => setExpiredAt(e.target.value)}
/>
<Group justify="flex-end" mt="sm">
<Button
variant="outline"
onClick={() => {
setName("");
setDescription("");
setExpiredAt("");
}}
>
Cancel
</Button>
<Button onClick={handleSubmit} type="submit" loading={loading}>
Save
</Button>
</Group>
</Stack>
</Card>
);
}
function ListApiKey() {
const [apiKeys, setApiKeys] = useState<any[]>([]);
useEffect(() => {
const fetchApiKeys = async () => {
const res = await apiFetch.api.apikey.list.get();
if (res.status === 200) {
setApiKeys(res.data?.apiKeys || []);
}
}
fetchApiKeys();
}, []);
return (
<Card>
<Stack>
<Text>API List</Text>
<Table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Expired At</th>
<th>Created At</th>
<th>Updated At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{apiKeys.map((apiKey: any, index: number) => (
<tr key={index}>
<td>{apiKey.name}</td>
<td>{apiKey.description}</td>
<td>{apiKey.expiredAt.toISOString().split('T')[0]}</td>
<td>{apiKey.createdAt.toISOString().split('T')[0]}</td>
<td>{apiKey.updatedAt.toISOString().split('T')[0]}</td>
<td>
<Button variant="outline" onClick={() => {
apiFetch.api.apikey.delete.delete({ id: apiKey.id })
setApiKeys(apiKeys.filter((api: any) => api.id !== apiKey.id))
}}>Delete</Button>
<Button variant="outline" onClick={() => {
navigator.clipboard.writeText(apiKey.key)
showNotification({
title: 'Success',
message: 'API key copied to clipboard',
color: 'green',
const { data, error, isLoading, mutate } = useSwr("/", () => apiFetch.api.apikey.list.get(), {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
refreshInterval: 3000,
})
}}>Copy</Button>
</td>
</tr>
const apiKeys = data?.data?.apiKeys || []
useEffect(() => {
mutate()
}, []);
if (error) return <Text color="red">Error fetching API keys</Text>
if (isLoading) return <Loader />
return (
<Card shadow="sm" radius="md" padding="lg">
<Stack gap="md">
<Title order={4}>API Key List</Title>
<Divider />
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Description</Table.Th>
<Table.Th>Expired At</Table.Th>
<Table.Th>Created At</Table.Th>
<Table.Th style={{ width: 160 }}>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{apiKeys.map((apiKey: any, index: number) => (
<Table.Tr key={index}>
<Table.Td>{apiKey.name}</Table.Td>
<Table.Td>{apiKey.description}</Table.Td>
<Table.Td>
{apiKey.expiredAt?.toISOString().split("T")[0]}
</Table.Td>
<Table.Td>
{apiKey.createdAt?.toISOString().split("T")[0]}
</Table.Td>
<Table.Td>
<Group gap="xs">
<Button
variant="light"
size="xs"
onClick={() => {
modals.openConfirmModal({
title: "Delete API Key",
children: (
<Text>
Are you sure you want to delete this API key?
</Text>
),
labels: { confirm: "Delete", cancel: "Cancel" },
onCancel: () => { },
onConfirm: async () => {
await apiFetch.api.apikey.delete.delete({ id: apiKey.id });
mutate()
},
})
}}
>
Delete
</Button>
<Button
variant="outline"
size="xs"
onClick={() => {
navigator.clipboard.writeText(apiKey.key);
showNotification({
title: "Copied",
message: "API key copied to clipboard",
color: "green",
});
}}
>
Copy
</Button>
</Group>
</Table.Td>
</Table.Tr>
))}
</tbody>
</Table.Tbody>
</Table>
</Stack>
</Card>

View File

@@ -30,17 +30,28 @@ import { default as clientRoute, default as clientRoutes } from '@/clientRoutes'
import apiFetch from '@/lib/apiFetch'
/* ----------------------- Logout ----------------------- */
function Logout() {
return <Group>
<Button variant='transparent' size='compact-xs' onClick={async () => {
return (
<Group justify="flex-end">
<Button
variant="light"
color="red"
size="xs"
onClick={async () => {
await apiFetch.auth.logout.delete()
localStorage.removeItem('token')
window.location.href = '/login'
}}>Logout</Button>
}}
>
Logout
</Button>
</Group>
)
}
/* ----------------------- Layout ----------------------- */
export default function DashboardLayout() {
const [opened, setOpened] = useLocalStorage({
key: 'nav_open',
@@ -56,9 +67,11 @@ export default function DashboardLayout() {
collapsed: { mobile: !opened, desktop: !opened },
}}
>
<AppShell.Navbar>
{/* NAVBAR */}
<AppShell.Navbar p="sm">
{/* Collapse toggle */}
<AppShell.Section>
<Group justify="flex-end" p="xs">
<Group justify="flex-end">
<Tooltip
label={opened ? 'Collapse navigation' : 'Expand navigation'}
withArrow
@@ -67,7 +80,6 @@ export default function DashboardLayout() {
variant="light"
color="gray"
onClick={() => setOpened(v => !v)}
aria-label="Toggle navigation"
radius="xl"
>
{opened ? <IconChevronLeft /> : <IconChevronRight />}
@@ -76,18 +88,25 @@ export default function DashboardLayout() {
</Group>
</AppShell.Section>
<AppShell.Section grow component={ScrollArea} flex={1}>
{/* Navigation */}
<AppShell.Section
grow
component={ScrollArea}
mt="sm"
>
<NavigationDashboard />
</AppShell.Section>
{/* User info */}
<AppShell.Section>
<HostView />
</AppShell.Section>
</AppShell.Navbar>
{/* MAIN CONTENT */}
<AppShell.Main>
<Stack>
<Paper withBorder shadow="md" radius="lg" p="md">
<Paper withBorder radius="lg" p="md" shadow="sm">
<Flex align="center" gap="md">
{!opened && (
<Tooltip label="Open navigation menu" withArrow>
@@ -95,18 +114,19 @@ export default function DashboardLayout() {
variant="light"
color="gray"
onClick={() => setOpened(true)}
aria-label="Open navigation"
radius="xl"
>
<IconChevronRight />
</ActionIcon>
</Tooltip>
)}
<Title order={3} fw={600}>
App Dashboard
</Title>
</Flex>
</Paper>
<Outlet />
</Stack>
</AppShell.Main>
@@ -114,6 +134,7 @@ export default function DashboardLayout() {
)
}
/* ----------------------- Host Info ----------------------- */
function HostView() {
const [host, setHost] = useState<User | null>(null)
@@ -127,18 +148,20 @@ function HostView() {
}, [])
return (
<Card radius="lg" withBorder shadow="sm" p="md">
<Card radius="md" withBorder shadow="xs" p="md">
{host ? (
<Stack>
<Stack gap="sm">
<Flex gap="md" align="center">
<Avatar size="md" radius="xl" color="blue">
<Avatar size="lg" radius="xl" color="blue">
{host.name?.[0]}
</Avatar>
<Stack gap={2}>
<Text fw={600}>{host.name}</Text>
<Text size="sm" c="dimmed">{host.email}</Text>
<Text fw={600} size="sm">{host.name}</Text>
<Text size="xs" c="dimmed">{host.email}</Text>
</Stack>
</Flex>
<Divider />
<Logout />
</Stack>
@@ -161,19 +184,20 @@ function NavigationDashboard() {
location.pathname.startsWith(clientRoute[path])
return (
<Stack gap="xs" p="sm">
<Stack gap="xs">
<NavLink
active={isActive('/dashboard/landing')}
leftSection={<IconDashboard size={20} />}
leftSection={<IconDashboard size={18} />}
label="Dashboard Overview"
description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes['/dashboard/landing'])}
/>
<NavLink
active={isActive('/dashboard/apikey')}
leftSection={<IconDashboard size={20} />}
label="Dashboard Overview"
description="Quick summary and activity highlights"
leftSection={<IconDashboard size={18} />}
label="API Keys"
description="Manage your API credentials"
onClick={() => navigate(clientRoutes['/dashboard/apikey'])}
/>
</Stack>

View File

@@ -1,8 +1,120 @@
import {
AppShell,
Group,
Text,
Button,
Card,
SimpleGrid,
Table,
Stack,
Title,
Avatar,
Divider,
Container,
} from "@mantine/core";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
</div>
<Container>
<Stack gap="lg">
{/* -------- STATS SECTION -------- */}
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }}>
<Card shadow="sm" padding="lg" radius="md">
<Text size="sm" c="dimmed">
Total Users
</Text>
<Title order={3}>1,234</Title>
</Card>
<Card shadow="sm" padding="lg" radius="md">
<Text size="sm" c="dimmed">
Active Sessions
</Text>
<Title order={3}>87</Title>
</Card>
<Card shadow="sm" padding="lg" radius="md">
<Text size="sm" c="dimmed">
API Calls today
</Text>
<Title order={3}>12,490</Title>
</Card>
<Card shadow="sm" padding="lg" radius="md">
<Text size="sm" c="dimmed">
Errors
</Text>
<Title order={3}>5</Title>
</Card>
</SimpleGrid>
{/* -------- QUICK ACTIONS -------- */}
<Card shadow="sm" radius="md" padding="lg">
<Group justify="space-between" mb="sm">
<Title order={4}>Quick Actions</Title>
</Group>
<Group>
<Button>Add API Key</Button>
<Button variant="outline">Manage Users</Button>
<Button variant="light">View Logs</Button>
</Group>
</Card>
{/* -------- ACTIVITY TABLE -------- */}
<Card shadow="sm" radius="md" padding="lg">
<Stack gap="md">
<Title order={4}>Recent Activity</Title>
<Divider />
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>User</Table.Th>
<Table.Th>Action</Table.Th>
<Table.Th>Date</Table.Th>
<Table.Th>Status</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
<Table.Td>John Doe</Table.Td>
<Table.Td>Generated new API key</Table.Td>
<Table.Td>2025-01-21</Table.Td>
<Table.Td>
<Button size="xs" variant="light" color="green">
Success
</Button>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Ana Smith</Table.Td>
<Table.Td>Deleted session</Table.Td>
<Table.Td>2025-01-20</Table.Td>
<Table.Td>
<Button size="xs" variant="light" color="blue">
Info
</Button>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Michael</Table.Td>
<Table.Td>Failed login attempt</Table.Td>
<Table.Td>2025-01-19</Table.Td>
<Table.Td>
<Button size="xs" variant="light" color="red">
Error
</Button>
</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Stack>
</Card>
</Stack>
</Container>
);
}