diff --git a/.github/workflows/formatLintCheck.yaml b/.github/workflows/formatLintCheck.yaml
index 79f9c86..066673b 100644
--- a/.github/workflows/formatLintCheck.yaml
+++ b/.github/workflows/formatLintCheck.yaml
@@ -33,4 +33,30 @@ jobs:
- name: Check formatting
run: npm run check
+ lint_front:
+ name: Lint & Format Check (Frontend)
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: frontend
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 25
+ cache: npm
+ cache-dependency-path: frontend/package-lock.json
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run ESLint
+ run: npm run lint
+
+ - name: Check formatting
+ run: npm run check
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..00ede22
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,145 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.*
+!.env.example
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Sveltekit cache directory
+.svelte-kit/
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Firebase cache directory
+.firebase/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v3
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# Vite logs files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+.vercel
+.env*.local
+
+# dumps
+backend/dumps/*.txt
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000..9e4c08a
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "all"
+}
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..f958397
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,224 @@
+# CTF Platform
+
+A platform powered by **AWS** to host and solve **Capture The Flag (CTF)** challenges. This platform allows users to launch isolated Docker containers for each challenge, providing a secure environment for solving CTF problems.
+
+## Features
+
+- User authentication and authorization (JWT)
+- Dynamic Docker container management for CTF challenges
+- Flag submission and tracking
+- Automatic cleanup of expired instances
+- User progress tracking
+
+## Quick Start
+
+### Prerequisites
+
+- Node.js 25.x
+- MongoDB 6.0+
+- Docker 20.10+
+
+### Backend Setup
+
+1. **Clone the repository**
+ ```bash
+ git clone https://github.com/sfeedbackx/ctf_platform.git
+ cd ctf_platform/backend
+ ```
+
+2. **Install dependencies**
+ ```bash
+ npm install
+ ```
+
+3. **Configure environment**
+ Create a `.env` file in the `backend/` directory:
+ ```env
+ PORT=3000
+ NODE_ENV=development
+ SERVER_HOST=localhost
+ DB_URL=mongodb://localhost:27017/ctf_platform
+ SECRET=your-secret-key-change-this-in-production
+ MAX_AGE=604800000
+ ```
+
+4. **Start MongoDB and Docker**
+ ```bash
+ # Start MongoDB (Linux/Mac)
+ sudo systemctl start mongod
+
+ # Verify Docker is running
+ docker ps
+ ```
+
+5. **Run the server**
+ ```bash
+ # Development mode (with hot reload)
+ npm run dev
+
+ # Production mode
+ npm run build
+ npm start
+ ```
+
+The backend API will be available at `http://localhost:3000`
+
+### Frontend Setup
+
+**Note**: The frontend is a React application. Setup instructions:
+
+1. **Navigate to frontend directory**
+ ```bash
+ cd frontend
+ ```
+
+2. **Install dependencies**
+ ```bash
+ npm install
+ ```
+
+3. **Configure environment**
+ Create a `.env` file:
+ ```env
+ VITE_API_URL=http://localhost:3000/api/v1
+ # or REACT_APP_API_URL (depending on your build tool)
+ ```
+
+4. **Start development server**
+ ```bash
+ npm run dev
+ ```
+
+5. **Configure CORS in backend**
+
+ **Important**: CORS is currently not configured. You need to add CORS middleware:
+
+ ```bash
+ cd backend
+ npm install cors @types/cors
+ ```
+
+ Then update `backend/src/app.ts`:
+ ```typescript
+ import cors from 'cors';
+
+ app.use(cors({
+ origin: process.env.FRONTEND_URL || 'http://localhost:5173',
+ credentials: true
+ }));
+ ```
+
+## Project Structure
+
+```
+ctf_platform/
+├── backend/ # Backend API server (Node.js + Express + TypeScript)
+│ ├── src/
+│ │ ├── config/ # Configuration and database setup
+│ │ ├── controller/ # Business logic
+│ │ ├── middlewares/ # Express middlewares
+│ │ ├── models/ # Mongoose models
+│ │ ├── router/ # Route definitions
+│ │ ├── types/ # TypeScript types
+│ │ ├── utils/ # Utility functions
+│ │ ├── app.ts # Express app configuration
+│ │ └── server.ts # Server entry point
+│ ├── scripts/ # Migration scripts
+│ └── package.json
+├── frontend/ # Frontend application (to be implemented)
+└── docs/ # Documentation
+ ├── architecture.md # System architecture
+ ├── api.md # API documentation
+ ├── setup.md # Detailed setup guide
+ ├── sequences.md # Sequence diagrams
+ └── security.md # Security considerations
+```
+
+## Naming Convention
+
+- Constants: `UPPER_SNAKE_CASE`
+- Variables & Functions: `camelCase`
+- Classes: `UpperCamelCase`
+
+## Documentation
+
+Comprehensive documentation is available in the `docs/` directory:
+
+- **[Architecture](docs/architecture.md)**: System architecture and design decisions
+- **[API Documentation](docs/api.md)**: Complete API reference
+- **[Setup Guide](docs/setup.md)**: Detailed setup instructions
+- **[Sequence Diagrams](docs/sequences.md)**: Visual flow diagrams
+- **[Security](docs/security.md)**: Security considerations and gaps
+
+## Important Security Notes
+
+**Before Production Deployment**:
+
+1. **CORS**: Not configured - must be added for frontend communication
+2. **Rate Limiting**: Not implemented - critical for preventing abuse
+3. **Database Security**: Database is exposed until AWS migration - use strong credentials and restrict access
+
+See [Security Documentation](docs/security.md) for details.
+
+## API Endpoints
+
+### Authentication
+- `POST /api/v1/signup` - Create user account
+- `POST /api/v1/login` - Authenticate user
+- `POST /api/v1/logout` - Logout user
+
+### CTF Challenges
+- `GET /api/v1/ctfs` - List all CTF challenges
+- `POST /api/v1/ctfs/:id/instances` - Start CTF instance
+- `GET /api/v1/ctfs/instances` - Get active instance
+- `PATCH /api/v1/ctfs/instances/:id` - Stop instance
+- `PATCH /api/v1/ctfs/:id` - Submit flag
+
+See [API Documentation](docs/api.md) for complete details.
+
+## Development
+
+### Available Scripts
+
+```bash
+# Development
+npm run dev # Start dev server with hot reload
+npm run build # Build TypeScript to JavaScript
+npm start # Start production server
+
+# Code Quality
+npm run lint # Run ESLint
+npm run format # Format code with Prettier
+npm run check # Check code formatting
+
+# Utilities
+npm run migrate # Run database migrations
+npm run docker_test # Test Docker utilities
+```
+
+## Technology Stack
+
+### Backend
+- **Runtime**: Node.js 25.x
+- **Framework**: Express.js 5.2.1
+- **Language**: TypeScript 5.9.3
+- **Database**: MongoDB (Mongoose 9.0.2)
+- **Authentication**: JWT (jsonwebtoken 9.0.3)
+- **Docker**: dockerode 4.0.9
+- **Scheduling**: node-cron 4.2.1
+
+## Contributing
+
+1. Follow the naming conventions
+2. Run `npm run lint` before committing
+3. Update documentation for new features
+4. Add tests for new functionality
+
+## License
+
+See [LICENSE](LICENSE) file for details.
+
+## Acknowledgements
+
+- Backend setup inspired by Aman Mittal's Express + TypeScript guide:
+ - [Backend setup reference](https://blog.logrocket.com/express-typescript-node/) — Aman Mittal
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..072a57e
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..d1b3ca7
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,4171 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.13.2",
+ "cors": "^2.8.5",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.11.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/axios": "^0.9.36",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "gh-pages": "^6.3.0",
+ "globals": "^16.5.0",
+ "prettier": "^3.7.4",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.3.1"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
+ "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
+ "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
+ "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
+ "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
+ "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
+ "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
+ "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
+ "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
+ "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
+ "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
+ "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
+ "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
+ "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
+ "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
+ "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
+ "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
+ "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
+ "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
+ "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
+ "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
+ "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
+ "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/axios": {
+ "version": "0.9.36",
+ "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz",
+ "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
+ "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.50.0",
+ "@typescript-eslint/type-utils": "8.50.0",
+ "@typescript-eslint/utils": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.50.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
+ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.50.0",
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
+ "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.50.0",
+ "@typescript-eslint/types": "^8.50.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
+ "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
+ "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
+ "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0",
+ "@typescript-eslint/utils": "8.50.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
+ "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
+ "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.50.0",
+ "@typescript-eslint/tsconfig-utils": "8.50.0",
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/visitor-keys": "8.50.0",
+ "debug": "^4.3.4",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
+ "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.50.0",
+ "@typescript-eslint/types": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
+ "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.50.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-plugin-react-compiler": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
+ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.11",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
+ "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001761",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
+ "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/email-addresses": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
+ "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/filename-reserved-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
+ "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/filenamify": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz",
+ "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "filename-reserved-regex": "^2.0.0",
+ "strip-outer": "^1.0.1",
+ "trim-repeated": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "11.3.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
+ "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gh-pages": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz",
+ "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.4",
+ "commander": "^13.0.0",
+ "email-addresses": "^5.0.0",
+ "filenamify": "^4.3.0",
+ "find-cache-dir": "^3.3.1",
+ "fs-extra": "^11.1.1",
+ "globby": "^11.1.0"
+ },
+ "bin": {
+ "gh-pages": "bin/gh-pages.js",
+ "gh-pages-clean": "bin/gh-pages-clean.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
+ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
+ "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
+ "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.12.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
+ "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.5",
+ "@rollup/rollup-android-arm64": "4.53.5",
+ "@rollup/rollup-darwin-arm64": "4.53.5",
+ "@rollup/rollup-darwin-x64": "4.53.5",
+ "@rollup/rollup-freebsd-arm64": "4.53.5",
+ "@rollup/rollup-freebsd-x64": "4.53.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.5",
+ "@rollup/rollup-linux-arm64-musl": "4.53.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.5",
+ "@rollup/rollup-linux-x64-gnu": "4.53.5",
+ "@rollup/rollup-linux-x64-musl": "4.53.5",
+ "@rollup/rollup-openharmony-arm64": "4.53.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.5",
+ "@rollup/rollup-win32-x64-gnu": "4.53.5",
+ "@rollup/rollup-win32-x64-msvc": "4.53.5",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-outer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
+ "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-outer/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-repeated": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
+ "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/trim-repeated/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz",
+ "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.50.0",
+ "@typescript-eslint/parser": "8.50.0",
+ "@typescript-eslint/typescript-estree": "8.50.0",
+ "@typescript-eslint/utils": "8.50.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
+ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..a7e8ff6
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "vercel-build": "vite build",
+ "preview": "vite preview",
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d dist",
+ "lint": "eslint 'src/**/*.ts'",
+ "format": "prettier --write \"src/**/*.{js,ts,json,tsx}\"",
+ "check": "prettier --check \"src/**/*.{js,ts,json,tsx}\""
+ },
+ "dependencies": {
+ "axios": "^1.13.2",
+ "cors": "^2.8.5",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.11.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/axios": "^0.9.36",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "prettier": "^3.7.4",
+ "gh-pages": "^6.3.0",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..0a534ea
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,45 @@
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ z-index: 1;
+}
+
+.app::after {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image:
+ linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
+ background-size: 50px 50px;
+ pointer-events: none;
+ z-index: 0;
+ opacity: 0.5;
+}
+
+.main-content {
+ flex: 1;
+ padding: 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+ width: 100%;
+ position: relative;
+ z-index: 2;
+}
+
+@media (max-width: 1024px) {
+ .main-content {
+ padding: 1.5rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .main-content {
+ padding: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..4f1b894
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import {
+ BrowserRouter as Router,
+ Routes,
+ Route,
+ Navigate,
+} from 'react-router-dom';
+import { AuthProvider, useAuth } from './context/AuthContext';
+import { ToastProvider } from './components/common/ToastContainer';
+import Navbar from './components/layout/Navbar';
+import Footer from './components/layout/Footer';
+import Home from './pages/Home';
+import Login from './components/auth/Login';
+import Register from './components/auth/Register';
+import Challenges from './pages/Challenges';
+import ChallengeDetail from './components/challenges/ChallengeDetail';
+import './App.css';
+
+// ✅ Fixed: Inside AuthProvider
+const AppContent: React.FC = () => {
+ const { isAuthenticated, loading } = useAuth();
+
+ // ✅ Loading screen
+ if (loading) {
+ return (
+
+ );
+ }
+
+ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
+ children,
+ }) => {
+ return isAuthenticated ? <>{children}> : ;
+ };
+
+ return (
+
+
+
+
+ } />
+ } />
+ } />
+
+ {/* ✅ PROTECTED CTF */}
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+ } />
+
+
+
+
+ );
+};
+
+const App: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/ActiveInstanceBanner.css b/frontend/src/components/ActiveInstanceBanner.css
new file mode 100644
index 0000000..9dc77ea
--- /dev/null
+++ b/frontend/src/components/ActiveInstanceBanner.css
@@ -0,0 +1,62 @@
+.active-instance-banner {
+ background: rgba(0, 102, 255, 0.1);
+ border: 2px solid #0066ff;
+ border-radius: 12px;
+ padding: 1.25rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+
+.info h3 {
+ color: #0066ff;
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 0.25rem;
+}
+
+.info p {
+ color: var(--text-primary);
+ margin-bottom: 0.25rem;
+ font-size: 0.95rem;
+}
+
+.pulse {
+ animation: pulse 1.5s infinite;
+}
+
+.timer {
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+.active-instance-banner button {
+ padding: 0.5rem 1.25rem;
+ background: #0066ff;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.active-instance-banner button:hover {
+ background: var(--primary-dark);
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+@media (max-width: 768px) {
+ .active-instance-banner {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+}
diff --git a/frontend/src/components/ActiveInstanceBanner.tsx b/frontend/src/components/ActiveInstanceBanner.tsx
new file mode 100644
index 0000000..478b8b7
--- /dev/null
+++ b/frontend/src/components/ActiveInstanceBanner.tsx
@@ -0,0 +1,53 @@
+import React, { useState, useEffect } from 'react';
+import { type CtfInstance } from '../types/ctf';
+import './ActiveInstanceBanner.css';
+
+interface Props {
+ instance: CtfInstance;
+ onStop: () => void;
+}
+
+export const ActiveInstanceBanner: React.FC = ({ instance, onStop }) => {
+ const [timeLeft, setTimeLeft] = useState('');
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ const now = new Date().getTime();
+ const expiry = new Date(instance.expiresAt).getTime();
+ const diff = expiry - now;
+
+ if (diff <= 0) {
+ setTimeLeft('EXPIRED');
+ clearInterval(timer);
+ } else {
+ const minutes = Math.floor(diff / 60000);
+ const seconds = Math.floor((diff % 60000) / 1000);
+ setTimeLeft(`${minutes}m ${seconds}s`);
+ }
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [instance.expiresAt]);
+
+ return (
+
+
+
Active Challenge Instance
+
+ {instance.status === 'PENDING' ? (
+ ⏳ Starting instance...
+ ) : (
+ <>
+ 🔗{' '}
+
+ Open Challenge
+
+ >
+ )}
+
+
⏱️ Expires in: {timeLeft}
+
+
+
+ );
+};
diff --git a/frontend/src/components/FlagSubmitModal.tsx b/frontend/src/components/FlagSubmitModal.tsx
new file mode 100644
index 0000000..87c3875
--- /dev/null
+++ b/frontend/src/components/FlagSubmitModal.tsx
@@ -0,0 +1,60 @@
+import React, { useState } from 'react';
+
+interface Props {
+ ctfName: string;
+ onSubmit: (flag: string) => void;
+ onClose: () => void;
+}
+
+export const FlagSubmitModal: React.FC = ({
+ ctfName,
+ onSubmit,
+ onClose,
+}) => {
+ const [flag, setFlag] = useState('');
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (flag.trim()) {
+ onSubmit(flag);
+ }
+ };
+
+ return (
+
+
+
Submit Flag
+
+ Challenge: {ctfName}
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/auth/Login.css b/frontend/src/components/auth/Login.css
new file mode 100644
index 0000000..e186168
--- /dev/null
+++ b/frontend/src/components/auth/Login.css
@@ -0,0 +1,104 @@
+.auth-container {
+ min-height: 80vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ background: var(--bg-primary);
+}
+
+.auth-card {
+ width: 100%;
+ max-width: 450px;
+ background: white;
+ border: 2px solid #e5e7eb;
+ border-radius: 16px;
+ padding: 3rem;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
+ animation: slideUp 0.5s ease-out;
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.auth-card h1 {
+ color: #111827;
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+ text-align: center;
+ font-weight: 700;
+}
+
+.auth-subtitle {
+ color: #6b7280;
+ text-align: center;
+ margin-bottom: 2rem;
+ font-size: 0.95rem;
+}
+
+.auth-form {
+ margin-top: 2rem;
+}
+
+.btn-full {
+ width: 100%;
+ margin-top: 0.5rem;
+}
+
+.auth-footer {
+ text-align: center;
+ margin-top: 2rem;
+ color: #6b7280;
+ font-size: 0.95rem;
+}
+
+.auth-footer a {
+ color: #6b73ff;
+ text-decoration: none;
+ font-weight: 600;
+ transition: all 0.2s ease;
+}
+
+.auth-footer a:hover {
+ color: #5860e6;
+ text-decoration: underline;
+}
+
+/* Alert styles for inline errors (if needed) */
+.alert {
+ padding: 1rem;
+ border-radius: 8px;
+ margin-bottom: 1.5rem;
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.alert-error {
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ color: #991b1b;
+}
+
+.alert-success {
+ background: #f0fdf4;
+ border: 1px solid #bbf7d0;
+ color: #166534;
+}
+
+@media (max-width: 768px) {
+ .auth-card {
+ padding: 2rem;
+ }
+
+ .auth-card h1 {
+ font-size: 1.75rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/auth/Login.tsx b/frontend/src/components/auth/Login.tsx
new file mode 100644
index 0000000..ed64886
--- /dev/null
+++ b/frontend/src/components/auth/Login.tsx
@@ -0,0 +1,77 @@
+// src/components/auth/Login.tsx
+import React, { useState } from 'react';
+import { useAuth } from '../../hooks/useAuth';
+import { useNavigate, Link } from 'react-router-dom';
+import { useToast } from '../common/ToastContainer';
+import { getErrorMessage } from '../../utils/errorHandler';
+import Input from '../common/Input';
+import Button from '../common/Button';
+import './Login.css';
+
+const Login: React.FC = () => {
+ const { login, isAuthenticated } = useAuth();
+ const { showToast } = useToast();
+ const navigate = useNavigate();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ if (isAuthenticated) navigate('/challenges');
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ try {
+ const credentials = { email, password };
+ await login(credentials);
+ showToast('Login successful! Welcome back.', 'success');
+ navigate('/challenges');
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ const errorMessage = getErrorMessage(err);
+ showToast(errorMessage, 'error');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
🚩 Login
+
Welcome back to CTF Platform
+
+
+
+
+ Don't have an account? Register here
+
+
+
+ );
+};
+
+export default Login;
diff --git a/frontend/src/components/auth/Register.css b/frontend/src/components/auth/Register.css
new file mode 100644
index 0000000..204dc38
--- /dev/null
+++ b/frontend/src/components/auth/Register.css
@@ -0,0 +1 @@
+@import './Login.css';
\ No newline at end of file
diff --git a/frontend/src/components/auth/Register.tsx b/frontend/src/components/auth/Register.tsx
new file mode 100644
index 0000000..6a72279
--- /dev/null
+++ b/frontend/src/components/auth/Register.tsx
@@ -0,0 +1,113 @@
+// Register.tsx - FULLY FIXED
+import React, { useEffect, useState } from 'react';
+import { useNavigate, Link } from 'react-router-dom';
+import { authService } from '../../services/authService';
+import { useToast } from '../common/ToastContainer';
+import { getErrorMessage } from '../../utils/errorHandler';
+import Input from '../common/Input';
+import Button from '../common/Button';
+import './Register.css';
+import { useAuth } from '../../context/AuthContext';
+
+const Register: React.FC = () => {
+ const { isAuthenticated } = useAuth();
+ const { showToast } = useToast();
+
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ confirmPassword: '',
+ });
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ // If user is already logged in, don't let them stay here
+ if (isAuthenticated) {
+ navigate('/challenges');
+ }
+ }, [isAuthenticated, navigate]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData({ ...formData, [e.target.name]: e.target.value });
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (formData.password !== formData.confirmPassword) {
+ showToast('Passwords do not match', 'error');
+ return;
+ }
+
+ if (formData.password.length < 6) {
+ showToast('Password must be at least 6 characters', 'error');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ await authService.register({
+ email: formData.email,
+ password: formData.password,
+ confirmPassword: formData.confirmPassword,
+ });
+ showToast('Registration successful! Please login.', 'success');
+ navigate('/login');
+ } catch (err: any) {
+ const errorMessage = getErrorMessage(err);
+ showToast(errorMessage, 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
🚩 Join CTF Platform
+
Create your account and start hacking
+
+
+
+
+ Already have an account? Login here
+
+
+
+ );
+};
+
+export default Register;
diff --git a/frontend/src/components/challenges/ChallengeCard.css b/frontend/src/components/challenges/ChallengeCard.css
new file mode 100644
index 0000000..b67ca29
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeCard.css
@@ -0,0 +1,105 @@
+/* ===== Challenge Card Styling ===== */
+.challenge-card {
+ background: #ffffff;
+ border: 2px solid #e5e7eb;
+ border-radius: 12px;
+ padding: 1.5rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.challenge-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 24px rgba(107, 115, 255, 0.15);
+ border-color: #6b73ff;
+}
+
+.challenge-card-solved {
+ border-color: #10b981;
+ background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
+}
+
+.challenge-card-solved:hover {
+ border-color: #10b981;
+ box-shadow: 0 8px 24px rgba(16, 185, 129, 0.2);
+}
+
+.challenge-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+}
+
+.challenge-header h3 {
+ font-size: 1.25rem;
+ color: #111827;
+ font-weight: 600;
+ flex: 1;
+}
+
+.challenge-solved-badge {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ font-size: 0.8rem;
+ font-weight: 700;
+ white-space: nowrap;
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+ animation: solvedPulse 2s ease-in-out infinite;
+}
+
+@keyframes solvedPulse {
+ 0%, 100% {
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+ }
+ 50% {
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.5);
+ }
+}
+
+.challenge-description {
+ color: #6b7280;
+ margin-bottom: 1rem;
+ min-height: 50px;
+ line-height: 1.5;
+}
+
+.challenge-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 12px;
+}
+
+.challenge-category {
+ background: #6b73ff;
+ color: white;
+ padding: 0.375rem 0.875rem;
+ border-radius: 12px;
+ font-size: 0.875rem;
+ font-weight: 600;
+}
+
+.challenge-difficulty {
+ padding: 0.375rem 0.875rem;
+ border-radius: 12px;
+ font-weight: 600;
+ font-size: 0.875rem;
+ color: white;
+}
+
+.challenge-difficulty.green {
+ background-color: #10b981;
+}
+
+.challenge-difficulty.orange {
+ background-color: #f97316;
+}
+
+.challenge-difficulty.red {
+ background-color: #ef4444;
+}
diff --git a/frontend/src/components/challenges/ChallengeCard.tsx b/frontend/src/components/challenges/ChallengeCard.tsx
new file mode 100644
index 0000000..149808d
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeCard.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import type { Ctf } from '../../types/ctf';
+import './ChallengeCard.css';
+
+interface ChallengeCardProps {
+ challenge: Ctf;
+ onClick: () => void;
+ isSolved?: boolean;
+}
+
+const ChallengeCard: React.FC = ({
+ challenge,
+ onClick,
+ isSolved = false,
+}) => {
+ const getDifficultyColor = (difficulty: string) => {
+ switch (difficulty.toLowerCase()) {
+ case 'easy':
+ return 'green';
+ case 'mid':
+ return 'orange';
+ case 'hard':
+ return 'red';
+ default:
+ return 'gray';
+ }
+ };
+
+ return (
+
+
+
{challenge.name}
+ {isSolved && ✓ Solved}
+
+
+
+ {challenge.description || 'No description available'}
+
+
+
+ {challenge.type}
+
+ {challenge.difficulty}
+
+
+
+ );
+};
+
+export default ChallengeCard;
diff --git a/frontend/src/components/challenges/ChallengeDetail.css b/frontend/src/components/challenges/ChallengeDetail.css
new file mode 100644
index 0000000..f6c2256
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeDetail.css
@@ -0,0 +1,173 @@
+.challenge-detail {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.challenge-detail-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+}
+
+.challenge-detail-header h1 {
+ color: var(--primary);
+ font-size: 2.5rem;
+ flex: 1;
+ min-width: 300px;
+}
+
+.badge-solved-large {
+ background: linear-gradient(135deg, var(--success), #04d98b);
+ color: var(--bg-primary);
+ padding: 0.75rem 1.5rem;
+ border-radius: 30px;
+ font-size: 1rem;
+ font-weight: 700;
+ box-shadow: 0 0 30px rgba(6, 255, 165, 0.5);
+ animation: pulse 2s infinite;
+}
+
+.challenge-detail-info {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ padding: 1rem;
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 12px;
+}
+
+.info-item {
+ padding: 0.5rem 1rem;
+ background: rgba(0, 240, 255, 0.1);
+ border: 1px solid var(--primary);
+ border-radius: 20px;
+ color: var(--primary);
+ font-weight: 600;
+}
+
+.challenge-detail-content {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 2rem;
+ margin-bottom: 2rem;
+}
+
+.challenge-detail-content h2,
+.challenge-detail-content h3 {
+ color: var(--primary);
+ margin-bottom: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.challenge-detail-content p {
+ color: var(--text-secondary);
+ line-height: 1.8;
+ font-size: 1.05rem;
+}
+
+.challenge-hints,
+.challenge-files {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: rgba(0, 240, 255, 0.05);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+}
+
+.challenge-hints ul {
+ list-style: none;
+ margin-top: 1rem;
+}
+
+.challenge-hints li {
+ padding: 0.75rem;
+ margin-bottom: 0.5rem;
+ background: var(--bg-tertiary);
+ border-left: 4px solid var(--accent);
+ border-radius: 4px;
+ color: var(--text-secondary);
+}
+
+.file-link {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, var(--accent), var(--secondary));
+ color: white;
+ text-decoration: none;
+ border-radius: 8px;
+ margin-top: 1rem;
+ margin-right: 1rem;
+ font-weight: 600;
+ transition: var(--transition);
+ box-shadow: 0 0 20px rgba(131, 56, 236, 0.3);
+}
+
+.file-link:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0 30px rgba(131, 56, 236, 0.5);
+}
+
+.challenge-submit {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 2rem;
+}
+
+.challenge-submit h2 {
+ color: var(--primary);
+ margin-bottom: 1.5rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.challenge-submit form {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.challenge-submit form .input {
+ flex: 1;
+ min-width: 250px;
+}
+
+.alert {
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ margin-bottom: 1rem;
+ font-weight: 500;
+}
+
+.alert-success {
+ background: rgba(16, 185, 129, 0.1);
+ border: 1px solid var(--success);
+ color: var(--success);
+}
+
+.alert-error {
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid var(--error);
+ color: var(--error);
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.7; }
+}
+
+@media (max-width: 768px) {
+ .challenge-detail-header h1 {
+ font-size: 1.8rem;
+ }
+
+ .challenge-submit form {
+ flex-direction: column;
+ }
+}
diff --git a/frontend/src/components/challenges/ChallengeDetail.tsx b/frontend/src/components/challenges/ChallengeDetail.tsx
new file mode 100644
index 0000000..a746c10
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeDetail.tsx
@@ -0,0 +1,166 @@
+// src/components/challenges/ChallengeDetail.tsx
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { challengeService } from '../../services/challengeService';
+import type { Ctf, CtfDifficulty, CtfType } from '../../types/ctf';
+import { useAuth } from '../../hooks/useAuth';
+import Button from '../common/Button';
+import Input from '../common/Input';
+import './ChallengeDetail.css';
+
+const ChallengeDetail: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const { refreshUser } = useAuth();
+
+ const [challenge, setChallenge] = useState(null);
+ const [flag, setFlag] = useState('');
+ const [message, setMessage] = useState<{
+ type: 'success' | 'error';
+ text: string;
+ } | null>(null);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ const fetchChallenge = async () => {
+ if (!id) return;
+
+ try {
+ // Fetch from API
+ const dataFromApi: any = await challengeService.getChallengeById(id);
+
+ // Map API data to Ctf type
+ const ctfChallenge: Ctf = {
+ id: dataFromApi.id || id,
+ name: dataFromApi.name || dataFromApi.title || `Challenge ${id}`,
+ type: (dataFromApi.type as CtfType) || 'OTHER',
+ description: dataFromApi.description || '',
+ difficulty: (dataFromApi.difficulty as CtfDifficulty) || 'MID',
+ hints: dataFromApi.hints || [],
+ resources: dataFromApi.resources || [],
+ withSite: dataFromApi.withSite ?? false,
+ };
+
+ setChallenge(ctfChallenge);
+ } catch (error) {
+ console.error('Failed to fetch challenge:', error);
+ navigate('/challenges');
+ }
+ };
+
+ fetchChallenge();
+ }, [id, navigate]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!id) return;
+
+ setLoading(true);
+ setMessage(null);
+
+ try {
+ const result = await challengeService.submitFlag(id, flag);
+
+ if (result.correct) {
+ setMessage({ type: 'success', text: '🎉 Correct! Flag accepted!' });
+ setFlag('');
+ await refreshUser();
+
+ // Refresh challenge after submission
+ const updatedDataFromApi: any =
+ await challengeService.getChallengeById(id);
+ const updatedChallenge: Ctf = {
+ id: updatedDataFromApi.id || id,
+ name:
+ updatedDataFromApi.name ||
+ updatedDataFromApi.title ||
+ `Challenge ${id}`,
+ type: (updatedDataFromApi.type as CtfType) || 'OTHER',
+ description: updatedDataFromApi.description || '',
+ difficulty: (updatedDataFromApi.difficulty as CtfDifficulty) || 'MID',
+ hints: updatedDataFromApi.hints || [],
+ resources: updatedDataFromApi.resources || [],
+ withSite: updatedDataFromApi.withSite ?? false,
+ };
+ setChallenge(updatedChallenge);
+ } else {
+ setMessage({ type: 'error', text: 'Incorrect flag. Try again!' });
+ }
+ } catch (error: any) {
+ setMessage({
+ type: 'error',
+ text: error.response?.data?.error || 'Submission failed',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (!challenge) return Loading...
;
+
+ return (
+
+
+
{challenge.name}
+
+
+
+
+ {challenge.type.replace('_', ' ')}
+
+ {challenge.difficulty}
+
+ With Site: {challenge.withSite ? 'Yes' : 'No'}
+
+
+
+
+
Description
+
{challenge.description || 'No description provided.'}
+
+ {challenge.hints.length > 0 && (
+
+
💡 Hints
+
+ {challenge.hints.map((hint, index) => (
+ - {hint}
+ ))}
+
+
+ )}
+
+ {challenge.resources.length > 0 && (
+
+
📚 Resources
+
+ {challenge.resources.map((res, index) => (
+ - {res}
+ ))}
+
+
+ )}
+
+
+
+
Submit Flag
+ {message && (
+
{message.text}
+ )}
+
+
+
+
+ );
+};
+
+export default ChallengeDetail;
diff --git a/frontend/src/components/challenges/ChallengeList.css b/frontend/src/components/challenges/ChallengeList.css
new file mode 100644
index 0000000..fa80714
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeList.css
@@ -0,0 +1,48 @@
+.challenge-list {
+ width: 100%;
+ background: var(--bg-primary); /* page background */
+ padding: 2rem 0;
+ min-height: 100vh;
+}
+
+.challenge-filters {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+}
+
+.challenge-filters select {
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ font-weight: 600;
+ cursor: pointer;
+ transition: var(--transition);
+ min-width: 200px;
+}
+
+.challenge-filters select:hover {
+ border-color: var(--primary);
+}
+
+.challenge-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.challenge-grid .challenge-card:hover {
+ transform: translateY(-3px);
+ box-shadow: var(--shadow-md);
+}
+
+.no-challenges {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: var(--text-secondary);
+ font-size: 1.2rem;
+ grid-column: 1 / -1;
+}
diff --git a/frontend/src/components/challenges/ChallengeList.tsx b/frontend/src/components/challenges/ChallengeList.tsx
new file mode 100644
index 0000000..03a46a8
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeList.tsx
@@ -0,0 +1,109 @@
+import React, { useState, useEffect } from 'react';
+import { challengeService } from '../../services/challengeService';
+import type { Ctf, CtfDifficulty, CtfType } from '../../types/ctf';
+import ChallengeCard from './ChallengeCard';
+import ChallengeModal from './ChallengeModal';
+import './ChallengeList.css';
+
+const ChallengeList: React.FC = () => {
+ const [challenges, setChallenges] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [category, setCategory] = useState('');
+ const [difficulty, setDifficulty] = useState('');
+ const [selectedChallenge, setSelectedChallenge] = useState(null);
+
+ useEffect(() => {
+ const fetchChallenges = async () => {
+ try {
+ setLoading(true);
+ const data = await challengeService.getAllChallenges(
+ category,
+ difficulty,
+ );
+
+ // Map API data to Ctf type
+ const mappedData: Ctf[] = data.map((challenge: any, index: number) => ({
+ id: challenge.id || `challenge-${index}`,
+ name: challenge.name || challenge.title || `Challenge ${index}`,
+ type: (challenge.type as CtfType) || 'OTHER',
+ description: challenge.description || '',
+ difficulty: (challenge.difficulty as CtfDifficulty) || 'MID',
+ hints: challenge.hints || [],
+ resources: challenge.resources || [],
+ withSite: challenge.withSite ?? false,
+ solved: challenge.solved ?? false,
+ }));
+
+ setChallenges(mappedData);
+ } catch (error) {
+ console.error('Failed to fetch challenges:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchChallenges();
+ }, [category, difficulty]);
+
+ const handleChallengeClick = (challenge: Ctf) => {
+ setSelectedChallenge(challenge);
+ };
+
+ const handleCloseModal = () => {
+ setSelectedChallenge(null);
+ };
+
+ if (loading) {
+ return Loading challenges...
;
+ }
+
+ return (
+
+ {/* Filters */}
+
+
+
+
+
+
+ {/* Challenges Grid */}
+
+ {challenges.length === 0 ? (
+
No challenges found
+ ) : (
+ challenges.map((challenge) => (
+
handleChallengeClick(challenge)}
+ isSolved={challenge.solved}
+ />
+ ))
+ )}
+
+
+ {/* Challenge Modal */}
+ {selectedChallenge && (
+
+ )}
+
+ );
+};
+
+export default ChallengeList;
diff --git a/frontend/src/components/challenges/ChallengeModal.css b/frontend/src/components/challenges/ChallengeModal.css
new file mode 100644
index 0000000..3d3fca3
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeModal.css
@@ -0,0 +1,458 @@
+.challenge-modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.75);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2;
+ padding: 1rem;
+ animation: fadeIn 0.2s ease;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+.challenge-modal {
+ background: #ffffff;
+ border-radius: 12px;
+ max-width: 700px;
+ width: 100%;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ animation: slideUp 0.3s ease;
+ z-index: 9999;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+.challenge-modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 1.5rem 2rem;
+ border-bottom: 2px solid #e5e7eb;
+ background: #f9fafb;
+}
+
+.challenge-modal-title-section {
+ flex: 1;
+}
+
+.challenge-modal-header h2 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: #111827;
+ margin-bottom: 0.75rem;
+}
+
+.challenge-modal-badges {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.challenge-modal-category {
+ background: #6b73ff;
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ font-size: 0.875rem;
+ font-weight: 600;
+}
+
+.challenge-modal-difficulty {
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ font-size: 0.875rem;
+ font-weight: 600;
+}
+
+.challenge-modal-solved-badge {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ font-size: 0.875rem;
+ font-weight: 700;
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+}
+
+.challenge-modal-close {
+ background: transparent;
+ border: none;
+ font-size: 1.5rem;
+ color: #6b7280;
+ cursor: pointer;
+ padding: 0.25rem 0.5rem;
+ line-height: 1;
+ transition: color 0.2s ease;
+ margin-left: 1rem;
+}
+
+.challenge-modal-close:hover {
+ color: #111827;
+}
+
+.challenge-modal-body {
+ padding: 2rem;
+}
+
+.challenge-modal-section {
+ margin-bottom: 2rem;
+}
+
+.challenge-modal-section:last-child {
+ margin-bottom: 0;
+}
+
+.challenge-modal-section h3 {
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: #111827;
+ margin-bottom: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.challenge-modal-description {
+ color: #374151;
+ line-height: 1.6;
+ font-size: 0.95rem;
+}
+
+.challenge-modal-hints-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.75rem;
+}
+
+.challenge-modal-hints-toggle {
+ background: #6b73ff;
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.challenge-modal-hints-toggle:hover {
+ background: #5860e6;
+}
+
+.challenge-modal-hints-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.challenge-modal-hints-list li {
+ background: #f9fafb;
+ border-left: 3px solid #6b73ff;
+ padding: 0.75rem 1rem;
+ margin-bottom: 0.5rem;
+ border-radius: 4px;
+ color: #374151;
+ font-size: 0.95rem;
+}
+
+.challenge-modal-resources-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.challenge-modal-resources-list li {
+ background: #f9fafb;
+ border-left: 3px solid #10b981;
+ padding: 0.75rem 1rem;
+ margin-bottom: 0.5rem;
+ border-radius: 4px;
+}
+
+.challenge-modal-resource-link {
+ color: #10b981;
+ text-decoration: none;
+ font-size: 0.95rem;
+ font-weight: 500;
+ word-break: break-all;
+ transition: color 0.2s ease;
+}
+
+.challenge-modal-resource-link:hover {
+ color: #059669;
+ text-decoration: underline;
+}
+
+.challenge-modal-launch-btn {
+ background: #6b73ff;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ width: 100%;
+}
+
+.challenge-modal-launch-btn:hover:not(:disabled) {
+ background: #5860e6;
+}
+
+.challenge-modal-launch-btn:disabled {
+ background: #d1d5db;
+ color: #6b7280;
+ cursor: not-allowed;
+}
+
+.challenge-modal-instance-active {
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ border: 2px solid #3b82f6;
+ border-radius: 12px;
+ padding: 1.5rem;
+}
+
+.instance-info {
+ margin-bottom: 1rem;
+}
+
+.instance-status-pending {
+ color: #f59e0b;
+ font-weight: 600;
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.pulse {
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.instance-link {
+ margin-bottom: 0.75rem;
+ font-size: 1rem;
+}
+
+.instance-link a {
+ color: #3b82f6;
+ text-decoration: none;
+ font-weight: 600;
+ transition: color 0.2s ease;
+}
+
+.instance-link a:hover {
+ color: #2563eb;
+ text-decoration: underline;
+}
+
+.instance-timer {
+ color: #6b7280;
+ font-size: 0.95rem;
+}
+
+.instance-timer strong {
+ color: #ef4444;
+ font-weight: 700;
+}
+
+.challenge-modal-stop-btn {
+ width: 100%;
+ background: #ef4444;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.challenge-modal-stop-btn:hover:not(:disabled) {
+ background: #dc2626;
+}
+
+.challenge-modal-stop-btn:disabled {
+ background: #d1d5db;
+ color: #6b7280;
+ cursor: not-allowed;
+}
+
+.challenge-modal-warning {
+ margin-top: 0.75rem;
+ padding: 0.75rem;
+ background: #fef3c7;
+ border-left: 3px solid #f59e0b;
+ border-radius: 4px;
+ color: #92400e;
+ font-size: 0.875rem;
+}
+
+.challenge-modal-flag-form {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.challenge-modal-flag-input {
+ flex: 1;
+ padding: 0.75rem 1rem;
+ border: 2px solid #e5e7eb;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-family: 'Courier New', monospace;
+ transition: border-color 0.2s ease;
+}
+
+.challenge-modal-flag-input:focus {
+ outline: none;
+ border-color: #6b73ff;
+ box-shadow: 0 0 0 3px rgba(107, 115, 255, 0.1);
+}
+
+.challenge-modal-flag-input:disabled {
+ background: #f3f4f6;
+ cursor: not-allowed;
+}
+
+.challenge-modal-submit-btn {
+ background: #10b981;
+ color: white;
+ border: none;
+ padding: 0.75rem 2rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ white-space: nowrap;
+}
+
+.challenge-modal-submit-btn:hover:not(:disabled) {
+ background: #059669;
+}
+
+.challenge-modal-submit-btn:disabled {
+ background: #d1d5db;
+ color: #6b7280;
+ cursor: not-allowed;
+}
+
+.challenge-modal-solved-message {
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+ border: 2px solid #10b981;
+ border-radius: 12px;
+ padding: 2rem;
+ text-align: center;
+}
+
+.challenge-modal-solved-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ animation: bounce 1s ease-in-out infinite;
+}
+
+@keyframes bounce {
+
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+.challenge-modal-solved-message p {
+ color: #166534;
+ font-size: 1.05rem;
+ font-weight: 600;
+ margin: 0;
+}
+
+/* Scrollbar styling */
+.challenge-modal::-webkit-scrollbar {
+ width: 8px;
+}
+
+.challenge-modal::-webkit-scrollbar-track {
+ background: #f3f4f6;
+}
+
+.challenge-modal::-webkit-scrollbar-thumb {
+ background: #d1d5db;
+ border-radius: 4px;
+}
+
+.challenge-modal::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .challenge-modal {
+ max-width: 100%;
+ margin: 0;
+ border-radius: 0;
+ max-height: 100vh;
+ }
+
+ .challenge-modal-header {
+ padding: 1rem 1.5rem;
+ }
+
+ .challenge-modal-header h2 {
+ font-size: 1.5rem;
+ }
+
+ .challenge-modal-body {
+ padding: 1.5rem;
+ }
+
+ .challenge-modal-flag-form {
+ flex-direction: column;
+ }
+
+ .challenge-modal-submit-btn {
+ width: 100%;
+ }
+}
diff --git a/frontend/src/components/challenges/ChallengeModal.tsx b/frontend/src/components/challenges/ChallengeModal.tsx
new file mode 100644
index 0000000..d5480ef
--- /dev/null
+++ b/frontend/src/components/challenges/ChallengeModal.tsx
@@ -0,0 +1,247 @@
+import React, { useState, useEffect } from 'react';
+import type { Ctf, CtfInstance } from '../../types/ctf';
+import './ChallengeModal.css';
+
+interface ChallengeModalProps {
+ challenge: Ctf;
+ isOpen: boolean;
+ onClose: () => void;
+ onLaunchInstance: (ctfId: string, ctfName: string) => void;
+ onSubmitFlag: (flag: string) => void;
+ activeInstance?: CtfInstance | null;
+ onStopInstance: () => void;
+ isLoading?: boolean;
+ isSolved?: boolean;
+}
+
+export const ChallengeModal: React.FC = ({
+ challenge,
+ isOpen,
+ onClose,
+ onLaunchInstance,
+ onSubmitFlag,
+ activeInstance = null,
+ onStopInstance,
+ isLoading = false,
+ isSolved = false,
+}) => {
+ const [flag, setFlag] = useState('');
+ const [showHints, setShowHints] = useState(false);
+ const [timeLeft, setTimeLeft] = useState('');
+
+ // Timer for active instance
+ useEffect(() => {
+ if (!activeInstance) {
+ setTimeLeft('');
+ return;
+ }
+
+ const timer = setInterval(() => {
+ const now = new Date().getTime();
+ const expiry = new Date(activeInstance.expiresAt).getTime();
+ const diff = expiry - now;
+
+ if (diff <= 0) {
+ setTimeLeft('EXPIRED');
+ clearInterval(timer);
+ } else {
+ const minutes = Math.floor(diff / 60000);
+ const seconds = Math.floor((diff % 60000) / 1000);
+ setTimeLeft(`${minutes}m ${seconds}s`);
+ }
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [activeInstance]);
+
+ if (!isOpen) return null;
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (flag.trim()) {
+ onSubmitFlag(flag);
+ setFlag('');
+ }
+ };
+
+ const handleBackdropClick = (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) {
+ onClose();
+ }
+ };
+
+ const getDifficultyColor = (difficulty: string) => {
+ switch (difficulty.toLowerCase()) {
+ case 'easy':
+ return '#10b981';
+ case 'mid':
+ return '#f97316';
+ case 'hard':
+ return '#ef4444';
+ default:
+ return '#6b7280';
+ }
+ };
+
+ return (
+
+
+
+
+
{challenge.name}
+
+ {challenge.type}
+
+ {challenge.difficulty}
+
+ {isSolved && (
+ ✓ Solved
+ )}
+
+
+
+
+
+
+
+
Description
+
+ {challenge.description || 'No description available'}
+
+
+
+ {challenge.hints && challenge.hints.length > 0 && (
+
+
+
Hints
+
+
+ {showHints && (
+
+ {challenge.hints.map((hint, index) => (
+ - {hint}
+ ))}
+
+ )}
+
+ )}
+
+ {challenge.withSite && (
+
+
Instance
+ {activeInstance ? (
+
+
+ {activeInstance.status === 'PENDING' ? (
+
+ ⏳ Starting instance...
+
+ ) : (
+ <>
+
+ 🔗{' '}
+
+ Open Challenge Instance
+
+
+
+ ⏱️ Expires in: {timeLeft}
+
+ >
+ )}
+
+
+
+ ) : (
+ <>
+
+ >
+ )}
+
+ )}
+
+ {challenge.resources && challenge.resources.length > 0 && (
+
+
Resources
+
+ {challenge.resources.map((resource, index) => (
+ -
+
+ {resource}
+
+
+ ))}
+
+
+ )}
+
+
+
Submit Flag
+ {isSolved ? (
+
+
🎉
+
Congratulations! You have already solved this challenge.
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/common/Button.css b/frontend/src/components/common/Button.css
new file mode 100644
index 0000000..3e4bd5f
--- /dev/null
+++ b/frontend/src/components/common/Button.css
@@ -0,0 +1,89 @@
+/* Button Base Styles */
+.btn {
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ border: none;
+ font-size: 0.95rem;
+ position: relative;
+ transition: all 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+}
+
+.btn:hover:not(:disabled) {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.btn:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Button Variants */
+.btn-primary {
+ background: #6b73ff;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: #5860e6;
+}
+
+.btn-secondary {
+ background: #f3f4f6;
+ color: #374151;
+ border: 2px solid #e5e7eb;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: #e5e7eb;
+ border-color: #d1d5db;
+}
+
+.btn-danger {
+ background: #ef4444;
+ color: white;
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: #dc2626;
+}
+
+/* Button Sizes */
+.btn-small {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+}
+
+.btn-medium {
+ padding: 0.75rem 1.5rem;
+ font-size: 0.95rem;
+}
+
+.btn-large {
+ padding: 1rem 2rem;
+ font-size: 1.05rem;
+}
+
+/* Button Loading Spinner */
+.spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
diff --git a/frontend/src/components/common/Button.tsx b/frontend/src/components/common/Button.tsx
new file mode 100644
index 0000000..ba8e494
--- /dev/null
+++ b/frontend/src/components/common/Button.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import './Button.css';
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'danger';
+ size?: 'small' | 'medium' | 'large';
+ loading?: boolean;
+}
+
+const Button: React.FC = ({
+ children,
+ variant = 'primary',
+ size = 'medium',
+ loading = false,
+ disabled,
+ className = '',
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export default Button;
diff --git a/frontend/src/components/common/Input.tsx b/frontend/src/components/common/Input.tsx
new file mode 100644
index 0000000..2fb5023
--- /dev/null
+++ b/frontend/src/components/common/Input.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import './input.css';
+
+interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+ error?: string;
+}
+
+const Input: React.FC = ({
+ label,
+ error,
+ className = '',
+ ...props
+}) => {
+ return (
+
+ {label && }
+
+ {error && {error}}
+
+ );
+};
+
+export default Input;
diff --git a/frontend/src/components/common/Modal.css b/frontend/src/components/common/Modal.css
new file mode 100644
index 0000000..39c4bcc
--- /dev/null
+++ b/frontend/src/components/common/Modal.css
@@ -0,0 +1,137 @@
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(10,14,39,0.95);
+ backdrop-filter: blur(12px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+ animation: fadeIn 0.3s ease-out;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.modal-content {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ max-width: 600px;
+ width: 90%;
+ max-height: 90vh;
+ overflow-y: auto;
+ backdrop-filter: blur(20px);
+ box-shadow: 0 20px 60px rgba(0,0,0,0.6);
+ animation: slideUpModal 0.4s ease-out;
+}
+
+@keyframes slideUpModal {
+ from { opacity: 0; transform: translateY(30px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem;
+ border-bottom: 2px solid var(--border);
+}
+
+.modal-header h2 {
+ color: var(--primary);
+ margin: 0;
+ text-shadow: 0 0 15px var(--primary-glow);
+}
+
+.modal-close {
+ background: transparent;
+ border: 2px solid var(--danger);
+ color: var(--danger);
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ font-size: 1.5rem;
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ line-height: 1;
+}
+
+.modal-close:hover {
+ background: var(--danger);
+ color: white;
+ box-shadow: 0 0 20px var(--secondary-glow);
+ transform: rotate(90deg);
+}
+
+.modal-body {
+ padding: 1.5rem;
+ animation: fadeInContent 0.4s ease-out;
+}
+
+@keyframes fadeInContent {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Hint / instance sections */
+.modal-section {
+ margin-bottom: 1.5rem;
+}
+
+.modal-instance label {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ display: block;
+ color: var(--primary);
+}
+
+.instance-url-wrapper {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.instance-url-wrapper input {
+ flex: 1;
+ padding: 0.5rem;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text);
+ font-size: 0.95rem;
+}
+
+.instance-url-wrapper button {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 8px;
+ background: var(--primary);
+ color: white;
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+.instance-url-wrapper button:hover {
+ background: var(--primary-glow);
+ box-shadow: 0 0 12px var(--primary-glow);
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .modal-content {
+ width: 95%;
+ max-height: 95vh;
+ }
+
+ .modal-header,
+ .modal-body {
+ padding: 1rem;
+ }
+}
diff --git a/frontend/src/components/common/Modal.tsx b/frontend/src/components/common/Modal.tsx
new file mode 100644
index 0000000..f8b94ca
--- /dev/null
+++ b/frontend/src/components/common/Modal.tsx
@@ -0,0 +1,60 @@
+import React, { useEffect } from 'react';
+import './Modal.css';
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ title?: string;
+ children: React.ReactNode;
+ instanceUrl?: string; // optional URL for CTF instance
+}
+
+const Modal: React.FC = ({
+ isOpen,
+ onClose,
+ title,
+ children,
+ instanceUrl,
+}) => {
+ useEffect(() => {
+ document.body.style.overflow = isOpen ? 'hidden' : 'unset';
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ const handleCopyUrl = () => {
+ if (instanceUrl) navigator.clipboard.writeText(instanceUrl);
+ };
+
+ return (
+
+
e.stopPropagation()}>
+
+ {title &&
{title}
}
+
+
+
+
+
{children}
+
+ {instanceUrl && (
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/frontend/src/components/common/README.md b/frontend/src/components/common/README.md
new file mode 100644
index 0000000..18d5dae
--- /dev/null
+++ b/frontend/src/components/common/README.md
@@ -0,0 +1,83 @@
+# Toast Notification System
+
+## Overview
+A user-friendly toast notification system that replaces technical error messages with readable notifications.
+
+## Features
+- ✅ Automatic error code translation (429 → "Too many requests...")
+- ✅ Multiple toast types: success, error, warning, info
+- ✅ Auto-dismiss after 5 seconds (configurable)
+- ✅ Smooth animations
+- ✅ Mobile responsive
+- ✅ Stacked notifications support
+
+## Usage
+
+### 1. Import the hook
+```tsx
+import { useToast } from '../components/common/ToastContainer';
+```
+
+### 2. Use in your component
+```tsx
+const MyComponent = () => {
+ const { showToast } = useToast();
+
+ const handleAction = async () => {
+ try {
+ await someApiCall();
+ showToast('Action completed successfully!', 'success');
+ } catch (err) {
+ const errorMessage = getErrorMessage(err);
+ showToast(errorMessage, 'error');
+ }
+ };
+};
+```
+
+## Toast Types
+- `success` - Green gradient, checkmark icon
+- `error` - Red gradient, X icon
+- `warning` - Orange gradient, warning icon
+- `info` - Blue gradient, info icon
+
+## Error Handler
+The `getErrorMessage()` utility automatically translates HTTP status codes:
+
+| Status Code | User-Friendly Message |
+|-------------|----------------------|
+| 429 | Too many requests. Please wait a moment before trying again. |
+| 400 | Invalid request. Please check your input. |
+| 401 | You need to be logged in to perform this action. |
+| 403 | You do not have permission to perform this action. |
+| 404 | The requested resource was not found. |
+| 500 | Server error. Please try again later. |
+| 503 | Service temporarily unavailable. Please try again later. |
+
+## Examples
+
+### Success notification
+```tsx
+showToast('Challenge solved!', 'success');
+```
+
+### Error with automatic translation
+```tsx
+import { getErrorMessage } from '../utils/errorHandler';
+
+try {
+ await submitFlag(flag);
+} catch (err) {
+ showToast(getErrorMessage(err), 'error');
+}
+```
+
+### Warning
+```tsx
+showToast('Instance will expire in 5 minutes', 'warning');
+```
+
+### Info
+```tsx
+showToast('New challenges available!', 'info');
+```
diff --git a/frontend/src/components/common/Toast.css b/frontend/src/components/common/Toast.css
new file mode 100644
index 0000000..57cdcc8
--- /dev/null
+++ b/frontend/src/components/common/Toast.css
@@ -0,0 +1,119 @@
+.toast-container {
+ position: fixed;
+ top: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ pointer-events: none;
+ align-items: center;
+}
+
+.toast {
+ min-width: 300px;
+ max-width: 500px;
+ padding: 1rem 1.5rem;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ animation: slideInDown 0.3s ease, fadeIn 0.3s ease;
+ backdrop-filter: blur(10px);
+ pointer-events: auto;
+}
+
+@keyframes slideInDown {
+ from {
+ transform: translateY(-100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.toast-success {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ color: white;
+}
+
+.toast-error {
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ color: white;
+}
+
+.toast-warning {
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
+ color: white;
+}
+
+.toast-info {
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
+ color: white;
+}
+
+.toast-icon {
+ font-size: 1.5rem;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.toast-message {
+ flex: 1;
+ font-size: 0.95rem;
+ font-weight: 500;
+ line-height: 1.4;
+}
+
+.toast-close {
+ background: transparent;
+ border: none;
+ color: white;
+ font-size: 1.25rem;
+ cursor: pointer;
+ padding: 0.25rem;
+ line-height: 1;
+ opacity: 0.8;
+ transition: opacity 0.2s ease;
+ flex-shrink: 0;
+}
+
+.toast-close:hover {
+ opacity: 1;
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .toast-container {
+ top: 1rem;
+ left: 1rem;
+ right: 1rem;
+ transform: none;
+ }
+
+ .toast {
+ min-width: auto;
+ max-width: none;
+ width: 100%;
+ }
+}
diff --git a/frontend/src/components/common/Toast.tsx b/frontend/src/components/common/Toast.tsx
new file mode 100644
index 0000000..1a50867
--- /dev/null
+++ b/frontend/src/components/common/Toast.tsx
@@ -0,0 +1,51 @@
+import React, { useEffect } from 'react';
+import './Toast.css';
+
+export type ToastType = 'success' | 'error' | 'warning' | 'info';
+
+export interface ToastProps {
+ message: string;
+ type: ToastType;
+ onClose: () => void;
+ duration?: number;
+}
+
+export const Toast: React.FC = ({
+ message,
+ type,
+ onClose,
+ duration = 5000,
+}) => {
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ onClose();
+ }, duration);
+
+ return () => clearTimeout(timer);
+ }, [duration, onClose]);
+
+ const getIcon = () => {
+ switch (type) {
+ case 'success':
+ return '✓';
+ case 'error':
+ return '✕';
+ case 'warning':
+ return '⚠';
+ case 'info':
+ return 'ℹ';
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+
{getIcon()}
+
{message}
+
+
+ );
+};
diff --git a/frontend/src/components/common/ToastContainer.tsx b/frontend/src/components/common/ToastContainer.tsx
new file mode 100644
index 0000000..f19212f
--- /dev/null
+++ b/frontend/src/components/common/ToastContainer.tsx
@@ -0,0 +1,57 @@
+import React, { createContext, useContext, useState, useCallback } from 'react';
+import { Toast, type ToastType } from './Toast';
+
+interface ToastMessage {
+ id: string;
+ message: string;
+ type: ToastType;
+}
+
+interface ToastContextType {
+ showToast: (message: string, type: ToastType) => void;
+}
+
+const ToastContext = createContext(undefined);
+
+export const useToast = () => {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error('useToast must be used within ToastProvider');
+ }
+ return context;
+};
+
+export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [toasts, setToasts] = useState([]);
+
+ const showToast = useCallback((message: string, type: ToastType) => {
+ const id = Date.now().toString();
+ setToasts((prev) => [...prev, { id, message, type }]);
+ }, []);
+
+ const removeToast = useCallback((id: string) => {
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
+ }, []);
+
+ return (
+
+ {children}
+
+ {toasts.map((toast, index) => (
+
+ removeToast(toast.id)}
+ />
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/components/common/input.css b/frontend/src/components/common/input.css
new file mode 100644
index 0000000..28109e6
--- /dev/null
+++ b/frontend/src/components/common/input.css
@@ -0,0 +1,79 @@
+/* Input Component Styles */
+
+.input-group {
+ position: relative;
+ margin-bottom: 1.5rem;
+}
+
+.input-label {
+ display: block;
+ font-weight: 600;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+ margin-bottom: 0.5rem;
+}
+
+.input {
+ width: 100%;
+ padding: 0.875rem 1rem;
+ border-radius: 8px;
+ border: 2px solid #e5e7eb;
+ background: white;
+ font-size: 0.95rem;
+ font-family: 'Inter', sans-serif;
+ transition: all 0.2s ease;
+ color: var(--text-primary);
+}
+
+.input:focus {
+ outline: none;
+ border-color: #6b73ff;
+ box-shadow: 0 0 0 3px rgba(107, 115, 255, 0.1);
+}
+
+.input::placeholder {
+ color: #9ca3af;
+}
+
+.input-error {
+ border-color: #ef4444;
+}
+
+.input-error:focus {
+ border-color: #ef4444;
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
+}
+
+.input-error-text {
+ display: block;
+ color: #ef4444;
+ font-size: 0.875rem;
+ margin-top: 0.5rem;
+ font-weight: 500;
+}
+
+.input-icon {
+ position: absolute;
+ right: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-secondary);
+ pointer-events: none;
+}
+
+.input-password-toggle {
+ position: absolute;
+ right: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0.5rem;
+ transition: var(--transition);
+}
+
+.input-password-toggle:hover {
+ color: #6b73ff;
+}
\ No newline at end of file
diff --git a/frontend/src/components/layout/Footer.css b/frontend/src/components/layout/Footer.css
new file mode 100644
index 0000000..1b8ef8c
--- /dev/null
+++ b/frontend/src/components/layout/Footer.css
@@ -0,0 +1,79 @@
+.footer {
+ background: var(--bg-card);
+ backdrop-filter: blur(20px);
+ border-top: 2px solid var(--border);
+ margin-top: 4rem;
+ padding: 3rem 2rem 1rem;
+}
+
+.footer-container {
+ max-width: 1400px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+ margin-bottom: 2rem;
+}
+
+.footer-section h3, .footer-section h4 {
+ color: var(--primary);
+ margin-bottom: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-size: 1.1rem;
+}
+
+.footer-section p {
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+.footer-section ul {
+ list-style: none;
+}
+
+.footer-section ul li {
+ margin-bottom: 0.5rem;
+}
+
+.footer-section a {
+ color: var(--text-secondary);
+ text-decoration: none;
+ transition: var(--transition);
+ position: relative;
+ display: inline-block;
+}
+
+.footer-section a::before {
+ content: '→ ';
+ opacity: 0;
+ transition: var(--transition);
+}
+
+.footer-section a:hover::before {
+ opacity: 1;
+}
+
+.footer-section a:hover {
+ color: var(--primary);
+ padding-left: 1rem;
+}
+
+.footer-bottom {
+ text-align: center;
+ padding-top: 2rem;
+ border-top: 1px solid var(--border);
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+@media (max-width: 768px) {
+ .footer {
+ padding: 2rem 1rem 1rem;
+ }
+
+ .footer-container {
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx
new file mode 100644
index 0000000..97dcf2f
--- /dev/null
+++ b/frontend/src/components/layout/Footer.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import './Footer.css';
+
+const Footer: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/frontend/src/components/layout/Navbar.css b/frontend/src/components/layout/Navbar.css
new file mode 100644
index 0000000..26e7440
--- /dev/null
+++ b/frontend/src/components/layout/Navbar.css
@@ -0,0 +1,139 @@
+.navbar {
+ color: #f8faff;
+ background: #4385ef;
+ border-bottom: 1px solid rgba(59, 130, 246, 0.3);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+}
+
+.navbar-container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.navbar-logo {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: rgb(234, 229, 229);
+ text-decoration: none;
+}
+
+.navbar-menu {
+ list-style: none;
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+}
+
+.navbar-menu a {
+ text-decoration: none;
+ font-weight: 500;
+ padding: 0.4rem 0.75rem;
+ border-radius: 6px;
+ transition: var(--transition);
+}
+
+.navbar-menu a:hover {
+ background: rgba(255, 255, 255, 0.15);
+ /* CHANGED: Better hover on blue bg */
+ color: white;
+ /* CHANGED: Stay white on hover */
+}
+
+.navbar-menu a.active {
+ color: white;
+ /* CHANGED: White active state */
+ font-weight: 600;
+}
+
+/* Buttons */
+.btn-login,
+.btn-register,
+.btn-logout {
+ padding: 0.5rem 1.25rem;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+ display: inline-block;
+}
+
+.btn-login,
+.btn-register {
+ background: white;
+ color: #4385ef;
+ border-color: white;
+}
+
+.btn-login:hover,
+.btn-register:hover {
+ background: rgba(255, 255, 255, 0.2);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+
+.btn-register:hover {
+ background: #f3f4f6;
+ color: #4385ef;
+}
+
+.btn-logout {
+ color: white;
+ border-color: rgba(255, 255, 255, 0.3);
+ background: rgba(239, 68, 68, 0.8);
+}
+
+.btn-logout:hover {
+ background: rgba(220, 38, 38, 0.9);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* Navbar Button Loading Spinner */
+.btn-login.btn-loading,
+.btn-register.btn-loading,
+.btn-logout.btn-loading {
+ color: transparent !important;
+}
+
+.btn-login.btn-loading::after,
+.btn-register.btn-loading::after,
+.btn-logout.btn-loading::after {
+ content: '';
+ width: 14px;
+ height: 14px;
+ margin-left: -7px;
+ margin-top: -7px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top-color: #0066ff;
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .navbar-container {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .navbar-menu {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+}
diff --git a/frontend/src/components/layout/Navbar.tsx b/frontend/src/components/layout/Navbar.tsx
new file mode 100644
index 0000000..c07bf66
--- /dev/null
+++ b/frontend/src/components/layout/Navbar.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Link, NavLink, useNavigate } from 'react-router-dom';
+import { useAuth } from '../../hooks/useAuth';
+import { useToast } from '../common/ToastContainer';
+import './Navbar.css';
+
+const Navbar: React.FC = () => {
+ const { isAuthenticated, logout } = useAuth();
+ const { showToast } = useToast();
+ const navigate = useNavigate();
+
+ const handleLogout = () => {
+ logout();
+ showToast('Logged out successfully', 'success');
+ navigate('/login');
+ };
+
+ return (
+
+ );
+};
+
+export default Navbar;
diff --git a/frontend/src/components/layout/Sidebar.css b/frontend/src/components/layout/Sidebar.css
new file mode 100644
index 0000000..a98cb1d
--- /dev/null
+++ b/frontend/src/components/layout/Sidebar.css
@@ -0,0 +1,83 @@
+.sidebar {
+ width: 280px;
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 1.5rem;
+ backdrop-filter: blur(10px);
+ position: sticky;
+ top: 100px;
+ height: fit-content;
+}
+
+.sidebar-section {
+ margin-bottom: 2rem;
+}
+
+.sidebar-section:last-child {
+ margin-bottom: 0;
+}
+
+.sidebar-section h3 {
+ color: var(--primary);
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid var(--border);
+}
+
+.sidebar-list {
+ list-style: none;
+}
+
+.sidebar-list li {
+ margin-bottom: 0.5rem;
+}
+
+.sidebar-list a {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-radius: 8px;
+ transition: var(--transition);
+ position: relative;
+ overflow: hidden;
+}
+
+.sidebar-list a::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 3px;
+ background: linear-gradient(180deg, var(--primary), var(--accent));
+ transform: scaleY(0);
+ transition: var(--transition);
+}
+
+.sidebar-list a:hover, .sidebar-list a.active {
+ background: rgba(0, 240, 255, 0.1);
+ color: var(--primary);
+}
+
+.sidebar-list a:hover::before, .sidebar-list a.active::before {
+ transform: scaleY(1);
+}
+
+.sidebar-icon {
+ font-size: 1.2rem;
+}
+
+@media (max-width: 1024px) {
+ .sidebar {
+ width: 100%;
+ position: relative;
+ top: 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..20fea59
--- /dev/null
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import './Sidebar.css';
+
+const Sidebar: React.FC = () => {
+ const location = useLocation();
+
+ const categories = [
+ { name: 'All', path: '/challenges' },
+ { name: 'Web', path: '/challenges?category=web' },
+ { name: 'Crypto', path: '/challenges?category=crypto' },
+ { name: 'Reverse', path: '/challenges?category=reverse' },
+ { name: 'Forensics', path: '/challenges?category=forensics' },
+ { name: 'Pwn', icon: '', path: '/challenges?category=pwn' },
+ ];
+
+ return (
+
+ );
+};
+
+export default Sidebar;
diff --git a/frontend/src/components/leaderboard/Leaderboard.css b/frontend/src/components/leaderboard/Leaderboard.css
new file mode 100644
index 0000000..29814d7
--- /dev/null
+++ b/frontend/src/components/leaderboard/Leaderboard.css
@@ -0,0 +1,87 @@
+.leaderboard-container {
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.leaderboard-container h1 {
+ color: var(--primary);
+ font-size: 2.5rem;
+ text-align: center;
+ margin-bottom: 2rem;
+ text-shadow: 0 0 30px var(--primary-glow);
+}
+
+.leaderboard-table {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 1.5rem;
+ backdrop-filter: blur(10px);
+ overflow-x: auto;
+}
+
+.leaderboard-table table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.leaderboard-table thead {
+ border-bottom: 2px solid var(--border);
+}
+
+.leaderboard-table th {
+ padding: 1rem;
+ text-align: left;
+ color: var(--primary);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-size: 0.9rem;
+}
+
+.leaderboard-table td {
+ padding: 1rem;
+ color: var(--text-secondary);
+ border-bottom: 1px solid rgba(0, 240, 255, 0.1);
+}
+
+.leaderboard-table tbody tr {
+ transition: var(--transition);
+}
+
+.leaderboard-table tbody tr:hover {
+ background: rgba(0, 240, 255, 0.05);
+}
+
+.leaderboard-table .top-three {
+ background: linear-gradient(90deg, rgba(0, 240, 255, 0.1), transparent);
+}
+
+.rank-cell {
+ font-size: 1.5rem;
+ font-weight: 700;
+ text-align: center !important;
+}
+
+.username-cell {
+ color: var(--primary);
+ font-weight: 600;
+}
+
+.score-cell {
+ font-weight: 700;
+ color: var(--success);
+ font-size: 1.1rem;
+}
+
+@media (max-width: 768px) {
+ .leaderboard-table {
+ padding: 1rem;
+ }
+
+ .leaderboard-table th,
+ .leaderboard-table td {
+ padding: 0.5rem;
+ font-size: 0.9rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/leaderboard/Leaderboard.tsx b/frontend/src/components/leaderboard/Leaderboard.tsx
new file mode 100644
index 0000000..c779ab3
--- /dev/null
+++ b/frontend/src/components/leaderboard/Leaderboard.tsx
@@ -0,0 +1,83 @@
+import React, { useState, useEffect } from 'react';
+import {
+ submissionService,
+ type LeaderboardEntry,
+} from '../../services/submissionService';
+import './Leaderboard.css';
+
+const Leaderboard: React.FC = () => {
+ const [leaderboard, setLeaderboard] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchLeaderboard = async () => {
+ try {
+ const data = await submissionService.getLeaderboard();
+ setLeaderboard(data);
+ } catch (error) {
+ console.error('Failed to fetch leaderboard:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchLeaderboard();
+
+ // Refresh every 30 seconds
+ const interval = setInterval(fetchLeaderboard, 30000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const getMedalEmoji = (rank: number) => {
+ switch (rank) {
+ case 1:
+ return '🥇';
+ case 2:
+ return '🥈';
+ case 3:
+ return '🥉';
+ default:
+ return rank;
+ }
+ };
+
+ if (loading) {
+ return Loading leaderboard...
;
+ }
+
+ return (
+
+
🏆 Leaderboard
+
+
+
+
+
+ | Rank |
+ User |
+ Team |
+ Challenges Solved |
+ Score |
+
+
+
+ {leaderboard.map((entry) => (
+
+ | {getMedalEmoji(entry.rank)} |
+ {entry.username} |
+ {entry.teamName || '-'} |
+ {entry.solvedCount} |
+ {entry.score} |
+
+ ))}
+
+
+
+
+ );
+};
+
+export default Leaderboard;
diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx
new file mode 100644
index 0000000..4326d89
--- /dev/null
+++ b/frontend/src/context/AuthContext.tsx
@@ -0,0 +1,104 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from 'react';
+
+import {
+ authService,
+ type User,
+ type LoginCredentials,
+ type RegisterData,
+} from '../services/authService';
+
+/* =====================
+ TYPES
+===================== */
+
+export interface AuthContextType {
+ user: User | null;
+ isAuthenticated: boolean;
+ loading: boolean;
+ login: (credentials: LoginCredentials) => Promise;
+ register: (data: RegisterData) => Promise;
+ logout: () => Promise;
+ refreshUser: () => void; // ✅ ADD THIS
+}
+
+/* =====================
+ CONTEXT (EXPORT IT)
+===================== */
+
+export const AuthContext = createContext(null);
+
+/* =====================
+ HOOK
+===================== */
+
+export const useAuth = (): AuthContextType => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within AuthProvider');
+ }
+ return context;
+};
+
+/* =====================
+ PROVIDER
+===================== */
+
+interface AuthProviderProps {
+ children: ReactNode;
+}
+
+export const AuthProvider: React.FC = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ // Init auth from localStorage
+ useEffect(() => {
+ refreshUser();
+ setLoading(false);
+ }, []);
+
+ const login = async (credentials: LoginCredentials) => {
+ const user = await authService.login(
+ credentials.email,
+ credentials.password,
+ );
+ setUser(user);
+ };
+
+ const register = async (data: RegisterData) => {
+ await authService.register(data);
+ };
+
+ const logout = async () => {
+ await authService.logout();
+ setUser(null);
+ };
+
+ // ✅ THIS FIXES YOUR ERROR
+ const refreshUser = () => {
+ const savedUser = authService.getCurrentUser();
+ setUser(savedUser);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts
new file mode 100644
index 0000000..de602e2
--- /dev/null
+++ b/frontend/src/hooks/useAuth.ts
@@ -0,0 +1,11 @@
+// src/hooks/useAuth.ts - CORRECT PATH
+import { useContext } from 'react';
+import { AuthContext } from '../context/AuthContext';
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within AuthProvider');
+ }
+ return context;
+};
diff --git a/frontend/src/hooks/useChallenges.ts b/frontend/src/hooks/useChallenges.ts
new file mode 100644
index 0000000..8d29a75
--- /dev/null
+++ b/frontend/src/hooks/useChallenges.ts
@@ -0,0 +1,32 @@
+import { useState, useEffect } from 'react';
+import { challengeService } from '../services/challengeService';
+import type { Challenge } from '../services/challengeService';
+export const useChallenges = (category?: string, difficulty?: string) => {
+ const [challenges, setChallenges] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchChallenges = async () => {
+ try {
+ setLoading(true);
+ const data = await challengeService.getAllChallenges(
+ category,
+ difficulty,
+ );
+ setChallenges(data);
+ setError(null);
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ setError(err.message || 'Erreur lors du chargement');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchChallenges();
+ }, [category, difficulty]);
+
+ return { challenges, loading, error, refetch: () => setChallenges([]) };
+};
diff --git a/frontend/src/hooks/useCtf.ts b/frontend/src/hooks/useCtf.ts
new file mode 100644
index 0000000..db089d4
--- /dev/null
+++ b/frontend/src/hooks/useCtf.ts
@@ -0,0 +1,92 @@
+import { useState, useEffect } from 'react';
+import { ctfService } from '../services/ctfService';
+import type { Ctf, CtfInstance } from '../types/ctf';
+
+export const useCtf = () => {
+ const [ctfs, setCtfs] = useState([]);
+ const [activeInstance, setActiveInstance] = useState(
+ null,
+ );
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchCtfs = async () => {
+ try {
+ setLoading(true);
+ const data = await ctfService.getAllCtfs();
+ setCtfs(data);
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : 'Failed to load challenges',
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchActiveInstance = async () => {
+ try {
+ const instance = await ctfService.getActiveInstance();
+ setActiveInstance(instance);
+ } catch (err) {
+ console.error('Failed to fetch active instance:', err);
+ }
+ };
+
+ const startInstance = async (ctfId: string) => {
+ try {
+ setError(null);
+ const instance = await ctfService.startInstance(ctfId);
+ setActiveInstance(instance);
+ return instance;
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : 'Failed to start instance';
+ setError(message);
+ throw err;
+ }
+ };
+
+ const stopInstance = async (instanceId: string) => {
+ try {
+ setError(null);
+ await ctfService.stopInstance(instanceId);
+ setActiveInstance(null);
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : 'Failed to stop instance';
+ setError(message);
+ throw err;
+ }
+ };
+
+ const submitFlag = async (ctfId: string, flag: string) => {
+ try {
+ setError(null);
+ const result = await ctfService.submitFlag(ctfId, flag);
+ await fetchCtfs();
+ return result;
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Wrong flag';
+ setError(message);
+ throw err;
+ }
+ };
+
+ useEffect(() => {
+ fetchCtfs();
+ fetchActiveInstance();
+ }, []);
+
+ return {
+ ctfs,
+ activeInstance,
+ loading,
+ error,
+ startInstance,
+ stopInstance,
+ submitFlag,
+ refreshCtfs: fetchCtfs,
+ refreshActiveInstance: fetchActiveInstance,
+ };
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..da1d324
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,116 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --primary: #6b73ff;
+ --primary-dark: #5860e6;
+
+ --bg-primary: #f6f7fb;
+ --bg-secondary: #ffffff;
+ --bg-tertiary: #eef0f7;
+ --bg-card: #ffffff;
+
+ --text-primary: #1f2937;
+ --text-secondary: #6b7280;
+ --text-muted: #9ca3af;
+
+ --border: #e5e7eb;
+ --border-hover: #d1d5db;
+
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
+
+ --transition: all 0.2s ease;
+}
+
+body {
+ font-family: 'Inter', sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.5;
+}
+
+/* ===== LAYOUT ===== */
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.main-content {
+ flex: 1;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 2rem;
+ width: 100%;
+}
+
+/* ===== BUTTONS ===== */
+button {
+ font-family: 'Inter', sans-serif;
+}
+
+.btn-primary:hover:not(:disabled),
+button[type="submit"]:hover:not(:disabled) {
+ background: #5860e6;
+}
+
+.btn-outline {
+ background: transparent;
+ color: #6b73ff;
+ border: 2px solid #6b73ff;
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn-outline:hover:not(:disabled) {
+ background: #f0f1ff;
+}
+
+button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* ===== INPUTS ===== */
+.input,
+input[type="text"],
+input[type="email"],
+input[type="password"],
+select {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ border: 2px solid #e5e7eb;
+ background: white;
+ font-size: 0.95rem;
+ font-family: 'Inter', sans-serif;
+ transition: all 0.2s ease;
+}
+
+.input:focus,
+input:focus,
+select:focus {
+ outline: none;
+ border-color: #6b73ff;
+ box-shadow: 0 0 0 3px rgba(107, 115, 255, 0.1);
+}
+
+/* ===== SCROLLBAR ===== */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-hover);
+ border-radius: 4px;
+}
diff --git a/frontend/src/layout/Navbar.tsx b/frontend/src/layout/Navbar.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..0d10ab5
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+// Get the root element from index.html
+const rootElement = document.getElementById('root');
+
+if (!rootElement) {
+ throw new Error('Root element not found');
+}
+
+// Create the React root and render the app
+const root = ReactDOM.createRoot(rootElement);
+
+root.render(
+
+
+ ,
+);
diff --git a/frontend/src/models/Challenge.ts b/frontend/src/models/Challenge.ts
new file mode 100644
index 0000000..3944aec
--- /dev/null
+++ b/frontend/src/models/Challenge.ts
@@ -0,0 +1,34 @@
+export type ChallengeType =
+ | 'WEB_EXPLOIT'
+ | 'BINARY_EXPLOIT'
+ | 'CRYPTO'
+ | 'FORENSICS'
+ | 'REVERSE';
+
+export type Difficulty = 'EASY' | 'MID' | 'HARD';
+
+export interface Container {
+ serviceName: string;
+ image: string;
+ internalPort: number;
+ exposed: boolean;
+ env: Record;
+}
+
+export interface Challenge {
+ _id: string;
+ type: ChallengeType;
+ title: string;
+ description: string;
+ category: string;
+ points: number;
+ difficulty: Difficulty;
+ solves: number;
+ solved?: boolean;
+ hints?: string[];
+ withShell: boolean;
+ resources: string[];
+ containers: Container[];
+ createdAt: string;
+ updatedAt: string;
+}
diff --git a/frontend/src/pages/Challenges.css b/frontend/src/pages/Challenges.css
new file mode 100644
index 0000000..f47a051
--- /dev/null
+++ b/frontend/src/pages/Challenges.css
@@ -0,0 +1,425 @@
+.challenges-page {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 2rem;
+ background: var(--bg-primary);
+ min-height: 100vh;
+ color: var(--text-primary);
+}
+
+.page-header {
+ margin-bottom: 2rem;
+}
+
+.page-header h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+ color: var(--primary);
+}
+
+.page-header p {
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+}
+
+.user-stats {
+ margin-top: 1rem;
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.stat-badge {
+ background: linear-gradient(135deg, #6b73ff 0%, #5860e6 100%);
+ color: white;
+ padding: 0.75rem 1.5rem;
+ border-radius: 12px;
+ font-size: 1rem;
+ font-weight: 600;
+ box-shadow: 0 4px 12px rgba(107, 115, 255, 0.3);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.page-content {
+ display: flex;
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+.categories-sidebar {
+ width: 280px;
+ min-width: 280px;
+ background: var(--bg-secondary);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 1.5rem;
+ height: fit-content;
+ position: sticky;
+ top: 2rem;
+}
+
+.sidebar-header {
+ color: var(--primary);
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.sidebar-top-select {
+ margin-bottom: 1.5rem;
+ width: 100%;
+}
+
+.sidebar-top-select label {
+ display: block;
+ font-weight: 600;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+ margin-bottom: 0.5rem;
+}
+
+.sidebar-top-select select {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ font-weight: 500;
+ cursor: pointer;
+ transition: border-color 0.2s ease;
+}
+
+.sidebar-top-select select:hover {
+ border-color: #0066ff;
+}
+
+.sidebar-top-select select:focus {
+ outline: none;
+ border-color: #0066ff;
+ box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
+}
+
+.category-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.category-item {
+ padding: 0.75rem 1rem;
+ background: transparent;
+ color: var(--text-secondary);
+ border: 1px solid transparent;
+ border-radius: 8px;
+ font-weight: 500;
+ font-size: 0.95rem;
+ cursor: pointer;
+ text-align: left;
+ transition: all 0.2s ease;
+}
+
+.category-item:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border-color: #0066ff;
+}
+
+.category-item.active {
+ background: var(--primary);
+ color: white;
+ border-color: #0066ff;
+ font-weight: 600;
+}
+
+.main-content {
+ flex: 1;
+}
+
+.difficulty-radio-group {
+ background: var(--bg-secondary);
+ border: 2px solid var(--border);
+ border-radius: 12px;
+ padding: 1.25rem;
+ margin-bottom: 1.5rem;
+}
+
+.radio-label {
+ display: block;
+ color: var(--primary);
+ font-weight: 600;
+ font-size: 0.95rem;
+ margin-bottom: 1rem;
+}
+
+.radio-options {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.radio-option {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ padding: 0.5rem 0.75rem;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+}
+
+.radio-option input[type="radio"] {
+ width: 16px;
+ height: 16px;
+ accent-color: #0066ff;
+ cursor: pointer;
+}
+
+.radio-option span {
+ font-weight: 500;
+ color: var(--text-primary);
+ font-size: 0.9rem;
+}
+
+.radio-option:hover {
+ background: var(--bg-tertiary);
+}
+
+.radio-option input[type="radio"]:checked+span {
+ color: #0066ff;
+ font-weight: 600;
+}
+
+/* Filters */
+.filters-container {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ padding: 1.5rem;
+ background: var(--bg-secondary);
+ border: 2px solid var(--border);
+ border-radius: 12px;
+}
+
+.filter-group {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.filter-group label {
+ font-weight: 600;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+}
+
+.filter-group select {
+ padding: 0.6rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ cursor: pointer;
+ font-weight: 500;
+ transition: border-color 0.2s ease;
+}
+
+.filter-group select:hover {
+ border-color: var(--primary);
+}
+
+.filter-group select:focus {
+ outline: none;
+ border-color: #0066ff;
+ box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
+}
+
+/* Stats Cards */
+.stats-container {
+ display: flex;
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+@media (max-width: 768px) {
+ .stats-container {
+ flex-direction: column;
+ }
+}
+
+.stat-card {
+ background: var(--bg-secondary);
+ border: 2px solid var(--primary);
+ padding: 2rem 2.5rem;
+ border-radius: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.2s ease;
+ flex: 1;
+ min-width: 200px;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ border-color: #0066ff;
+ box-shadow: 0 8px 24px rgba(0, 102, 255, 0.15);
+}
+
+.stat-value {
+ font-size: 3rem;
+ font-weight: 700;
+ color: var(--primary);
+ margin-bottom: 0.5rem;
+}
+
+.stat-label {
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Challenges Grid */
+.challenges-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 2rem;
+}
+
+/* Loading, Error, No Challenges */
+.loading-container,
+.error-container,
+.no-challenges {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: var(--text-secondary);
+}
+
+.spinner {
+ border: 4px solid var(--border);
+ border-top: 4px solid #0066ff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.loading-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 3;
+}
+
+.loading-card {
+ background: var(--bg-secondary);
+ padding: 3rem;
+ border-radius: 16px;
+ text-align: center;
+ border: 2px solid var(--border);
+}
+
+.loading-card p {
+ color: var(--text-primary);
+ font-size: 1.1rem;
+ margin-top: 1rem;
+}
+
+.error-container {
+ background: var(--bg-secondary);
+ padding: 3rem;
+ border-radius: 16px;
+ border: 2px solid var(--error);
+ max-width: 500px;
+ margin: 4rem auto;
+}
+
+.error-container h2 {
+ color: var(--error);
+ margin-bottom: 1rem;
+}
+
+.error-container p {
+ color: var(--text-secondary);
+}
+
+.error-container button {
+ margin-top: 1.5rem;
+ padding: 0.75rem 2rem;
+ background: var(--primary);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: background 0.2s ease;
+}
+
+.error-container button:hover {
+ background: var(--primary-dark);
+}
+
+.no-challenges {
+ background: var(--bg-secondary);
+ padding: 3rem;
+ border-radius: 16px;
+ border: 2px solid var(--border);
+}
+
+.no-challenges p {
+ font-size: 1.1rem;
+ color: var(--text-secondary);
+}
+
+/* Responsive */
+@media (max-width: 1024px) {
+ .page-content {
+ flex-direction: column;
+ }
+
+ .categories-sidebar {
+ width: 100%;
+ position: static;
+ }
+}
+
+@media (max-width: 768px) {
+ .challenges-page {
+ padding: 1rem;
+ }
+
+ .page-header h1 {
+ font-size: 2rem;
+ }
+
+ .filters-container {
+ flex-direction: column;
+ }
+
+ .radio-options {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .challenges-grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+}
diff --git a/frontend/src/pages/Challenges.tsx b/frontend/src/pages/Challenges.tsx
new file mode 100644
index 0000000..a9fb692
--- /dev/null
+++ b/frontend/src/pages/Challenges.tsx
@@ -0,0 +1,395 @@
+import React, { useState } from 'react';
+import { useCtf } from '../hooks/useCtf';
+import { useAuth } from '../context/AuthContext';
+import { useToast } from '../components/common/ToastContainer';
+import { getErrorMessage } from '../utils/errorHandler';
+import ChallengeCard from '../components/challenges/ChallengeCard';
+import { ActiveInstanceBanner } from '../components/ActiveInstanceBanner';
+import Modal from '../components/common/Modal';
+import { ChallengeModal } from '../components/challenges/ChallengeModal';
+import './Challenges.css';
+import type { Ctf } from '../types/ctf';
+
+const ChallengePage: React.FC = () => {
+ const { user, refreshUser } = useAuth();
+ const { showToast } = useToast();
+ const {
+ ctfs,
+ activeInstance,
+ error,
+ startInstance,
+ stopInstance,
+ submitFlag,
+ } = useCtf();
+
+ const [selectedChallenge, setSelectedChallenge] = useState(null);
+ const [launchModalCtf, setLaunchModalCtf] = useState<{
+ id: string;
+ name: string;
+ } | null>(null);
+ const [instanceUrl, setInstanceUrl] = useState('');
+ const [actionLoading, setActionLoading] = useState(false);
+ const [filterCategory, setFilterCategory] = useState('all');
+ const [filterDifficulty, setFilterDifficulty] = useState('all');
+
+ const handleLaunchClick = (ctfId: string, ctfName: string) => {
+ setLaunchModalCtf({ id: ctfId, name: ctfName });
+ };
+
+ const handleConfirmLaunch = async () => {
+ if (!launchModalCtf) return;
+
+ try {
+ setActionLoading(true);
+ const instance = await startInstance(launchModalCtf.id);
+ if (instance.url) {
+ setInstanceUrl(instance.url);
+ showToast('Instance launched successfully!', 'success');
+ }
+ } catch (err) {
+ const errorMessage = getErrorMessage(err);
+ showToast(errorMessage, 'error');
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleStopInstance = async () => {
+ if (!activeInstance) return;
+ const confirmed = window.confirm(
+ 'Are you sure you want to stop this instance?\n\nYou will need to restart it to continue the challenge.',
+ );
+ if (!confirmed) return;
+ try {
+ setActionLoading(true);
+ await stopInstance(activeInstance.id);
+ showToast('Instance stopped successfully', 'success');
+ } catch (err) {
+ const errorMessage = getErrorMessage(err);
+ showToast(errorMessage, 'error');
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleSubmitFlag = async (flag: string) => {
+ if (!selectedChallenge) return;
+
+ // Check if already solved
+ const isSolved = user?.solvedCtf?.includes(selectedChallenge.id);
+ if (isSolved) {
+ showToast('You have already solved this challenge!', 'info');
+ return;
+ }
+
+ try {
+ setActionLoading(true);
+ const result = await submitFlag(selectedChallenge.id, flag);
+
+ if (result.success) {
+ showToast(result.message || 'Flag submitted successfully!', 'success');
+
+ // Update user data in localStorage immediately
+ if (user) {
+ const updatedUser = {
+ ...user,
+ solvedCtf: [...user.solvedCtf, selectedChallenge.id],
+ numberOfSolvedCtf: (user.numberOfSolvedCtf || 0) + 1,
+ };
+
+ // Update localStorage and refresh context
+ const authService = await import('../services/authService');
+ authService.default.updateUser(updatedUser);
+ refreshUser();
+ }
+
+ // Close modal
+ setSelectedChallenge(null);
+ } else {
+ showToast(result.message || 'Incorrect flag', 'error');
+ }
+ } catch (err) {
+ const errorMessage = getErrorMessage(err);
+ showToast(errorMessage, 'error');
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ // Get solved CTF IDs from user
+ const solvedCtfIds: string[] = user?.solvedCtf || [];
+
+ const filteredCtfs = ctfs.filter((ctf) => {
+ const categoryMatch =
+ filterCategory === 'all' || ctf.type === filterCategory;
+ const difficultyMatch =
+ filterDifficulty === 'all' || ctf.difficulty === filterDifficulty;
+ return categoryMatch && difficultyMatch;
+ });
+
+ if (error) {
+ return (
+
+
+
❌ Error Loading Challenges
+
{error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
CTF Challenges
+
Test your skills across various cybersecurity challenges
+ {user && (
+
+
+ 🏆 {user.numberOfSolvedCtf || 0} / {ctfs.length} Solved
+
+
+ )}
+
+
+
+
+
+
Category
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {activeInstance && (
+
+ )}
+
+
+
+
+ {filteredCtfs.length === 0 ? (
+
+
No challenges found with the selected filters
+
+ ) : (
+ filteredCtfs.map((ctf) => {
+ const isSolved = solvedCtfIds.includes(ctf.id);
+ return (
+
setSelectedChallenge(ctf)}
+ isSolved={isSolved}
+ />
+ );
+ })
+ )}
+
+
+
+
+ {/* Challenge Detail Modal */}
+ {selectedChallenge && (
+
setSelectedChallenge(null)}
+ onLaunchInstance={handleLaunchClick}
+ onSubmitFlag={handleSubmitFlag}
+ activeInstance={
+ activeInstance && activeInstance.ctfId === selectedChallenge.id
+ ? activeInstance
+ : null
+ }
+ onStopInstance={handleStopInstance}
+ isLoading={actionLoading}
+ isSolved={solvedCtfIds.includes(selectedChallenge.id)}
+ />
+ )}
+
+ {/* Launch Instance Modal */}
+ {launchModalCtf && (
+ {
+ setLaunchModalCtf(null);
+ setInstanceUrl('');
+ }}
+ title="Launch Instance"
+ instanceUrl={instanceUrl}
+ >
+ {!instanceUrl ? (
+
+
+ Launch a new instance for{' '}
+ {launchModalCtf?.name}?
+
+
+ This will create a new container. Existing instances will be
+ unaffected.
+
+
+
+
+
+
+ ) : (
+
+
+ Instance launched successfully!{' '}
+ Do not close this tab.
+
+
+
+ )}
+
+ )}
+
+ );
+};
+
+export default ChallengePage;
diff --git a/frontend/src/pages/Home.css b/frontend/src/pages/Home.css
new file mode 100644
index 0000000..4a5a341
--- /dev/null
+++ b/frontend/src/pages/Home.css
@@ -0,0 +1,167 @@
+.home-page {
+ width: 100%;
+}
+
+.hero {
+ text-align: center;
+ padding: 4rem 2rem;
+ margin-bottom: 4rem;
+ position: relative;
+}
+
+.hero h1 {
+ font-size: 3.5rem;
+ color: var(--primary);
+ margin-bottom: 1rem;
+ text-shadow: 0 0 40px var(--primary-glow);
+ animation: glow 3s ease-in-out infinite;
+}
+
+@keyframes glow {
+
+ 0%,
+ 100% {
+ text-shadow: 0 0 40px var(--primary-glow);
+ }
+
+ 50% {
+ text-shadow: 0 0 60px var(--primary-glow), 0 0 80px var(--primary-glow);
+ }
+}
+
+.hero-subtitle {
+ font-size: 1.3rem;
+ color: var(--text-secondary);
+ margin-bottom: 2rem;
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+ line-height: 1.6;
+}
+
+.hero-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-top: 2rem;
+}
+
+.features {
+ margin: 4rem 0;
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+}
+
+.feature-card {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 2rem;
+ text-align: center;
+ transition: var(--transition);
+ backdrop-filter: blur(10px);
+}
+
+.feature-card:hover {
+ border-color: var(--primary);
+ transform: translateY(-8px);
+ box-shadow: 0 12px 40px var(--primary-glow);
+}
+
+.feature-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ filter: drop-shadow(0 0 10px var(--primary-glow));
+}
+
+.feature-card h3 {
+ color: var(--primary);
+ margin-bottom: 0.5rem;
+ font-size: 1.3rem;
+}
+
+.feature-card p {
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+.stats {
+ text-align: center;
+ margin: 4rem 0;
+ padding: 3rem 2rem;
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 24px;
+ backdrop-filter: blur(10px);
+}
+
+.stats h2 {
+ color: var(--primary);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ text-shadow: 0 0 20px var(--primary-glow);
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 2rem;
+}
+
+.stat-card {
+ padding: 2rem;
+ background: linear-gradient(135deg, rgba(0, 240, 255, 0.1), rgba(131, 56, 236, 0.1));
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ transition: var(--transition);
+}
+
+.stat-card:hover {
+ border-color: var(--primary);
+ transform: translateY(-4px);
+ box-shadow: 0 8px 30px var(--primary-glow);
+}
+
+.stat-number {
+ font-size: 3rem;
+ font-weight: 700;
+ color: var(--primary);
+ margin-bottom: 0.5rem;
+ text-shadow: 0 0 20px var(--primary-glow);
+}
+
+.stat-label {
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+@media (max-width: 768px) {
+ .hero h1 {
+ font-size: 2rem;
+ }
+
+ .hero-subtitle {
+ font-size: 1rem;
+ }
+
+ .hero-actions {
+ flex-direction: column;
+ }
+
+ .feature-grid,
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stat-number {
+ font-size: 2rem;
+ }
+}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
new file mode 100644
index 0000000..29f44c7
--- /dev/null
+++ b/frontend/src/pages/Home.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { useAuth } from '../hooks/useAuth';
+import './Home.css';
+
+const Home: React.FC = () => {
+ const { isAuthenticated } = useAuth();
+
+ return (
+
+
+ 🚩 Welcome to CTF Platform
+
+ Test your hacking skills with challenging cybersecurity puzzles
+
+ {!isAuthenticated && (
+
+
+ Get Started
+
+
+ )}
+ {isAuthenticated && (
+
+
+ View Challenges
+
+
+ )}
+
+
+
+
+
+
Web Exploitation
+
SQL injection, XSS, CSRF and more
+
+
+
Cryptography
+
Crack codes and break encryption
+
+
+
Reverse Engineering
+
Analyze binaries and find vulnerabilities
+
+
+
Forensics
+
Investigate and find hidden data
+
+
+
+
+ );
+};
+
+export default Home;
diff --git a/frontend/src/pages/Profile.css b/frontend/src/pages/Profile.css
new file mode 100644
index 0000000..978feef
--- /dev/null
+++ b/frontend/src/pages/Profile.css
@@ -0,0 +1,174 @@
+.profile-page {
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.profile-header {
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+ padding: 2rem;
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ margin-bottom: 2rem;
+ backdrop-filter: blur(10px);
+}
+
+.profile-avatar {
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--primary), var(--accent));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 3rem;
+ font-weight: 700;
+ color: white;
+ box-shadow: 0 0 30px var(--primary-glow);
+ flex-shrink: 0;
+}
+
+.profile-info h1 {
+ color: var(--primary);
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+.profile-info p {
+ color: var(--text-secondary);
+ margin-bottom: 0.25rem;
+}
+
+.team-name {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.profile-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.stat-box {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 12px;
+ padding: 1.5rem;
+ text-align: center;
+ transition: var(--transition);
+ backdrop-filter: blur(10px);
+}
+
+.stat-box:hover {
+ border-color: var(--primary);
+ transform: translateY(-4px);
+ box-shadow: 0 8px 30px var(--primary-glow);
+}
+
+.stat-value {
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--primary);
+ margin-bottom: 0.5rem;
+ text-shadow: 0 0 15px var(--primary-glow);
+}
+
+.stat-label {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.profile-submissions {
+ background: var(--bg-card);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 2rem;
+ backdrop-filter: blur(10px);
+}
+
+.profile-submissions h2 {
+ color: var(--primary);
+ margin-bottom: 1.5rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.submissions-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.submissions-table th {
+ text-align: left;
+ padding: 1rem;
+ color: var(--primary);
+ font-weight: 600;
+ border-bottom: 2px solid var(--border);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-size: 0.9rem;
+}
+
+.submissions-table td {
+ padding: 1rem;
+ color: var(--text-secondary);
+ border-bottom: 1px solid rgba(0, 240, 255, 0.1);
+}
+
+.submissions-table tbody tr {
+ transition: var(--transition);
+}
+
+.submissions-table tbody tr:hover {
+ background: rgba(0, 240, 255, 0.05);
+}
+
+.status-badge {
+ padding: 0.5rem 1rem;
+ border-radius: 20px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ display: inline-block;
+}
+
+.status-badge.correct {
+ background: rgba(6, 255, 165, 0.2);
+ color: var(--success);
+ border: 1px solid var(--success);
+}
+
+.status-badge.incorrect {
+ background: rgba(255, 0, 110, 0.2);
+ color: var(--danger);
+ border: 1px solid var(--danger);
+}
+
+@media (max-width: 768px) {
+ .profile-header {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .profile-info h1 {
+ font-size: 1.5rem;
+ }
+
+ .profile-stats {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .submissions-table {
+ font-size: 0.85rem;
+ }
+
+ .submissions-table th,
+ .submissions-table td {
+ padding: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
new file mode 100644
index 0000000..6ac1484
--- /dev/null
+++ b/frontend/src/pages/Profile.tsx
@@ -0,0 +1,148 @@
+// src/pages/Profile.tsx
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../hooks/useAuth';
+import {
+ submissionService,
+ type Submission,
+} from '../services/submissionService';
+import './Profile.css';
+
+const Profile: React.FC = () => {
+ const { user, loading: authLoading } = useAuth();
+ const [submissions, setSubmissions] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchSubmissions = async () => {
+ try {
+ const data = await submissionService.getMySubmissions();
+ setSubmissions(data);
+ } catch (error: any) {
+ // Ignore 401 to avoid forcing logout
+ if (error.response?.status !== 401) {
+ console.error('Failed to fetch submissions:', error);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (user) {
+ fetchSubmissions();
+ } else {
+ setLoading(false);
+ }
+ }, [user]);
+
+ // ✅ Show auth loading
+ if (authLoading) {
+ return (
+
+ );
+ }
+
+ // ✅ Show login prompt if no user
+ if (!user) {
+ return (
+
+ );
+ }
+
+ // ✅ Safe values
+ const username = user.username ?? 'User';
+ const avatarLetter = username.charAt(0).toUpperCase();
+ const email = user.email ?? 'N/A';
+ const score = user.score ?? 0;
+ const correctSubmissions = submissions.filter((s) => s.correct).length;
+ const totalSubmissions = submissions.length;
+ const successRate =
+ totalSubmissions > 0
+ ? Math.round((correctSubmissions / totalSubmissions) * 100)
+ : 0;
+
+ return (
+
+
+
{avatarLetter}
+
+
{username}
+
{email}
+ {user.teamName &&
Team: {user.teamName}
}
+
+
+
+
+
+
⭐ {score}
+
Total Score
+
+
+
{correctSubmissions}
+
Challenges Solved
+
+
+
{successRate}%
+
Success Rate
+
+
+
{totalSubmissions}
+
Total Attempts
+
+
+
+
+
Recent Submissions
+ {loading ? (
+
Loading submissions...
+ ) : submissions.length === 0 ? (
+
+ No submissions yet. Start solving challenges!
+
+ ) : (
+
+
+
+ | Challenge |
+ Status |
+ Date |
+
+
+
+ {submissions.slice(0, 10).map((submission) => (
+
+ | {submission.ctfId?.slice(-6) || 'Challenge'} |
+
+
+ {submission.correct ? '✓ Correct' : '✗ Incorrect'}
+
+ |
+
+ {new Date(submission.submittedAt).toLocaleDateString()}
+ |
+
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+export default Profile;
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
new file mode 100644
index 0000000..16e9ae1
--- /dev/null
+++ b/frontend/src/services/api.ts
@@ -0,0 +1,46 @@
+import axios from 'axios';
+
+const API_BASE_URL =
+ import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
+
+const api = axios.create({
+ baseURL: API_BASE_URL,
+ headers: { 'Content-Type': 'application/json' },
+ withCredentials: false,
+});
+
+api.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers = config.headers ?? {};
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => Promise.reject(error),
+);
+
+// 🚫 DO NOT AUTO-LOGOUT ON EVERY 401
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ const status = error.response?.status;
+
+ // Only redirect if token exists AND backend explicitly rejects it
+ if (status === 401 && localStorage.getItem('token')) {
+ console.warn('Unauthorized – token invalid or expired');
+
+ // Clear auth ONCE
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+
+ // Hard redirect to reset state
+ window.location.replace('/login');
+ }
+
+ return Promise.reject(error);
+ },
+);
+
+export default api;
diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts
new file mode 100644
index 0000000..bf1d39e
--- /dev/null
+++ b/frontend/src/services/authService.ts
@@ -0,0 +1,78 @@
+// src/services/authService.ts
+import axios, { type AxiosResponse } from 'axios';
+
+export interface RegisterData {
+ email: string;
+ password: string;
+ confirmPassword: string;
+}
+
+export interface LoginCredentials {
+ email: string;
+ password: string;
+}
+
+export interface User {
+ id: string;
+ email: string;
+ username?: string;
+ solvedCtf: string[];
+ numberOfSolvedCtf: number;
+}
+
+const API_BASE_URL =
+ import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
+
+// ✅ shared axios config (cookies-based auth)
+const api = axios.create({
+ baseURL: API_BASE_URL,
+ headers: { 'Content-Type': 'application/json' },
+ withCredentials: true, // REQUIRED for httpOnly cookie
+});
+
+export const authService = {
+ // ================= REGISTER =================
+ async register(data: RegisterData): Promise {
+ const response: AxiosResponse = await api.post('/signup', data);
+ return response.data;
+ },
+
+ // ================= LOGIN =================
+ async login(email: string, password: string): Promise {
+ if (!email || !password) {
+ throw new Error('Email or password missing');
+ }
+
+ const response: AxiosResponse<{ userWithoutPassword: User }> =
+ await api.post('/login', {
+ email,
+ password,
+ });
+
+ const user = response.data.userWithoutPassword;
+
+ // ✅ only store non-sensitive user info
+ localStorage.setItem('user', JSON.stringify(user));
+
+ return user;
+ },
+
+ // ================= LOGOUT =================
+ async logout(): Promise {
+ await api.post('/logout');
+ localStorage.removeItem('user');
+ },
+
+ // ================= CURRENT USER =================
+ getCurrentUser(): User | null {
+ const raw = localStorage.getItem('user');
+ return raw ? JSON.parse(raw) : null;
+ },
+
+ // ================= UPDATE USER =================
+ updateUser(user: User): void {
+ localStorage.setItem('user', JSON.stringify(user));
+ },
+};
+
+export default authService;
diff --git a/frontend/src/services/challengeService.ts b/frontend/src/services/challengeService.ts
new file mode 100644
index 0000000..a1ec155
--- /dev/null
+++ b/frontend/src/services/challengeService.ts
@@ -0,0 +1,52 @@
+import api from './api';
+
+export type ChallengeType =
+ | 'WEB_EXPLOIT'
+ | 'BINARY_EXPLOIT'
+ | 'CRYPTO'
+ | 'FORENSICS'
+ | 'REVERSE';
+export type Difficulty = 'EASY' | 'MID' | 'HARD';
+
+export interface Challenge {
+ _id: string;
+ name: string;
+ type: ChallengeType;
+ description: string;
+ difficulty: Difficulty;
+ points: number;
+ solves: number;
+ hints?: string[];
+ withSite?: boolean;
+ solved?: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export const challengeService = {
+ async getAllChallenges(
+ category?: string,
+ difficulty?: string,
+ ): Promise {
+ const params = new URLSearchParams();
+ if (category) params.append('type', category);
+ if (difficulty) params.append('difficulty', difficulty);
+
+ const response = await api.get(`/ctfs?${params.toString()}`);
+ return response.data.data || response.data || [];
+ },
+
+ async getChallengeById(id: string): Promise {
+ const response = await api.get(`/ctfs/${id}`);
+ return response.data.data || response.data;
+ },
+
+ async submitFlag(challengeId: string, flag: string) {
+ const response = await api.patch(`/ctfs/${challengeId}`, { flag });
+ return {
+ correct: response.data.success || response.data.correct,
+ message: response.data.message || 'Invalid flag',
+ points: response.data.points || 0,
+ };
+ },
+};
diff --git a/frontend/src/services/ctfService.ts b/frontend/src/services/ctfService.ts
new file mode 100644
index 0000000..35d9e86
--- /dev/null
+++ b/frontend/src/services/ctfService.ts
@@ -0,0 +1,106 @@
+import type { Ctf, CtfInstance } from '../types/ctf';
+
+const API_BASE_URL =
+ import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
+
+export const ctfService = {
+ // =========================
+ // Get all CTFs (public)
+ // =========================
+ async getAllCtfs(): Promise {
+ const response = await fetch(`${API_BASE_URL}/ctfs`, {
+ credentials: 'include', // Send cookies
+ });
+
+ if (!response.ok) {
+ const error = await response
+ .json()
+ .catch(() => ({ message: 'Failed to fetch CTFs' }));
+ throw new Error(error.message);
+ }
+
+ return response.json();
+ },
+
+ // =========================
+ // Get active instance
+ // =========================
+ async getActiveInstance(): Promise {
+ const response = await fetch(`${API_BASE_URL}/ctfs/instances`, {
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ const error = await response
+ .json()
+ .catch(() => ({ message: 'Failed to fetch active instance' }));
+ throw new Error(error.message);
+ }
+
+ const data = await response.json();
+ return data.instance;
+ },
+
+ // =========================
+ // Start a CTF instance
+ // =========================
+ async startInstance(ctfId: string): Promise {
+ const response = await fetch(`${API_BASE_URL}/ctfs/${ctfId}/instances`, {
+ method: 'POST',
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ const error = await response
+ .json()
+ .catch(() => ({ message: 'Failed to start instance' }));
+ throw new Error(error.message);
+ }
+
+ return response.json();
+ },
+
+ // =========================
+ // Stop a running instance
+ // =========================
+ async stopInstance(instanceId: string): Promise {
+ const response = await fetch(
+ `${API_BASE_URL}/ctfs/instances/${instanceId}`,
+ {
+ method: 'PATCH',
+ credentials: 'include',
+ },
+ );
+
+ if (!response.ok) {
+ const error = await response
+ .json()
+ .catch(() => ({ message: 'Failed to stop instance' }));
+ throw new Error(error.message);
+ }
+ },
+
+ // =========================
+ // Submit flag
+ // =========================
+ async submitFlag(
+ ctfId: string,
+ flag: string,
+ ): Promise<{ success: boolean; message: string }> {
+ const response = await fetch(`${API_BASE_URL}/ctfs/${ctfId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ flag }),
+ });
+
+ if (!response.ok) {
+ const error = await response
+ .json()
+ .catch(() => ({ message: 'Wrong flag' }));
+ throw new Error(error.message);
+ }
+
+ return response.json();
+ },
+};
diff --git a/frontend/src/services/instanceService.ts b/frontend/src/services/instanceService.ts
new file mode 100644
index 0000000..b6dc593
--- /dev/null
+++ b/frontend/src/services/instanceService.ts
@@ -0,0 +1,49 @@
+import api from './api';
+
+export type InstanceStatus = 'RUNNING' | 'STOPPED';
+
+export interface Instance {
+ _id: string; // MongoDB instance ID
+ containers: string[]; // Container IDs
+ userId: string;
+ ctfId: string; // Challenge ID
+ mainUrl: string; // URL for user to access the instance
+ expiresAt: string; // Expiration date
+ status: InstanceStatus;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export const instanceService = {
+ /**
+ * Start a new instance for a challenge
+ */
+ async startInstance(challengeId: string): Promise {
+ const response = await api.post(`/ctfs/${challengeId}/instances`, null, {
+ withCredentials: true,
+ });
+ return response.data || response.data.data; // handles both response shapes
+ },
+
+ /**
+ * Get the current user's active instance (if any)
+ */
+ async getActiveInstance(): Promise {
+ const response = await api.get(`/ctfs/instances`, {
+ withCredentials: true,
+ });
+ // Backend returns: { success: true, instance: {...} } or null
+ return response.data.instance || null;
+ },
+
+ /**
+ * Stop a running instance
+ */
+ async stopInstance(instanceId: string) {
+ const response = await api.patch(`/ctfs/instances/${instanceId}`, null, {
+ withCredentials: true,
+ });
+ return response.data; // { success: true, message: "CTF instance stopped successfully" }
+ },
+};
+export default instanceService;
diff --git a/frontend/src/services/submissionService.ts b/frontend/src/services/submissionService.ts
new file mode 100644
index 0000000..ef00401
--- /dev/null
+++ b/frontend/src/services/submissionService.ts
@@ -0,0 +1,86 @@
+// src/services/submissionService.ts
+
+export interface Submission {
+ _id: string;
+ ctfId: string;
+ userId: string;
+ flag: string;
+ correct: boolean;
+ submittedAt: string;
+ points?: number;
+ ctf?: { _id: string; name: string };
+}
+
+export interface LeaderboardEntry {
+ rank: number;
+ username: string;
+ teamName?: string;
+ score: number;
+ solvedCount: number;
+ _id?: string;
+}
+
+export const submissionService = {
+ // ✅ MOCK: user submissions
+ async getMySubmissions(): Promise {
+ return [
+ {
+ _id: 'sub1',
+ ctfId: 'ctf1',
+ userId: 'me',
+ flag: '***HIDDEN***',
+ correct: true,
+ submittedAt: new Date().toISOString(),
+ points: 50,
+ ctf: { _id: 'ctf1', name: 'Crypto Challenge' },
+ },
+ {
+ _id: 'sub2',
+ ctfId: 'ctf2',
+ userId: 'me',
+ flag: '***HIDDEN***',
+ correct: false,
+ submittedAt: new Date().toISOString(),
+ points: 0,
+ ctf: { _id: 'ctf2', name: 'Web Exploit' },
+ },
+ ];
+ },
+
+ // ✅ MOCK: leaderboard
+ async getLeaderboard(): Promise {
+ return [
+ { rank: 1, username: 'Alice', score: 120, solvedCount: 10, _id: 'u1' },
+ { rank: 2, username: 'Bob', score: 100, solvedCount: 8, _id: 'u2' },
+ { rank: 3, username: 'Charlie', score: 80, solvedCount: 6, _id: 'u3' },
+ { rank: 4, username: 'David', score: 60, solvedCount: 4, _id: 'u4' },
+ { rank: 5, username: 'Eve', score: 50, solvedCount: 3, _id: 'u5' },
+ ];
+ },
+
+ // ✅ MOCK: all submissions (for admin/testing)
+ async getAllSubmissions(): Promise {
+ return [
+ {
+ _id: 'sub1',
+ ctfId: 'ctf1',
+ userId: 'u1',
+ flag: '***HIDDEN***',
+ correct: true,
+ submittedAt: new Date().toISOString(),
+ points: 50,
+ ctf: { _id: 'ctf1', name: 'Crypto Challenge' },
+ },
+ {
+ _id: 'sub2',
+ ctfId: 'ctf2',
+ userId: 'u2',
+ flag: '***HIDDEN***',
+ correct: false,
+ submittedAt: new Date().toISOString(),
+ points: 0,
+ ctf: { _id: 'ctf2', name: 'Web Exploit' },
+ },
+ ];
+ },
+};
diff --git a/frontend/src/types/ctf.ts b/frontend/src/types/ctf.ts
new file mode 100644
index 0000000..55dab1d
--- /dev/null
+++ b/frontend/src/types/ctf.ts
@@ -0,0 +1,29 @@
+export type CtfDifficulty = 'EASY' | 'MID' | 'HARD';
+export type CtfType = 'WEB_EXPLOIT' | 'BE' | 'OTHER';
+export type InstanceState =
+ | 'RUNNING'
+ | 'STOPPED'
+ | 'PENDING'
+ | 'FAILED'
+ | 'TERMINATED';
+
+export interface Ctf {
+ id: string;
+ name: string;
+ type: CtfType;
+ description?: string;
+ difficulty: CtfDifficulty;
+ hints: string[];
+ resources: string[];
+ withSite: boolean;
+ solved?: boolean;
+}
+
+export interface CtfInstance {
+ id: string;
+ ctfId: string;
+ userId: string;
+ status: InstanceState;
+ url?: string;
+ expiresAt: Date;
+}
diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts
new file mode 100644
index 0000000..3064d71
--- /dev/null
+++ b/frontend/src/utils/constants.ts
@@ -0,0 +1,27 @@
+export const API_BASE_URL =
+ import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
+
+export const CATEGORIES = [
+ { id: 'web', name: 'Web', icon: '🌐' },
+ { id: 'crypto', name: 'Cryptography', icon: '🔐' },
+ { id: 'reverse', name: 'Reverse Engineering', icon: '🔄' },
+ { id: 'forensics', name: 'Forensics', icon: '🔍' },
+ { id: 'pwn', name: 'Binary Exploitation', icon: '💥' },
+];
+
+export const DIFFICULTIES = [
+ { id: 'easy', name: 'Easy', color: 'green' },
+ { id: 'medium', name: 'Medium', color: 'orange' },
+ { id: 'hard', name: 'Hard', color: 'red' },
+];
+
+export const ROUTES = {
+ HOME: '/',
+ LOGIN: '/login',
+ REGISTER: '/register',
+ CHALLENGES: '/challenges',
+ CHALLENGE_DETAIL: '/challenges/:id',
+ LEADERBOARD: '/leaderboard',
+ PROFILE: '/profile',
+ ADMIN: '/admin',
+};
diff --git a/frontend/src/utils/errorHandler.ts b/frontend/src/utils/errorHandler.ts
new file mode 100644
index 0000000..b41d230
--- /dev/null
+++ b/frontend/src/utils/errorHandler.ts
@@ -0,0 +1,36 @@
+import { AxiosError } from 'axios';
+
+export const getErrorMessage = (error: unknown): string => {
+ if (error instanceof Error) {
+ // Check if it's an Axios error
+ if ('response' in error && error.response) {
+ const axiosError = error as AxiosError<{ message?: string }>;
+ const status = axiosError.response?.status;
+ const message = axiosError.response?.data?.message;
+
+ // Handle specific status codes
+ switch (status) {
+ case 429:
+ return 'Too many requests. Please wait a moment before trying again.';
+ case 400:
+ return message || 'Invalid request. Please check your input.';
+ case 401:
+ return 'You need to be logged in to perform this action.';
+ case 403:
+ return 'You do not have permission to perform this action.';
+ case 404:
+ return 'The requested resource was not found.';
+ case 500:
+ return 'Server error. Please try again later.';
+ case 503:
+ return 'Service temporarily unavailable. Please try again later.';
+ default:
+ return message || error.message || 'An unexpected error occurred.';
+ }
+ }
+
+ return error.message;
+ }
+
+ return 'An unexpected error occurred.';
+};
diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts
new file mode 100644
index 0000000..3064d71
--- /dev/null
+++ b/frontend/src/utils/helpers.ts
@@ -0,0 +1,27 @@
+export const API_BASE_URL =
+ import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
+
+export const CATEGORIES = [
+ { id: 'web', name: 'Web', icon: '🌐' },
+ { id: 'crypto', name: 'Cryptography', icon: '🔐' },
+ { id: 'reverse', name: 'Reverse Engineering', icon: '🔄' },
+ { id: 'forensics', name: 'Forensics', icon: '🔍' },
+ { id: 'pwn', name: 'Binary Exploitation', icon: '💥' },
+];
+
+export const DIFFICULTIES = [
+ { id: 'easy', name: 'Easy', color: 'green' },
+ { id: 'medium', name: 'Medium', color: 'orange' },
+ { id: 'hard', name: 'Hard', color: 'red' },
+];
+
+export const ROUTES = {
+ HOME: '/',
+ LOGIN: '/login',
+ REGISTER: '/register',
+ CHALLENGES: '/challenges',
+ CHALLENGE_DETAIL: '/challenges/:id',
+ LEADERBOARD: '/leaderboard',
+ PROFILE: '/profile',
+ ADMIN: '/admin',
+};
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..257232f
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [
+ react({
+ babel: {
+ plugins: [['babel-plugin-react-compiler']],
+ },
+ }),
+ ],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '/api/v1')
+ }
+ }
+ },
+ build: {
+ outDir: 'dist',
+ },
+});