tambahan
This commit is contained in:
150
README.md
150
README.md
@@ -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
53
bin/env.generate.ts
Normal 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
260
bin/route.generate.ts
Normal 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
196
bun.lock
@@ -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" }, ""],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ const config = {
|
||||
"db"
|
||||
],
|
||||
"activeProvider": "postgresql",
|
||||
"postinstall": true,
|
||||
"inlineDatasources": {
|
||||
"db": {
|
||||
"url": {
|
||||
|
||||
@@ -173,6 +173,7 @@ const config = {
|
||||
"db"
|
||||
],
|
||||
"activeProvider": "postgresql",
|
||||
"postinstall": true,
|
||||
"inlineDatasources": {
|
||||
"db": {
|
||||
"url": {
|
||||
|
||||
@@ -172,6 +172,7 @@ const config = {
|
||||
"db"
|
||||
],
|
||||
"activeProvider": "postgresql",
|
||||
"postinstall": true,
|
||||
"inlineDatasources": {
|
||||
"db": {
|
||||
"url": {
|
||||
|
||||
16
package.json
16
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/App.tsx
28
src/App.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div>
|
||||
<h1>404 Not Found</h1>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h1>404 Not Found</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user