This commit is contained in:
bipproduction
2025-11-23 17:35:59 +08:00
parent 73c281d2b1
commit 51c9c4f126
19 changed files with 1168 additions and 766 deletions

150
README.md
View File

@@ -1,90 +1,138 @@
# Full-Stack Template: Bun, ElysiaJS, React, and Prisma
# Bun React Template Starter
This is a comprehensive full-stack application template built with a modern JavaScript toolchain. It provides a solid foundation for building fast, type-safe web applications.
This template is a starting point for building modern full-stack web applications using Bun, React, ElysiaJS, and Prisma. This project is designed to provide a fast, efficient, and structured development experience with a cutting-edge technology stack.
## Features & Tech Stack
## Key Features
- **Runtime & Bundler**: [Bun](https://bun.sh/) - An incredibly fast all-in-one JavaScript runtime.
- **Backend Framework**: [ElysiaJS](https://elysiajs.com/) - A fast, ergonomic, and type-safe backend framework for Bun.
- **Frontend Library**: [React](https://react.dev/) - A popular library for building user interfaces.
- **UI Components**: [Mantine](https://mantine.dev/) - A full-featured React component library.
- **Database ORM**: [Prisma](https://www.prisma.io/) - A next-generation Node.js and TypeScript ORM.
- **Type-Safe Client**: [Eden](https://elysiajs.com/plugins/eden.html) - Creates a type-safe client from your ElysiaJS API to be used in the frontend.
- **API Documentation**: [Swagger](https://elysiajs.com/plugins/swagger.html) - Automatically generated API documentation.
- **Authentication**: JWT-based authentication middleware is included.
- **Super-Fast Runtime**: Built on top of [Bun](https://bun.sh/), a high-performance JavaScript runtime.
- **End-to-End Typesafe Backend**: Utilizes [ElysiaJS](https://elysiajs.com/) for a type-safe API from the backend to the frontend.
- **Automatic API Documentation**: Comes with [Elysia Swagger](https://elysiajs.com/plugins/swagger) to automatically generate interactive API documentation.
- **Modern Frontend**: A feature-rich and customizable user interface using [React](https://react.dev/) and [Mantine UI](https://mantine.dev/).
- **Easy Database Access**: Integrated with [Prisma](https://www.prisma.io/) as an ORM for intuitive and secure database interactions.
- **Clear Project Structure**: Logical file and folder organization to facilitate easy navigation and development.
## 🚀 Getting Started
## Tech Stack
### 1. Prerequisites
- **Runtime**: Bun
- **Backend**:
- **Framework**: ElysiaJS
- **ElysiaJS Modules**:
- `@elysiajs/cors`: Manages Cross-Origin Resource Sharing policies.
- `@elysiajs/jwt`: JSON Web Token-based authentication.
- `@elysiajs/swagger`: Creates API documentation (Swagger/OpenAPI).
- `@elysiajs/eden`: A typesafe RPC-like client to connect the frontend with the Elysia API.
- **Frontend**:
- **Library**: React
- **UI Framework**: Mantine
- **Routing**: React Router
- **Data Fetching**: SWR
- **Database**:
- **ORM**: Prisma
- **Supported Databases**: PostgreSQL (default), MySQL, SQLite, etc.
- **Language**: TypeScript
Ensure you have [Bun](https://bun.sh/docs/installation) installed on your system.
## Getting Started
### 2. Clone the Repository
### 1. Clone the Repository
```bash
git clone <repository-url>
cd bun-template
git clone https://github.com/your-username/bun-react-template-starter.git
cd bun-react-template-starter
```
### 3. Install Dependencies
### 2. Install Dependencies
Ensure you have [Bun](https://bun.sh/docs/installation) installed. Then, run the following command:
```bash
bun install
```
### 4. Set Up Environment Variables
### 3. Configure Environment Variables
Copy the example environment file and update it with your own configuration.
Copy the `.env.example` file to `.env` and customize the values.
```bash
cp .env.example .env
```
You will need to configure the following variables in the `.env` file:
Fill in your `.env` file similar to the example below:
- `DATABASE_URL`: Your PostgreSQL connection string.
- `JWT_SECRET`: A secret key for signing JWTs.
- `BUN_PUBLIC_BASE_URL`: The public base URL of your application (e.g., `http://localhost:3000`).
- `PORT`: The port the server will run on.
```
DATABASE_URL="postgresql://user:password@host:port/database?schema=public"
JWT_SECRET=a_super_long_and_secure_secret
BUN_PUBLIC_BASE_URL=http://localhost:3000
PORT=3000
```
### 5. Set Up the Database
Run the Prisma migration to create the database schema. This will also apply any pending migrations.
After that, create TypeScript type declarations for your environment variables with the provided script:
```bash
bunx prisma migrate dev
bun run generate:env
```
### 6. Seed the Database (Optional)
This command will generate a `types/env.d.ts` file based on your `.env`.
You can seed the database with initial data using the provided seed script.
### 4. Database Preparation
Make sure your PostgreSQL database server is running. Then, apply the Prisma schema to your database:
```bash
bunx prisma db seed
bunx prisma db push
```
## 📜 Available Scripts
You can also seed the database with initial data using the following script:
- **`bun dev`**: Starts the development server for both the backend and frontend with hot-reloading. The server runs on the port specified in your `.env` file (defaults to 3000).
```bash
bun run seed
```
- **`bun build`**: Builds the React frontend for production. The output is placed in the `dist/` directory.
### 5. Running the Development Server
- **`bun start`**: Starts the application in production mode. Make sure you have run `bun build` first.
```bash
bun run dev
```
## 📂 Project Structure
The application will be running at `http://localhost:3000`. The server supports hot-reloading, so changes in the code will be reflected instantly without needing a manual restart.
### 6. Accessing API Documentation (Swagger)
Once the server is running, you can access the automatically generated API documentation at:
`http://localhost:3000/swagger`
## Available Scripts
- `bun run dev`: Runs the development server with hot-reloading.
- `bun run build`: Builds the frontend application for production into the `dist` directory.
- `bun run start`: Runs the application in production mode.
- `bun run seed`: Executes the database seeding script located in `prisma/seed.ts`.
- `bun run generate:route`: A utility to create new route files in the backend.
- `bun run generate:env`: Generates a type definition file (`.d.ts`) from the variables in `.env`.
## Project Structure
```
.
├── prisma/ # Prisma schema, migrations, and seed script
├── src/
│ ├── components/ # Shared React components
│ ├── lib/ # Shared library functions (e.g., apiFetch)
│ ├── pages/ # React components for different pages/routes
│ ├── server/ # ElysiaJS backend code (routes, middlewares)
│ ├── App.tsx # Main React App component
│ ├── frontend.tsx # Frontend entry point (client-side)
│ ├── index.css # Global styles
│ ├── index.html # HTML template for the frontend
── index.tsx # Main application entry point (server-side)
└── ...
```
/
├── bin/ # Utility scripts (generators)
├── prisma/ # Database schema, migrations, and seed
├── src/ # Main source code
│ ├── App.tsx # Root application component
│ ├── clientRoutes.ts # Route definitions for the frontend
│ ├── frontend.tsx # Entry point for client-side rendering (React)
│ ├── index.css # Global CSS file
│ ├── index.html # Main HTML template
│ ├── index.tsx # Main entry point for the app (server and client)
│ ├── components/ # Reusable React components
── lib/ # Shared libraries/helpers (e.g., apiFetch)
│ ├── pages/ # React page components
│ └── server/ # Backend code (ElysiaJS)
│ ├── lib/ # Server-specific libraries (e.g., prisma client)
│ ├── middlewares/ # Middleware for the API
│ └── routes/ # API route files
└── types/ # TypeScript type definitions
```
## Contributing
Contributions are highly welcome! Please feel free to create a pull request to add features, fix bugs, or improve the documentation.

53
bin/env.generate.ts Normal file
View File

@@ -0,0 +1,53 @@
import * as fs from "fs";
import * as path from "path";
import * as dotenv from "dotenv";
interface GenerateEnvTypesOptions {
envFilePath?: string;
outputDir?: string;
outputFileName?: string;
}
export function generateEnvTypes(options: GenerateEnvTypesOptions = {}) {
const {
envFilePath = path.resolve(process.cwd(), ".env"),
outputDir = path.resolve(process.cwd(), "types"),
outputFileName = "env.d.ts",
} = options;
const outputFile = path.join(outputDir, outputFileName);
// 1. Baca .env
if (!fs.existsSync(envFilePath)) {
console.warn(`⚠️ .env file not found at: ${envFilePath}`);
return;
}
const envContent = fs.readFileSync(envFilePath, "utf-8");
const parsed = dotenv.parse(envContent);
// 2. Generate TypeScript declare
const lines = Object.keys(parsed).map((key) => ` ${key}?: string;`);
const fileContent = `declare namespace NodeJS {
interface ProcessEnv {
${lines.join("\n")}
}
}
`;
// 3. Buat folder kalau belum ada
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 4. Tulis file
fs.writeFileSync(outputFile, fileContent, "utf-8");
console.log(`✅ Env types generated at: ${outputFile}`);
}
if (import.meta.main) {
generateEnvTypes();
}

260
bin/route.generate.ts Normal file
View File

@@ -0,0 +1,260 @@
#!/usr/bin/env bun
import fs from "fs";
import path from "path";
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import * as t from "@babel/types";
import { readdirSync, statSync, writeFileSync } from "fs";
import _ from "lodash";
import { basename, extname, join, relative } from "path";
const PAGES_DIR = join(process.cwd(), "src/pages");
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
/**
* ✅ Ubah nama file menjadi PascalCase
* - Support: snake_case, kebab-case, camelCase, PascalCase
*/
const toComponentName = (fileName: string): string => {
return fileName
.replace(/\.[^/.]+$/, "") // hilangkan ekstensi file
.replace(/[_-]+/g, " ") // snake_case & kebab-case → spasi
.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase → spasi
.replace(/\b\w/g, (c) => c.toUpperCase()) // kapital tiap kata
.replace(/\s+/g, ""); // gabung semua → PascalCase
};
/**
* ✅ Normalisasi nama menjadi path route (kebab-case)
*/
function toRoutePath(name: string): string {
name = name.replace(/\.[^/.]+$/, ""); // hapus ekstensi
if (name.toLowerCase() === "home") return "/";
if (name.toLowerCase() === "login") return "/login";
if (name.toLowerCase() === "notfound") return "/*";
// Hapus prefix/suffix umum
name = name.replace(/_page$/i, "").replace(/^form_/i, "");
// ✅ Normalisasi ke kebab-case
return _.kebabCase(name);
}
// 🧭 Scan folder pages secara rekursif
function scan(dir: string): any[] {
const items = readdirSync(dir);
const routes: any[] = [];
for (const item of items) {
const full = join(dir, item);
const stat = statSync(full);
if (stat.isDirectory()) {
routes.push({
name: item,
path: _.kebabCase(item),
children: scan(full),
});
} else if (extname(item) === ".tsx") {
routes.push({
name: basename(item, ".tsx"),
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
});
}
}
return routes;
}
// 🏗️ Generate <Route> JSX dari struktur folder
function generateJSX(routes: any[], parentPath = ""): string {
let jsx = "";
for (const route of routes) {
if (route.children) {
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
if (layout) {
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
const nested = route.children.filter((r: any) => r !== layout);
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
const homeFile = route.children.find((r: any) =>
r.name.toLowerCase().endsWith("_home")
);
const indexRoute = homeFile
? `<Route index element={<${toComponentName(homeFile.name)} />} />\n`
: "";
jsx += `
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
${indexRoute}
${nestedRoutes}
</Route>
`;
} else {
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
}
} else {
const Component = toComponentName(route.name);
const routePath = toRoutePath(route.name);
const fullPath = routePath.startsWith("/")
? routePath
: `${parentPath}/${routePath}`.replace(/\/+/g, "/");
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
}
}
return jsx;
}
// 🧾 Generate import otomatis
function generateImports(routes: any[]): string {
const imports = new Set<string>();
function collect(rs: any[]) {
for (const r of rs) {
if (r.children) collect(r.children);
else {
const Comp = toComponentName(r.name);
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
}
}
}
collect(routes);
return Array.from(imports).join("\n");
}
function generateRoutes() {
const allRoutes = scan(PAGES_DIR);
const imports = generateImports(allRoutes);
const jsxRoutes = generateJSX(allRoutes);
const finalCode = `
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
import { BrowserRouter, Routes, Route } from "react-router-dom";
${imports}
export default function AppRoutes() {
return (
<BrowserRouter>
<Routes>
${jsxRoutes}
</Routes>
</BrowserRouter>
);
}
`;
writeFileSync(OUTPUT_FILE, finalCode);
console.log(`✅ Routes generated → ${OUTPUT_FILE}`);
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"]);
}
// --- Extract untuk clientRoutes.ts ---
const SRC_DIR = path.resolve(process.cwd(), "src");
const APP_ROUTES_FILE = path.join(SRC_DIR, "AppRoutes.tsx");
interface RouteNode {
path: string;
children: RouteNode[];
}
function getAttributePath(attrs: (t.JSXAttribute | t.JSXSpreadAttribute)[]) {
const pathAttr = attrs.find(
(attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "path" })
) as t.JSXAttribute | undefined;
if (pathAttr && t.isStringLiteral(pathAttr.value)) return pathAttr.value.value;
return "";
}
function extractRouteNodes(node: t.JSXElement): RouteNode | null {
const opening = node.openingElement;
if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "Route") return null;
const currentPath = getAttributePath(opening.attributes);
const children: RouteNode[] = [];
for (const child of node.children) {
if (t.isJSXElement(child)) {
const childNode = extractRouteNodes(child);
if (childNode) children.push(childNode);
}
}
return { path: currentPath, children };
}
function flattenRoutes(node: RouteNode, parentPath = ""): Record<string, string> {
const record: Record<string, string> = {};
let fullPath = node.path;
if (fullPath) {
if (!fullPath.startsWith("/")) {
if (parentPath) {
if (fullPath === "/") fullPath = parentPath;
else fullPath = `${parentPath.replace(/\/$/, "")}/${fullPath}`;
}
if (!fullPath.startsWith("/")) fullPath = `/${fullPath}`;
}
fullPath = fullPath.replace(/\/+/g, "/");
record[fullPath] = fullPath;
}
for (const child of node.children) {
Object.assign(record, flattenRoutes(child, fullPath || parentPath));
}
return record;
}
function extractRoutes(code: string): Record<string, string> {
const ast = parser.parse(code, {
sourceType: "module",
plugins: ["typescript", "jsx"],
});
const routes: Record<string, string> = {};
traverse(ast, {
JSXElement(path) {
const opening = path.node.openingElement;
if (t.isJSXIdentifier(opening.name) && opening.name.name === "Routes") {
for (const child of path.node.children) {
if (t.isJSXElement(child)) {
const node = extractRouteNodes(child);
if (node) Object.assign(routes, flattenRoutes(node));
}
}
}
},
});
return routes;
}
export default function route() {
generateRoutes();
if (!fs.existsSync(APP_ROUTES_FILE)) {
console.error("❌ AppRoutes.tsx not found in src/");
process.exit(1);
}
const code = fs.readFileSync(APP_ROUTES_FILE, "utf-8");
const routes = extractRoutes(code);
console.log("✅ Generated Routes:");
console.log(routes);
const outPath = path.join(SRC_DIR, "clientRoutes.ts");
fs.writeFileSync(
outPath,
`// AUTO-GENERATED FILE\nconst clientRoutes = ${JSON.stringify(routes, null, 2)} as const;\n\nexport default clientRoutes;`
);
console.log(`📄 clientRoutes.ts saved → ${outPath}`);
}
route()

196
bun.lock
View File

@@ -14,18 +14,24 @@
"@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",
"dotenv": "^17.2.3",
"elysia": "^1.4.16",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"swr": "^2.3.6",
},
"devDependencies": {
"@babel/parser": "^7.28.5",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@types/babel__traverse": "^7.28.0",
"@types/bun": "latest",
"@types/jwt-decode": "^3.1.0",
"@types/lodash": "^4.17.21",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
@@ -36,37 +42,63 @@
},
},
"packages": {
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, ""],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, ""],
"@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=="],
"@elysiajs/jwt": ["@elysiajs/jwt@1.4.0", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, ""],
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, ""],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, ""],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, ""],
"@floating-ui/react": ["@floating-ui/react@0.27.16", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g=="],
"@floating-ui/react": ["@floating-ui/react@0.27.16", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, ""],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, ""],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, ""],
"@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=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@mantine/hooks": ["@mantine/hooks@8.3.8", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-2YCUk5IWz+Ebi7VpbdscUz1MwulyaVPKr236ugMfpK0PFwsun4aBaLCAc8UeMGP0LtoSkuFvnsCPR4U6rhNfeQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@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=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@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=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mantine/store": ["@mantine/store@8.3.8", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-B6LEed839OR2t9pnC7Bl3zhMyYzUvJZ46YaOpH9zCqLiFX+u4FKC+UCNzqkz2a+I+olrNlONLnrCA0NDTCjz9A=="],
"@mantine/core": ["@mantine/core@8.3.9", "", { "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.9", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-ivj0Crn5N521cI2eWZBsBGckg0ZYRqfOJz5vbbvYmfj65bp0EdsyqZuOxXzIcn2aUScQhskfvzyhV5XIUv81PQ=="],
"@mantine/hooks": ["@mantine/hooks@8.3.9", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-Dfz7W0+K1cq4Gb1WFQCZn8tsMXkLH6MV409wZR/ToqsxdNDUMJ/xxbfnwEXWEZjXNJd1wDETHgc+cZG2lTe3Xw=="],
"@mantine/modals": ["@mantine/modals@8.3.9", "", { "peerDependencies": { "@mantine/core": "8.3.9", "@mantine/hooks": "8.3.9", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-0WOikHgECJeWA/1TNf+sxOnpNwQjmpyph3XEhzFkgneimW6Ry7R6qd/i345CDLSu6kP6FGGRI73SUROiTcu2Ng=="],
"@mantine/notifications": ["@mantine/notifications@8.3.9", "", { "dependencies": { "@mantine/store": "8.3.9", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.9", "@mantine/hooks": "8.3.9", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-emUdoCyaccf/NuNmJ4fQgloJ7hEod0Pde7XIoD9xUUztVchL143oWRU2gYm6cwqzSyjpjTaqPXfz5UvEBRYjZw=="],
"@mantine/store": ["@mantine/store@8.3.9", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-Z4tYW597mD3NxHLlJ3OJ1aKucmwrD9nhqobz+142JNw01aHqzKjxVXlu3L5GGa7F3u3OjXJk/qb1QmUs4sU+Jw=="],
"@prisma/client": ["@prisma/client@6.19.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g=="],
@@ -82,13 +114,13 @@
"@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=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, ""],
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, ""],
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, ""],
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, ""],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
@@ -96,43 +128,47 @@
"@tabler/icons-react": ["@tabler/icons-react@3.35.0", "", { "dependencies": { "@tabler/icons": "3.35.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
"@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/lodash": ["@types/lodash@4.17.21", "", {}, "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ=="],
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, ""],
"@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="],
"@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=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, ""],
"add": ["add@2.0.6", "", {}, "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q=="],
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, ""],
"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=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, ""],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"clsx": ["clsx@2.1.1", "", {}, ""],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"cookie": ["cookie@1.0.2", "", {}, ""],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, ""],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -146,7 +182,7 @@
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, ""],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
@@ -160,41 +196,43 @@
"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=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, ""],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-type": ["file-type@21.1.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="],
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, ""],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"hookable": ["hookable@5.5.3", "", {}, ""],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
"jose": ["jose@6.1.0", "", {}, ""],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
@@ -204,31 +242,31 @@
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, ""],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"pathe": ["pathe@1.1.2", "", {}, ""],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picocolors": ["picocolors@1.1.1", "", {}, ""],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"picomatch": ["picomatch@4.0.3", "", {}, ""],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""],
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, ""],
"postcss-mixins": ["postcss-mixins@12.1.2", "", { "dependencies": { "postcss-js": "^4.0.1", "postcss-simple-vars": "^7.0.1", "sugarss": "^5.0.0", "tinyglobby": "^0.2.14" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg=="],
"postcss-mixins": ["postcss-mixins@12.1.2", "", { "dependencies": { "postcss-js": "^4.0.1", "postcss-simple-vars": "^7.0.1", "sugarss": "^5.0.0", "tinyglobby": "^0.2.14" }, "peerDependencies": { "postcss": "^8.2.14" } }, ""],
"postcss-nested": ["postcss-nested@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw=="],
"postcss-nested": ["postcss-nested@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, ""],
"postcss-preset-mantine": ["postcss-preset-mantine@1.18.0", "", { "dependencies": { "postcss-mixins": "^12.0.0", "postcss-nested": "^7.0.2" }, "peerDependencies": { "postcss": ">=8.0.0" } }, "sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA=="],
"postcss-preset-mantine": ["postcss-preset-mantine@1.18.0", "", { "dependencies": { "postcss-mixins": "^12.0.0", "postcss-nested": "^7.0.2" }, "peerDependencies": { "postcss": ">=8.0.0" } }, ""],
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, ""],
"postcss-simple-vars": ["postcss-simple-vars@7.0.1", "", { "peerDependencies": { "postcss": "^8.2.1" } }, "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A=="],
"postcss-simple-vars": ["postcss-simple-vars@7.0.1", "", { "peerDependencies": { "postcss": "^8.2.1" } }, ""],
"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=="],
@@ -238,92 +276,90 @@
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react": ["react@19.2.0", "", {}, ""],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, ""],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-number-format": ["react-number-format@5.4.4", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA=="],
"react-number-format": ["react-number-format@5.4.4", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, ""],
"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-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" } }, ""],
"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.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=="],
"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" } }, ""],
"react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
"react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"scheduler": ["scheduler@0.27.0", "", {}, ""],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, ""],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, ""],
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
"sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
"sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, ""],
"swr": ["swr@2.3.6", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw=="],
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
"tabbable": ["tabbable@6.2.0", "", {}, ""],
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, ""],
"token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tslib": ["tslib@2.8.1", "", {}, ""],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-fest": ["type-fest@4.41.0", "", {}, ""],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"undici-types": ["undici-types@7.14.0", "", {}, ""],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "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-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, ""],
"use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="],
"use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
"use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="],
"use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.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-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.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" } }, ""],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, ""],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zhead": ["zhead@2.2.4", "", {}, ""],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod": ["zod@3.25.76", "", {}, ""],
"@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=="],
"@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" } }, ""],
"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=="],
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, ""],
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": "bin/nanoid.js" }, ""],
}
}

View File

@@ -172,6 +172,7 @@ const config = {
"db"
],
"activeProvider": "postgresql",
"postinstall": true,
"inlineDatasources": {
"db": {
"url": {

View File

@@ -173,6 +173,7 @@ const config = {
"db"
],
"activeProvider": "postgresql",
"postinstall": true,
"inlineDatasources": {
"db": {
"url": {

View File

@@ -172,6 +172,7 @@ const config = {
"db"
],
"activeProvider": "postgresql",
"postinstall": true,
"inlineDatasources": {
"db": {
"url": {

View File

@@ -1,5 +1,5 @@
{
"name": "bun-react-template",
"name": "bun-react-template-starter",
"version": "0.1.0",
"private": true,
"type": "module",
@@ -7,7 +7,9 @@
"dev": "bun --hot src/index.tsx",
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
"start": "NODE_ENV=production bun src/index.tsx",
"seed": "bun prisma/seed.ts"
"seed": "bun prisma/seed.ts",
"generate:route": "bun bin/route.generate.ts",
"generate:env": "bun bin/env.generate.ts"
},
"dependencies": {
"@elysiajs/cors": "^1.4.0",
@@ -20,18 +22,24 @@
"@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",
"dotenv": "^17.2.3",
"elysia": "^1.4.16",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"swr": "^2.3.6"
},
"devDependencies": {
"@babel/parser": "^7.28.5",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@types/babel__traverse": "^7.28.0",
"@types/bun": "latest",
"@types/jwt-decode": "^3.1.0",
"@types/lodash": "^4.17.21",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
@@ -39,4 +47,4 @@
"postcss-simple-vars": "^7.0.1",
"prisma": "^6.19.0"
}
}
}

View File

@@ -1,17 +1,17 @@
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';
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>;
return (
<MantineProvider>
<Notifications />
<ModalsProvider>
<AppRoutes />
</ModalsProvider>
</MantineProvider>
);
}

View File

@@ -1,32 +1,25 @@
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import NotFound from "./pages/NotFound";
import Login from "./pages/Login";
import ProtectedRoute from "./components/ProtectedRoute";
import Dashboard from "./pages/dashboard/dashboard_page";
import Home from "./pages/Home";
import ApikeyPage from "./pages/dashboard/apikey/apikey_page";
import DashboardPage from "./pages/dashboard/dashboard_page";
import DashboardLayout from "./pages/dashboard/dashboard_layout";
import ApiKeyPage from "./pages/dashboard/apikey/apikey_page";
import NotFound from "./pages/NotFound";
export default function AppRoutes() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Dashboard />} />
<Route path="landing" element={<Dashboard />} />
<Route path="apikey" element={<ApiKeyPage />} />
</Route>
</Route>
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Home />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="/dashboard/apikey/apikey" element={<ApikeyPage />} />
<Route path="/dashboard/dashboard" element={<DashboardPage />} />
</Route>
<Route path="/*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}

View File

@@ -1,10 +1,10 @@
// AUTO-GENERATED FILE
const clientRoutes = {
"/": "/",
"/login": "/login",
"/": "/",
"/dashboard": "/dashboard",
"/dashboard/landing": "/dashboard/landing",
"/dashboard/apikey": "/dashboard/apikey",
"/dashboard/apikey/apikey": "/dashboard/apikey/apikey",
"/dashboard/dashboard": "/dashboard/dashboard",
"/*": "/*"
} as const;

View File

@@ -1,25 +1,25 @@
import { useEffect, useState } from 'react'
import { Navigate, Outlet } from 'react-router-dom'
import clientRoutes from '@/clientRoutes'
import apiFetch from '@/lib/apiFetch'
import { useEffect, useState } from "react";
import { Navigate, Outlet } from "react-router-dom";
import clientRoutes from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
export default function ProtectedRoute() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
useEffect(() => {
async function checkSession() {
try {
// backend otomatis baca cookie JWT dari request
const res = await apiFetch.api.user.find.get()
setIsAuthenticated(res.status === 200)
const res = await apiFetch.api.user.find.get();
setIsAuthenticated(res.status === 200);
} catch {
setIsAuthenticated(false)
setIsAuthenticated(false);
}
}
checkSession()
}, [])
checkSession();
}, []);
if (isAuthenticated === null) return null // or loading spinner
if (!isAuthenticated) return <Navigate to={clientRoutes['/login']} replace />
return <Outlet />
if (isAuthenticated === null) return null; // or loading spinner
if (!isAuthenticated) return <Navigate to={clientRoutes["/login"]} replace />;
return <Outlet />;
}

View File

@@ -1,27 +1,26 @@
import Elysia, { t } from "elysia";
import Swagger from "@elysiajs/swagger";
import html from "./index.html"
import html from "./index.html";
import Dashboard from "./server/routes/darmasaba";
import { apiAuth } from "./server/middlewares/apiAuth";
import Auth from "./server/routes/auth_route";
import ApiKeyRoute from "./server/routes/apikey_route";
import type { User } from "generated/prisma";
const Docs = new Elysia()
.use(Swagger({
const Docs = new Elysia().use(
Swagger({
path: "/docs",
}))
}),
);
const ApiUser = new Elysia({
prefix: "/user",
})
.get('/find', (ctx) => {
const { user } = ctx as any
return {
user: user as User
}
})
}).get("/find", (ctx) => {
const { user } = ctx as any;
return {
user: user as User,
};
});
const Api = new Elysia({
prefix: "/api",
@@ -29,7 +28,7 @@ const Api = new Elysia({
.use(apiAuth)
.use(ApiKeyRoute)
.use(Dashboard)
.use(ApiUser)
.use(ApiUser);
const app = new Elysia()
.use(Api)
@@ -40,6 +39,4 @@ const app = new Elysia()
console.log("Server running at http://localhost:3000");
});
export type ServerApp = typeof app;

View File

@@ -2,34 +2,30 @@ import clientRoutes from "@/clientRoutes";
import { Button, Card, Container, Group, Stack, Title } from "@mantine/core";
export default function Home() {
return (
<Container size={420} py={80}>
<Card shadow="sm" padding="xl" radius="md">
<Stack gap="md">
<Title order={2} ta="center">
Home
</Title>
return (
<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>
<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>
);
<Button
size="sm"
component="a"
href={clientRoutes["/login"]}
variant="light"
>
Login
</Button>
</Group>
</Stack>
</Card>
</Container>
);
}

View File

@@ -1,71 +1,77 @@
import { Button, Card, Container, Group, PasswordInput, Stack, Text, TextInput, Title } 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);
try {
const response = await apiFetch.auth.login.post({
email,
password,
});
const handleSubmit = async () => {
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;
}
if (response.data?.token) {
localStorage.setItem("token", response.data.token);
window.location.href = "/dashboard";
return;
}
if (response.error) {
alert(JSON.stringify(response.error));
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
if (response.error) {
alert(JSON.stringify(response.error));
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
return (
<Container size={420} py={80}>
<Card shadow="sm" radius="md" padding="xl">
<Stack gap="md">
<Title order={2} ta="center">
Login
</Title>
return (
<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
/>
<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
/>
<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>
);
<Group justify="flex-end" mt="sm">
<Button onClick={handleSubmit} loading={loading} fullWidth>
Login
</Button>
</Group>
</Stack>
</Card>
</Container>
);
}

View File

@@ -1,9 +1,7 @@
export default function NotFound() {
return (
<div>
<h1>404 Not Found</h1>
</div>
);
return (
<div>
<h1>404 Not Found</h1>
</div>
);
}

View File

@@ -1,15 +1,15 @@
import {
Button,
Card,
Container,
Group,
Stack,
Table,
Text,
TextInput,
Title,
Divider,
Loader,
Button,
Card,
Container,
Group,
Stack,
Table,
Text,
TextInput,
Title,
Divider,
Loader,
} from "@mantine/core";
import { useEffect, useState } from "react";
import apiFetch from "@/lib/apiFetch";
@@ -18,207 +18,213 @@ import useSwr from "swr";
import { modals } from "@mantine/modals";
export default function ApiKeyPage() {
return (
<Container size="md" w="100%" py="lg">
<Stack gap="lg">
<Title order={2}>API Key Management</Title>
<CreateApiKey />
<ListApiKey />
</Stack>
</Container>
);
return (
<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 [loading, setLoading] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [expiredAt, setExpiredAt] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
setLoading(true);
const handleSubmit = async () => {
try {
setLoading(true);
if (!name || !description || !expiredAt) {
showNotification({
title: "Error",
message: "All fields are required",
color: "red",
});
return;
}
if (!name || !description || !expiredAt) {
showNotification({
title: "Error",
message: "All fields are required",
color: "red",
});
return;
}
const res = await apiFetch.api.apikey.create.post({
name,
description,
expiredAt,
});
const res = await apiFetch.api.apikey.create.post({
name,
description,
expiredAt,
});
if (res.status === 200) {
setName("");
setDescription("");
setExpiredAt("");
if (res.status === 200) {
setName("");
setDescription("");
setExpiredAt("");
showNotification({
title: "Success",
message: "API key created successfully",
color: "green",
});
}
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);
}
};
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 shadow="sm" radius="md" padding="lg">
<Stack gap="md">
<Title order={4}>Create API Key</Title>
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="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="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)}
/>
<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>
<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>
);
<Button onClick={handleSubmit} type="submit" loading={loading}>
Save
</Button>
</Group>
</Stack>
</Card>
);
}
function ListApiKey() {
const { data, error, isLoading, mutate } = useSwr("/", () => apiFetch.api.apikey.list.get(), {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
refreshInterval: 3000,
})
const apiKeys = data?.data?.apiKeys || []
const { data, error, isLoading, mutate } = useSwr(
"/",
() => apiFetch.api.apikey.list.get(),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
refreshInterval: 3000,
},
);
const apiKeys = data?.data?.apiKeys || [];
useEffect(() => {
mutate()
}, []);
useEffect(() => {
mutate();
}, []);
if (error) return <Text color="red">Error fetching API keys</Text>
if (isLoading) return <Loader />
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>
return (
<Card shadow="sm" radius="md" padding="lg">
<Stack gap="md">
<Title order={4}>API Key List</Title>
<Divider />
<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 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>
<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>
))}
</Table.Tbody>
</Table>
</Stack>
</Card>
);
<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>
))}
</Table.Tbody>
</Table>
</Stack>
</Card>
);
}

View File

@@ -1,205 +1,204 @@
import { useEffect, useState } from 'react'
import { useEffect, useState } from "react";
import {
ActionIcon,
AppShell,
Avatar,
Button,
Card,
Divider,
Flex,
Group,
NavLink,
Paper,
ScrollArea,
Stack,
Text,
Title,
Tooltip
} from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
ActionIcon,
AppShell,
Avatar,
Button,
Card,
Divider,
Flex,
Group,
NavLink,
Paper,
ScrollArea,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import {
IconChevronLeft,
IconChevronRight,
IconDashboard
} from '@tabler/icons-react'
import type { User } from 'generated/prisma'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { default as clientRoute, default as clientRoutes } from '@/clientRoutes'
import apiFetch from '@/lib/apiFetch'
IconChevronLeft,
IconChevronRight,
IconDashboard,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
default as clientRoute,
default as clientRoutes,
} from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
/* ----------------------- Logout ----------------------- */
function Logout() {
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>
</Group>
)
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>
</Group>
);
}
/* ----------------------- Layout ----------------------- */
export default function DashboardLayout() {
const [opened, setOpened] = useLocalStorage({
key: 'nav_open',
defaultValue: true,
})
const [opened, setOpened] = useLocalStorage({
key: "nav_open",
defaultValue: true,
});
return (
<AppShell
padding="md"
navbar={{
width: 260,
breakpoint: 'sm',
collapsed: { mobile: !opened, desktop: !opened },
}}
>
{/* NAVBAR */}
<AppShell.Navbar p="sm">
{/* Collapse toggle */}
<AppShell.Section>
<Group justify="flex-end">
<Tooltip
label={opened ? 'Collapse navigation' : 'Expand navigation'}
withArrow
>
<ActionIcon
variant="light"
color="gray"
onClick={() => setOpened(v => !v)}
radius="xl"
>
{opened ? <IconChevronLeft /> : <IconChevronRight />}
</ActionIcon>
</Tooltip>
</Group>
</AppShell.Section>
return (
<AppShell
padding="md"
navbar={{
width: 260,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: !opened },
}}
>
{/* NAVBAR */}
<AppShell.Navbar p="sm">
{/* Collapse toggle */}
<AppShell.Section>
<Group justify="flex-end">
<Tooltip
label={opened ? "Collapse navigation" : "Expand navigation"}
withArrow
>
<ActionIcon
variant="light"
color="gray"
onClick={() => setOpened((v) => !v)}
radius="xl"
>
{opened ? <IconChevronLeft /> : <IconChevronRight />}
</ActionIcon>
</Tooltip>
</Group>
</AppShell.Section>
{/* Navigation */}
<AppShell.Section
grow
component={ScrollArea}
mt="sm"
>
<NavigationDashboard />
</AppShell.Section>
{/* Navigation */}
<AppShell.Section grow component={ScrollArea} mt="sm">
<NavigationDashboard />
</AppShell.Section>
{/* User info */}
<AppShell.Section>
<HostView />
</AppShell.Section>
</AppShell.Navbar>
{/* User info */}
<AppShell.Section>
<HostView />
</AppShell.Section>
</AppShell.Navbar>
{/* MAIN CONTENT */}
<AppShell.Main>
<Stack>
<Paper withBorder radius="lg" p="md" shadow="sm">
<Flex align="center" gap="md">
{!opened && (
<Tooltip label="Open navigation menu" withArrow>
<ActionIcon
variant="light"
color="gray"
onClick={() => setOpened(true)}
radius="xl"
>
<IconChevronRight />
</ActionIcon>
</Tooltip>
)}
{/* MAIN CONTENT */}
<AppShell.Main>
<Stack>
<Paper withBorder radius="lg" p="md" shadow="sm">
<Flex align="center" gap="md">
{!opened && (
<Tooltip label="Open navigation menu" withArrow>
<ActionIcon
variant="light"
color="gray"
onClick={() => setOpened(true)}
radius="xl"
>
<IconChevronRight />
</ActionIcon>
</Tooltip>
)}
<Title order={3} fw={600}>
App Dashboard
</Title>
</Flex>
</Paper>
<Title order={3} fw={600}>
App Dashboard
</Title>
</Flex>
</Paper>
<Outlet />
</Stack>
</AppShell.Main>
</AppShell>
)
<Outlet />
</Stack>
</AppShell.Main>
</AppShell>
);
}
/* ----------------------- Host Info ----------------------- */
function HostView() {
const [host, setHost] = useState<User | null>(null)
const [host, setHost] = useState<User | null>(null);
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get()
setHost(data?.user ?? null)
}
fetchHost()
}, [])
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get();
setHost(data?.user ?? null);
}
fetchHost();
}, []);
return (
<Card radius="md" withBorder shadow="xs" p="md">
{host ? (
<Stack gap="sm">
<Flex gap="md" align="center">
<Avatar size="lg" radius="xl" color="blue">
{host.name?.[0]}
</Avatar>
return (
<Card radius="md" withBorder shadow="xs" p="md">
{host ? (
<Stack gap="sm">
<Flex gap="md" align="center">
<Avatar size="lg" radius="xl" color="blue">
{host.name?.[0]}
</Avatar>
<Stack gap={2}>
<Text fw={600} size="sm">{host.name}</Text>
<Text size="xs" c="dimmed">{host.email}</Text>
</Stack>
</Flex>
<Stack gap={2}>
<Text fw={600} size="sm">
{host.name}
</Text>
<Text size="xs" c="dimmed">
{host.email}
</Text>
</Stack>
</Flex>
<Divider />
<Logout />
</Stack>
) : (
<Text size="sm" c="dimmed" ta="center">
No host information available
</Text>
)}
</Card>
)
<Divider />
<Logout />
</Stack>
) : (
<Text size="sm" c="dimmed" ta="center">
No host information available
</Text>
)}
</Card>
);
}
/* ----------------------- Navigation ----------------------- */
function NavigationDashboard() {
const navigate = useNavigate()
const location = useLocation()
const navigate = useNavigate();
const location = useLocation();
const isActive = (path: keyof typeof clientRoute) =>
location.pathname.startsWith(clientRoute[path])
const isActive = (path: keyof typeof clientRoute) =>
location.pathname.startsWith(clientRoute[path]);
return (
<Stack gap="xs">
<NavLink
active={isActive('/dashboard/landing')}
leftSection={<IconDashboard size={18} />}
label="Dashboard Overview"
description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes['/dashboard/landing'])}
/>
return (
<Stack gap="xs">
<NavLink
active={isActive("/dashboard/dashboard")}
leftSection={<IconDashboard size={18} />}
label="Dashboard Overview"
description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes["/dashboard/dashboard"])}
/>
<NavLink
active={isActive('/dashboard/apikey')}
leftSection={<IconDashboard size={18} />}
label="API Keys"
description="Manage your API credentials"
onClick={() => navigate(clientRoutes['/dashboard/apikey'])}
/>
</Stack>
)
<NavLink
active={isActive("/dashboard/apikey/apikey")}
leftSection={<IconDashboard size={18} />}
label="API Keys"
description="Manage your API credentials"
onClick={() => navigate(clientRoutes["/dashboard/apikey/apikey"])}
/>
</Stack>
);
}

View File

@@ -1,121 +1,120 @@
import {
AppShell,
Group,
Text,
Button,
Card,
SimpleGrid,
Table,
Stack,
Title,
Avatar,
Divider,
Container,
AppShell,
Group,
Text,
Button,
Card,
SimpleGrid,
Table,
Stack,
Title,
Avatar,
Divider,
Container,
} from "@mantine/core";
export default function Dashboard() {
return (
<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>
return (
<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">
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">
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>
<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>
{/* -------- 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>
<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 />
{/* -------- 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 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.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>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>
);
<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>
);
}